/*
 * Copyright (c) 2008-2016 Haulmont.
 *
 * 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 com.haulmont.cuba.core.global;

import org.apache.commons.lang3.exception.ExceptionUtils;

import javax.annotation.Nullable;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

/**
 * Exception that returns to clients from the middleware. Contains the information about the whole server-side
 * exception chain in the <code>Cause</code> objects list. Actual exception instances are included only if they
 * explicitly declared as available for the clients (annotated with {@link SupportedByClient}).
 *
 */
public class RemoteException extends RuntimeException {

    private static final long serialVersionUID = -681950463552884310L;

    public static class Cause implements Serializable {

        private static final long serialVersionUID = 7677717005347643512L;

        private String className;
        private String message;
        private Throwable throwable;

        public Cause(Throwable throwable) {
            this.className = throwable.getClass().getName();
            this.message = throwable.getMessage();
            this.throwable = throwable;
        }

        public Cause(String className, String message) {
            this.className = className;
            this.message = message;
        }

        public String getClassName() {
            return className;
        }

        public String getMessage() {
            return message;
        }

        @Nullable
        public Throwable getThrowable() {
            return throwable;
        }

        @Override
        public String toString() {
            return className + ": " + message;
        }
    }

    private List<Cause> causes = new ArrayList<>();

    @SuppressWarnings("unchecked")
    public RemoteException(Throwable throwable) {
        List<Throwable> list = ExceptionUtils.getThrowableList(throwable);
        for (int i = 0; i < list.size(); i++) {
            Throwable t = list.get(i);
            boolean suitable = true;
            List<Throwable> causesOfT = list.subList(i, list.size());
            for (Throwable aCauseOfT : causesOfT) {
                if (!isSuitable(aCauseOfT)) {
                    suitable = false;
                    break;
                }
            }
            if (suitable)
                causes.add(new Cause(t));
            else
                causes.add(new Cause(t.getClass().getName(), t.getMessage()));
        }
    }

    private boolean isSuitable(Throwable throwable) {
        return (throwable.getClass().getName().startsWith("java.")
                || throwable.getClass().isAnnotationPresent(SupportedByClient.class));
    }

    public List<Cause> getCauses() {
        return Collections.unmodifiableList(causes);
    }

    /**
     * Search for {@link Cause} by its exception class name. Subclasses are not taken into account.
     *
     * @param throwableClassName exception class name
     * @return Cause instance or null if it is not found
     */
    @Nullable
    public Cause getCause(String throwableClassName) {
        for (Cause cause : causes) {
            if (cause.getClassName().equals(throwableClassName)) {
                return cause;
            }
        }
        return null;
    }

    /**
     * Search for {@link Cause} by its exception class. Subclasses are not taken into account. The given class is
     * converted into its string name, so it doesn't matter what Cause contains - class or its name only.
     *
     * @param throwableClass exception class
     * @return true if such cause is found
     */
    public boolean contains(Class<?> throwableClass) {
        Objects.requireNonNull(throwableClass, "throwableClass is null");
        return getCause(throwableClass.getName()) != null;
    }

    /**
     * Search for {@link Cause} by its exception class. Subclasses are not taken into account.
     *
     * @param throwableClassName exception class name
     * @return true if such cause is found
     */
    public boolean contains(String throwableClassName) {
        return getCause(throwableClassName) != null;
    }

    /**
     * @return First exception in the causes list if it is checked or it is supported by client, null otherwise
     */
    @Nullable
    @SuppressWarnings({"ThrowableResultOfMethodCallIgnored"})
    public Exception getFirstCauseException() {
        if (!causes.isEmpty()) {
            Throwable t = causes.get(0).getThrowable();
            if (t != null) {
                if (!(t instanceof RuntimeException) && !(t instanceof Error)) {
                    return (Exception) t;
                }
                if (!(t instanceof Error) && t.getClass().getAnnotation(SupportedByClient.class) != null)
                    return (Exception) t;
            }
        }
        return null;
    }

    @Override
    @Nullable
    public String getMessage() {
        if (!causes.isEmpty())
            return causes.get(causes.size() - 1).getMessage();
        else
            return null;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("com.haulmont.cuba.core.global.RemoteException:");
        for (Cause cause : causes) {
            sb.append("\n---\n").append(cause.getClassName()).append(": ").append(cause.getMessage());
        }
        return sb.toString();
    }
}
