/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.flink.util;

import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Serializable;
import java.lang.ref.WeakReference;
import java.util.HashSet;
import java.util.Set;

/**
 * Utility class for dealing with user-defined Throwable types that are serialized (for example
 * during RPC/Actor communication), but cannot be resolved with the default class loader.
 *
 * <p>This exception mimics the original exception with respect to message and stack trace, and
 * contains the original exception in serialized form. The original exception can be re-obtained by
 * supplying the appropriate class loader.
 */
public class SerializedThrowable extends Exception implements Serializable {

    private static final long serialVersionUID = 7284183123441947635L;

    /** The original exception in serialized form. */
    private final byte[] serializedException;

    /** Name of the original error class. */
    private final String originalErrorClassName;

    /** The original stack trace, to be printed. */
    private final String fullStringifiedStackTrace;

    /**
     * The original exception, not transported via serialization, because the class may not be part
     * of the system class loader. In addition, we make sure our cached references to not prevent
     * unloading the exception class.
     */
    private transient WeakReference<Throwable> cachedException;

    /**
     * Create a new SerializedThrowable.
     *
     * @param exception The exception to serialize.
     */
    public SerializedThrowable(Throwable exception) {
        this(exception, new HashSet<>());
    }

    private SerializedThrowable(Throwable exception, Set<Throwable> alreadySeen) {
        super(getClassNameAndMessageOrError(exception));

        if (!(exception instanceof SerializedThrowable)) {
            // serialize and memoize the original message
            byte[] serialized;
            // introduce the synchronization here to avoid deadlock of multi thread serializing
            // exceptions
            synchronized (SerializedThrowable.class) {
                try {
                    serialized = InstantiationUtil.serializeObject(exception);
                } catch (Throwable t) {
                    serialized = null;
                }
            }
            this.serializedException = serialized;
            this.cachedException = new WeakReference<>(exception);

            // record the original exception's properties (name, stack prints)
            this.originalErrorClassName = exception.getClass().getName();
            this.fullStringifiedStackTrace = ExceptionUtils.stringifyException(exception);

            // mimic the original exception's stack trace
            setStackTrace(exception.getStackTrace());

            // mimic the original exception's cause
            if (exception.getCause() == null) {
                initCause(null);
            } else {
                // exception causes may by cyclic, so we truncate the cycle when we find it
                if (alreadySeen.add(exception)) {
                    // we are not in a cycle, yet
                    initCause(new SerializedThrowable(exception.getCause(), alreadySeen));
                }
            }
            // mimic suppressed exceptions
            this.addAllSuppressed(exception.getSuppressed(), alreadySeen);
        } else {
            // copy from that serialized throwable
            SerializedThrowable other = (SerializedThrowable) exception;
            this.serializedException = other.serializedException;
            this.originalErrorClassName = other.originalErrorClassName;
            this.fullStringifiedStackTrace = other.fullStringifiedStackTrace;
            this.cachedException = other.cachedException;
            this.setStackTrace(other.getStackTrace());
            this.initCause(other.getCause());
            this.addAllSuppressed(other.getSuppressed(), alreadySeen);
        }
    }

    public Throwable deserializeError(ClassLoader classloader) {
        if (serializedException == null) {
            // failed to serialize the original exception
            // return this SerializedThrowable as a stand in
            return this;
        }

        Throwable cached = cachedException == null ? null : cachedException.get();
        if (cached == null) {
            try {
                cached = InstantiationUtil.deserializeObject(serializedException, classloader);
                cachedException = new WeakReference<>(cached);
            } catch (Throwable t) {
                // something went wrong
                // return this SerializedThrowable as a stand in
                return this;
            }
        }
        return cached;
    }

    public String getOriginalErrorClassName() {
        return originalErrorClassName;
    }

    public byte[] getSerializedException() {
        return serializedException;
    }

    public String getFullStringifiedStackTrace() {
        return fullStringifiedStackTrace;
    }

    /**
     * Add all suppressed exceptions to this exception.
     *
     * @param suppressed The suppressed exceptions to add.
     * @param alreadySeen The set of exceptions that have already been seen.
     */
    private void addAllSuppressed(Throwable[] suppressed, Set<Throwable> alreadySeen) {
        for (Throwable s : suppressed) {
            if (alreadySeen.add(s)) {
                SerializedThrowable serializedThrowable;
                if (s instanceof SerializedThrowable) {
                    serializedThrowable = (SerializedThrowable) s;
                } else {
                    serializedThrowable = new SerializedThrowable(s);
                }
                this.addSuppressed(serializedThrowable);
            }
        }
    }

    // ------------------------------------------------------------------------
    //  Override the behavior of Throwable
    // ------------------------------------------------------------------------

    @Override
    public void printStackTrace(PrintStream s) {
        s.print(fullStringifiedStackTrace);
        s.flush();
    }

    @Override
    public void printStackTrace(PrintWriter s) {
        s.print(fullStringifiedStackTrace);
        s.flush();
    }

    @Override
    public String toString() {
        String message = getLocalizedMessage();
        return (message != null)
                ? (originalErrorClassName + ": " + message)
                : originalErrorClassName;
    }

    // ------------------------------------------------------------------------
    //  Static utilities
    // ------------------------------------------------------------------------

    public static Throwable get(Throwable serThrowable, ClassLoader loader) {
        if (serThrowable instanceof SerializedThrowable) {
            return ((SerializedThrowable) serThrowable).deserializeError(loader);
        } else {
            return serThrowable;
        }
    }

    private static String getClassNameAndMessageOrError(Throwable error) {
        try {
            String className = error.getClass().getName();
            String message = error.getMessage();
            if (message != null) {
                return String.format("%s: %s", className, message);
            }
            return className;
        } catch (Throwable t) {
            return "(failed to get message)";
        }
    }
}
