/*
 * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.jersey.internal;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.logging.Logger;

import org.glassfish.jersey.Severity;
import org.glassfish.jersey.internal.util.Producer;

/**
 * Errors utility used to file processing messages (e.g. validation, provider, resource building errors, hint).
 * <p/>
 * Error filing methods ({@code #warning}, {@code #error}, {@code #fatal}) can be invoked only in the "error scope" which is
 * created by {@link #process(Producer)} or
 * {@link #processWithException(Producer)} methods. Filed error messages are present also in this
 * scope.
 * <p/>
 * TODO do not use static thread local?
 *
 * @author Michal Gajdos
 */
public class Errors {

    private static final Logger LOGGER = Logger.getLogger(Errors.class.getName());

    private static final ThreadLocal<Errors> errors = new ThreadLocal<Errors>();

    /**
     * Add an error to the list of messages.
     *
     * @param message  message of the error.
     * @param severity indicates severity of added error.
     */
    public static void error(final String message, Severity severity) {
        error(null, message, severity);
    }

    /**
     * Add an error to the list of messages.
     *
     * @param source   source of the error.
     * @param message  message of the error.
     * @param severity indicates severity of added error.
     */
    public static void error(final Object source, final String message, final Severity severity) {
        getInstance().issues.add(new ErrorMessage(source, message, severity));
    }

    /**
     * Add a fatal error to the list of messages.
     *
     * @param source  source of the error.
     * @param message message of the error.
     */
    public static void fatal(final Object source, final String message) {
        error(source, message, Severity.FATAL);
    }

    /**
     * Add a warning to the list of messages.
     *
     * @param source  source of the error.
     * @param message message of the error.
     */
    public static void warning(final Object source, final String message) {
        error(source, message, Severity.WARNING);
    }

    /**
     * Add a hint to the list of messages.
     *
     * @param source  source of the error.
     * @param message message of the error.
     */
    public static void hint(final Object source, final String message) {
        getInstance().issues.add(new ErrorMessage(source, message, Severity.HINT));
    }

    /**
     * Log errors and throw an exception if there are any fatal issues detected and
     * the {@code throwException} flag has been set to {@code true}.
     *
     * @param throwException if set to {@code true}, any fatal issues will cause a {@link ErrorMessagesException}
     *                       to be thrown.
     */
    private static void processErrors(final boolean throwException) {
        final List<ErrorMessage> errors = new ArrayList<ErrorMessage>(Errors.errors.get().issues);
        boolean isFatal = logErrors(errors);
        if (throwException && isFatal) {
            throw new ErrorMessagesException(errors);
        }
    }

    /**
     * Log errors and return a status flag indicating whether a fatal issue has been found
     * in the error collection.
     * <p>
     * The {@code afterMark} flag indicates whether only those issues should be logged that were
     * added after a {@link #mark() mark has been set}.
     * </p>
     *
     * @param afterMark if {@code true}, only issues added after a mark has been set are returned,
     *                  if {@code false} all issues are returned.
     * @return {@code true} if there are any fatal issues present in the collection, {@code false}
     *         otherwise.
     */
    public static boolean logErrors(final boolean afterMark) {
        return logErrors(getInstance()._getErrorMessages(afterMark));
    }

    /**
     * Log supplied errors and return a status flag indicating whether a fatal issue has been found
     * in the error collection.
     *
     * @param errors a collection of errors to be logged.
     * @return {@code true} if there are any fatal issues present in the collection, {@code false}
     *         otherwise.
     */
    private static boolean logErrors(final Collection<ErrorMessage> errors) {
        boolean isFatal = false;

        if (!errors.isEmpty()) {
            StringBuilder fatals = new StringBuilder("\n");
            StringBuilder warnings = new StringBuilder();
            StringBuilder hints = new StringBuilder();

            for (final ErrorMessage error : errors) {
                switch (error.getSeverity()) {
                    case FATAL:
                        isFatal = true;
                        fatals.append(LocalizationMessages.ERROR_MSG(error.getMessage())).append('\n');
                        break;
                    case WARNING:
                        warnings.append(LocalizationMessages.WARNING_MSG(error.getMessage())).append('\n');
                        break;
                    case HINT:
                        hints.append(LocalizationMessages.HINT_MSG(error.getMessage())).append('\n');
                        break;
                }
            }

            if (isFatal) {
                LOGGER.severe(LocalizationMessages.ERRORS_AND_WARNINGS_DETECTED(fatals.append(warnings)
                        .append(hints).toString()));
            } else {
                if (warnings.length() > 0) {
                    LOGGER.warning(LocalizationMessages.WARNINGS_DETECTED(warnings.toString()));
                }

                if (hints.length() > 0) {
                    LOGGER.config(LocalizationMessages.HINTS_DETECTED(hints.toString()));
                }
            }
        }

        return isFatal;
    }


    /**
     * Check whether a fatal error is present in the list of all messages.
     *
     * @return {@code true} if there are any fatal issues in this error context, {@code false} otherwise.
     */
    public static boolean fatalIssuesFound() {
        for (final ErrorMessage message : getInstance().issues) {
            if (message.getSeverity() == Severity.FATAL) {
                return true;
            }
        }
        return false;
    }

    /**
     * Invoke given producer task and gather errors.
     * <p/>
     * After the task is complete all gathered errors are logged. No exception is thrown
     * even if there is a fatal error present in the list of errors.
     *
     * @param producer producer task to be invoked.
     * @return the result produced by the task.
     */
    public static <T> T process(final Producer<T> producer) {
        return process(producer, false);
    }

    /**
     * Invoke given callable task and gather messages.
     * <p/>
     * After the task is complete all gathered errors are logged. Any exception thrown
     * by the throwable is re-thrown.
     *
     * @param task callable task to be invoked.
     * @return the result produced by the task.
     * @throws Exception exception thrown by the task.
     */
    public static <T> T process(final Callable<T> task) throws Exception {
        return process(task, true);
    }

    /**
     * Invoke given producer task and gather messages.
     * <p/>
     * After the task is complete all gathered errors are logged. If there is a fatal error
     * present in the list of errors an {@link ErrorMessagesException exception} is thrown.
     *
     * @param producer producer task to be invoked.
     * @return the result produced by the task.
     */
    public static <T> T processWithException(final Producer<T> producer) {
        return process(producer, true);
    }

    /**
     * Invoke given task and gather messages.
     * <p/>
     * After the task is complete all gathered errors are logged. No exception is thrown
     * even if there is a fatal error present in the list of errors.
     *
     * @param task task to be invoked.
     */
    public static void process(final Runnable task) {
        process(new Producer<Void>() {

            @Override
            public Void call() {
                task.run();
                return null;
            }
        }, false);
    }

    /**
     * Invoke given task and gather messages.
     * <p/>
     * After the task is complete all gathered errors are logged. If there is a fatal error
     * present in the list of errors an {@link ErrorMessagesException exception} is thrown.
     *
     * @param task task to be invoked.
     */
    public static void processWithException(final Runnable task) {
        process(new Producer<Void>() {
            @Override
            public Void call() {
                task.run();
                return null;
            }
        }, true);
    }

    private static <T> T process(final Producer<T> task, final boolean throwException) {
        try {
            return process((Callable<T>) task, throwException);
        } catch (RuntimeException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    private static <T> T process(final Callable<T> task, final boolean throwException) throws Exception {
        Errors instance = errors.get();
        if (instance == null) {
            instance = new Errors();
            errors.set(instance);
        }
        instance.preProcess();

        Exception caught = null;
        try {
            return task.call();
        } catch (Exception re) {
            // If a runtime exception is caught then report errors and rethrow.
            caught = re;
        } finally {
            instance.postProcess(throwException && caught == null);
        }

        throw caught;
    }

    private static Errors getInstance() {
        final Errors instance = errors.get();
        // No error processing in scope
        if (instance == null) {
            throw new IllegalStateException(LocalizationMessages.NO_ERROR_PROCESSING_IN_SCOPE());
        }
        // The following should not be necessary but given the fragile nature of
        // static thread local probably best to add it in case some internals of
        // this class change
        if (instance.stack == 0) {
            errors.remove();
            throw new IllegalStateException(LocalizationMessages.NO_ERROR_PROCESSING_IN_SCOPE());
        }
        return instance;
    }

    /**
     * Get the list of all error messages.
     *
     * @return non-null error message list.
     */
    public static List<ErrorMessage> getErrorMessages() {
        return getErrorMessages(false);
    }

    /**
     * Get the list of error messages.
     * <p>
     * The {@code afterMark} flag indicates whether only those issues should be returned that were
     * added after a {@link #mark() mark has been set}.
     * </p>
     *
     * @param afterMark if {@code true}, only issues added after a mark has been set are returned,
     *                  if {@code false} all issues are returned.
     * @return non-null error list.
     */
    public static List<ErrorMessage> getErrorMessages(final boolean afterMark) {
        return getInstance()._getErrorMessages(afterMark);
    }

    /**
     * Set a mark at a current position in the errors messages list.
     */
    public static void mark() {
        getInstance()._mark();
    }

    /**
     * Remove a previously set mark, if any.
     */
    public static void unmark() {
        getInstance()._unmark();
    }

    /**
     * Removes all issues that have been added since the last marked position as well as
     * removes the last mark.
     */
    public static void reset() {
        getInstance()._reset();
    }

    private final ArrayList<ErrorMessage> issues = new ArrayList<ErrorMessage>(0);

    private Errors() {
    }

    private Deque<Integer> mark = new ArrayDeque<Integer>(4);
    private int stack = 0;

    private void _mark() {
        mark.addLast(issues.size());
    }

    private void _unmark() {
        mark.pollLast();
    }

    private void _reset() {
        final Integer _pos = mark.pollLast(); // also performs "unmark" functionality
        final int markedPos = (_pos == null) ? -1 : _pos;

        if (markedPos >= 0 && markedPos < issues.size()) {
            issues.subList(markedPos, issues.size()).clear();
        }
    }

    private void preProcess() {
        stack++;
    }

    private void postProcess(boolean throwException) {
        stack--;

        if (stack == 0) {
            try {
                if (!issues.isEmpty()) {
                    processErrors(throwException);
                }
            } finally {
                errors.remove();
            }
        }
    }

    private List<ErrorMessage> _getErrorMessages(final boolean afterMark) {
        if (afterMark) {
            final Integer _pos = mark.peekLast();
            final int markedPos = (_pos == null) ? -1 : _pos;

            if (markedPos >= 0 && markedPos < issues.size()) {
                return Collections.unmodifiableList(new ArrayList<ErrorMessage>(issues.subList(markedPos, issues.size())));
            } // else return all errors
        }

        return Collections.unmodifiableList(new ArrayList<ErrorMessage>(issues));
    }

    /**
     * Error message exception.
     */
    public static class ErrorMessagesException extends RuntimeException {

        private final List<ErrorMessage> messages;

        private ErrorMessagesException(final List<ErrorMessage> messages) {
            this.messages = messages;
        }

        /**
         * Get encountered error messages.
         *
         * @return encountered error messages.
         */
        public List<ErrorMessage> getMessages() {
            return messages;
        }
    }

    /**
     * Generic error message.
     */
    public static class ErrorMessage {

        private final Object source;
        private final String message;
        private final Severity severity;

        private ErrorMessage(final Object source, final String message, Severity severity) {
            this.source = source;
            this.message = message;
            this.severity = severity;
        }

        /**
         * Get {@link Severity}.
         *
         * @return severity of current {@code ErrorMessage}.
         */
        public Severity getSeverity() {
            return severity;
        }

        /**
         * Human-readable description of the issue.
         *
         * @return message describing the issue.
         */
        public String getMessage() {
            return message;
        }

        /**
         * The issue source.
         * <p/>
         * Identifies the object where the issue was found.
         *
         * @return source of the issue.
         */
        public Object getSource() {
            return source;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            ErrorMessage that = (ErrorMessage) o;

            if (message != null ? !message.equals(that.message) : that.message != null) {
                return false;
            }
            if (severity != that.severity) {
                return false;
            }
            if (source != null ? !source.equals(that.source) : that.source != null) {
                return false;
            }

            return true;
        }

        @Override
        public int hashCode() {
            int result = source != null ? source.hashCode() : 0;
            result = 31 * result + (message != null ? message.hashCode() : 0);
            result = 31 * result + (severity != null ? severity.hashCode() : 0);
            return result;
        }
    }
}
