/*
 * Copyright Terracotta, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.ehcache.clustered.common.internal.messages;

import org.ehcache.clustered.common.internal.exceptions.ClusterException;
import org.ehcache.clustered.common.internal.exceptions.UnknownClusterException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terracotta.runnel.Struct;
import org.terracotta.runnel.StructBuilder;
import org.terracotta.runnel.decoding.StructArrayDecoder;
import org.terracotta.runnel.decoding.StructDecoder;
import org.terracotta.runnel.encoding.StructArrayEncoder;
import org.terracotta.runnel.encoding.StructEncoder;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

final class ExceptionCodec {

  private ExceptionCodec() {
    //no instances please
  }

  private static final Logger LOGGER = LoggerFactory.getLogger(ExceptionCodec.class);

  private static final String DECLARING_CLASS_FIELD = "declaringClass";
  private static final String METHOD_NAME_FIELD = "methodName";
  private static final String FILE_NAME_FIELD = "fileName";
  private static final String LINE_NUM_FIELD = "lineNumber";
  private static final String FQCN_FIELD = "fqcn";
  private static final String MESSAGE_FIELD = "message";
  private static final String STACKTRACE_ELEMENTS_FIELD = "stacktraceElements";

  private static final Struct STE_STRUCT = StructBuilder.newStructBuilder()
      .string(DECLARING_CLASS_FIELD, 10)
      .string(METHOD_NAME_FIELD, 20)
      .string(FILE_NAME_FIELD, 30)
      .int32(LINE_NUM_FIELD, 40)
      .build();

  static final Struct EXCEPTION_STRUCT = StructBuilder.newStructBuilder()
      .string(FQCN_FIELD, 10)
      .string(MESSAGE_FIELD, 20)
      .structs(STACKTRACE_ELEMENTS_FIELD, 30, STE_STRUCT)
      .build();

  public static void encode(StructEncoder<?> encoder, ClusterException exception) {
    encoder.string(FQCN_FIELD, exception.getClass().getCanonicalName());
    encoder.string(MESSAGE_FIELD, exception.getMessage());
    StructArrayEncoder<?> arrayEncoder = encoder.structs(STACKTRACE_ELEMENTS_FIELD);
    for (StackTraceElement stackTraceElement : exception.getStackTrace()) {
      StructEncoder<?> element = arrayEncoder.add();
      element.string(DECLARING_CLASS_FIELD, stackTraceElement.getClassName());
      element.string(METHOD_NAME_FIELD, stackTraceElement.getMethodName());
      if (stackTraceElement.getFileName() != null) {
        element.string(FILE_NAME_FIELD, stackTraceElement.getFileName());
      }
      element.int32(LINE_NUM_FIELD, stackTraceElement.getLineNumber());
      element.end();
    }
    arrayEncoder.end();
  }

  public static ClusterException decode(StructDecoder<StructDecoder<Void>> decoder) {
    String exceptionClassName = decoder.string(FQCN_FIELD);
    String message = decoder.string(MESSAGE_FIELD);
    StructArrayDecoder<StructDecoder<StructDecoder<Void>>> arrayDecoder = decoder.structs(STACKTRACE_ELEMENTS_FIELD);
    StackTraceElement[] stackTraceElements = new StackTraceElement[arrayDecoder.length()];
    for (int i = 0; i < arrayDecoder.length(); i++) {
      StructDecoder<?> element = arrayDecoder.next();
      stackTraceElements[i] = new StackTraceElement(
        element.string(DECLARING_CLASS_FIELD),
        element.string(METHOD_NAME_FIELD),
        element.string(FILE_NAME_FIELD),
        element.int32(LINE_NUM_FIELD));
      element.end();
    }
    arrayDecoder.end();
    Class<? extends ClusterException> clazz = null;
    ClusterException exception;
    try {
      clazz = Class.forName(exceptionClassName).asSubclass(ClusterException.class);
    } catch (ClassNotFoundException e) {
      LOGGER.error("Exception type not found", e);
    }
    exception = getClusterException(message, clazz);
    if (exception == null) {
      exception = new UnknownClusterException(message);
    }
    exception.setStackTrace(stackTraceElements);
    return exception;
  }

  @SuppressWarnings("unchecked")
  private static ClusterException getClusterException(String message, Class<? extends ClusterException> clazz) {
    ClusterException exception = null;
    if (clazz != null) {
      try {
        Constructor<? extends ClusterException> declaredConstructor = clazz.getDeclaredConstructor(String.class);
        exception = declaredConstructor.newInstance(message);
      } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
        LOGGER.error("Failed to instantiate exception object.", e);
      }
    }
    return exception;
  }

}