/*
 * Copyright The WildFly Authors
 * SPDX-License-Identifier: Apache-2.0
 */

package org.wildfly.clustering.marshalling.protostream;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.LinkedList;
import java.util.List;

import org.infinispan.protostream.descriptors.WireType;

/**
 * Generic marshaller for a Throwable.
 * @author Paul Ferraro
 * @param <E> the target type of this marshaller
 */
public class ExceptionMarshaller<E extends Throwable> implements ProtoStreamMarshaller<E> {

    private static final int CLASS_INDEX = 1;
    private static final int MESSAGE_INDEX = 2;
    private static final int CAUSE_INDEX = 3;
    private static final int STACK_TRACE_ELEMENT_INDEX = 4;
    private static final int SUPPRESSED_INDEX = 5;

    private final Class<E> exceptionClass;

    public ExceptionMarshaller(Class<E> exceptionClass) {
        this.exceptionClass = exceptionClass;
    }

    @Override
    public Class<? extends E> getJavaClass() {
        return this.exceptionClass;
    }

    @Override
    public E readFrom(ProtoStreamReader reader) throws IOException {
        Class<E> exceptionClass = this.exceptionClass;
        String message = null;
        Throwable cause = null;
        List<StackTraceElement> stackTrace = new LinkedList<>();
        List<Throwable> suppressed = new LinkedList<>();
        while (!reader.isAtEnd()) {
            int tag = reader.readTag();
            switch (WireType.getTagFieldNumber(tag)) {
                case CLASS_INDEX:
                    exceptionClass = reader.readObject(Class.class);
                    break;
                case MESSAGE_INDEX:
                    message = reader.readString();
                    break;
                case CAUSE_INDEX:
                    cause = reader.readAny(Throwable.class);
                    break;
                case STACK_TRACE_ELEMENT_INDEX:
                    stackTrace.add(reader.readObject(StackTraceElement.class));
                    break;
                case SUPPRESSED_INDEX:
                    suppressed.add(reader.readAny(Throwable.class));
                    break;
                default:
                    reader.skipField(tag);
            }
        }
        E exception = this.createException(exceptionClass, message, cause);
        if (!stackTrace.isEmpty()) {
            exception.setStackTrace(stackTrace.toArray(new StackTraceElement[0]));
        }
        for (Throwable e : suppressed) {
            exception.addSuppressed(e);
        }
        return exception;
    }

    @Override
    public void writeTo(ProtoStreamWriter writer, E exception) throws IOException {
        if (this.exceptionClass == Throwable.class) {
            writer.writeObject(CLASS_INDEX, exception.getClass());
        }
        String message = exception.getMessage();
        Throwable cause = exception.getCause();
        // Avoid serializing redundant message
        if ((message != null) && ((cause == null) || !cause.toString().equals(message))) {
            writer.writeString(MESSAGE_INDEX, message);
        }
        if (cause != null) {
            writer.writeAny(CAUSE_INDEX, cause);
        }
        for (StackTraceElement element : exception.getStackTrace()) {
            writer.writeObject(STACK_TRACE_ELEMENT_INDEX, element);
        }
        for (Throwable suppressed : exception.getSuppressed()) {
            writer.writeAny(SUPPRESSED_INDEX, suppressed);
        }
    }

    private E createException(Class<E> exceptionClass, String message, Throwable cause) throws IOException {
        Constructor<E> emptyConstructor = this.getConstructor(exceptionClass);
        Constructor<E> messageConstructor = this.getConstructor(exceptionClass, String.class);
        Constructor<E> causeConstructor = this.getConstructor(exceptionClass, Throwable.class);
        Constructor<E> messageCauseConstructor = this.getConstructor(exceptionClass, String.class, Throwable.class);
        try {
            if (cause != null) {
                if (message != null) {
                    if (messageCauseConstructor != null) {
                        return messageCauseConstructor.newInstance(message, cause);
                    }
                } else {
                    if (causeConstructor != null) {
                        return causeConstructor.newInstance(cause);
                    }
                }
            }
            E exception = (message != null) ? ((messageConstructor != null) ? messageConstructor.newInstance(message) : null) : ((emptyConstructor != null) ? emptyConstructor.newInstance() : null);
            if (exception == null) {
                throw new NoSuchMethodException(String.format("%s(%s)", exceptionClass.getName(), (message != null) ? String.class.getName() : ""));
            }
            if (cause != null) {
                exception.initCause(cause);
            }
            return exception;
        } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
            throw new IOException(e);
        }
    }

    private Constructor<E> getConstructor(Class<E> exceptionClass, Class<?>... parameterTypes) {
        try {
            return exceptionClass.getConstructor(parameterTypes);
        } catch (NoSuchMethodException e) {
            return null;
        }
    }
}
