/*
 *  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.codehaus.groovy.control;

import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.control.messages.ExceptionMessage;
import org.codehaus.groovy.control.messages.LocatedMessage;
import org.codehaus.groovy.control.messages.Message;
import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
import org.codehaus.groovy.control.messages.WarningMessage;
import org.codehaus.groovy.syntax.CSTNode;
import org.codehaus.groovy.syntax.SyntaxException;

import java.io.PrintWriter;
import java.io.Serial;
import java.io.Serializable;
import java.util.LinkedList;
import java.util.List;

/**
 * A base class for collecting messages and errors during processing.
 * Each CompilationUnit should have an ErrorCollector, and the SourceUnits
 * should share their ErrorCollector with the CompilationUnit.
 */
public class ErrorCollector implements Serializable {

    @Serial private static final long serialVersionUID = 2844774170905056755L;

    /**
     * ErrorMessages collected during processing
     */
    protected LinkedList<Message> errors;

    /**
     * WarningMessages collected during processing
     */
    protected LinkedList<WarningMessage> warnings;

    /**
     * Configuration and other settings that control processing
     */
    protected final CompilerConfiguration configuration;

    /**
     * Initialize the ErrorReporter.
     */
    public ErrorCollector(final CompilerConfiguration configuration) {
        this.configuration = configuration;
    }

    /**
     * Merges the recorded warnings and errors from another collector into this one.
     *
     * @param that the collector whose contents should be appended
     */
    public void addCollectorContents(final ErrorCollector that) {
        if (that.errors != null) {
            if (this.errors == null) {
                this.errors = that.errors;
            } else {
                this.errors.addAll(that.errors);
            }
        }
        if (that.warnings != null) {
            if (this.warnings == null) {
                this.warnings = that.warnings;
            } else {
                this.warnings.addAll(that.warnings);
            }
        }
    }

    /**
     * Adds a syntax error for the supplied AST node without failing immediately.
     *
     * @param error the error message
     * @param node the offending AST node
     * @param source the source unit containing the node
     */
    public void addErrorAndContinue(final String error, final ASTNode node, final SourceUnit source) {
        addErrorAndContinue(Message.create(new SyntaxException(error, node), source));
    }

    /**
     * Adds a syntax error without failing immediately.
     *
     * @param error the syntax error
     * @param source the source unit containing the error
     */
    public void addErrorAndContinue(final SyntaxException error, final SourceUnit source) {
        addErrorAndContinue(Message.create(error, source));
    }

    /**
     * Adds an error to the message set, but does not cause a failure. The message is not required to have a source
     * line and column specified, but it is best practice to try and include that information.
     */
    public void addErrorAndContinue(final Message message) {
        if (errors == null) {
            errors = new LinkedList<>();
        }
        errors.add(message);
    }

    /**
     * Adds a non-fatal error to the message set, which may cause a failure if the error threshold is exceeded.
     * The message is not required to have a source line and column specified, but it is best practice to try
     * and include that information.
     */
    public void addError(final Message message) throws CompilationFailedException {
        addErrorAndContinue(message);

        if (errors != null && errors.size() >= configuration.getTolerance()) {
            failIfErrors();
        }
    }

    /**
     * Adds an optionally-fatal error to the message set.
     * The message is not required to have a source line and column specified, but it is best practice to try
     * and include that information.
     * @param fatal
     *      if true then processing will stop
     */
    public void addError(final Message message, final boolean fatal) throws CompilationFailedException {
        if (fatal) {
            addFatalError(message);
        } else {
            addError(message);
        }
    }

    /**
     * Adds a syntax error and fails if it is marked fatal or exceeds tolerance.
     *
     * @param error the syntax error
     * @param source the source unit containing the error
     * @throws CompilationFailedException if compilation must stop
     */
    public void addError(final SyntaxException error, final SourceUnit source) throws CompilationFailedException {
        addError(Message.create(error, source), error.isFatal());
    }

    /**
     * Adds a source-located error message.
     *
     * @param error the error text
     * @param context the CST node describing the source location
     * @param source the source unit containing the error
     * @throws CompilationFailedException if compilation must stop
     */
    public void addError(final String error, final CSTNode context, final SourceUnit source) throws CompilationFailedException {
        addError(new LocatedMessage(error, context, source));
    }

    /**
     * Adds an exception-based error for the supplied source unit and then fails.
     *
     * @param exception the exception to report
     * @param source the source unit being processed
     * @throws CompilationFailedException if compilation must stop
     */
    public void addException(final Exception exception, final SourceUnit source) throws CompilationFailedException {
        addError(new ExceptionMessage(exception, configuration.getDebug(), source));
        failIfErrors();
    }

    /**
     * Adds an error to the message set and throws CompilationFailedException.
     */
    public void addFatalError(final Message message) throws CompilationFailedException {
        addError(message);
        failIfErrors();
    }

    /**
     * Adds a warning to the message set.
     */
    public void addWarning(final WarningMessage message) {
        if (message.isRelevant(configuration.getWarningLevel())) {
            if (warnings == null) {
                warnings = new LinkedList<>();
            }
            warnings.add(message);
        }
    }

    /**
     * Adds a warning to the message set if it is relevant.
     */
    public void addWarning(final int importance, final String text, final CSTNode context, final SourceUnit source) {
        if (WarningMessage.isRelevant(importance, configuration.getWarningLevel())) {
            addWarning(new WarningMessage(importance, text, context, source));
        }
    }

    /**
     * Adds a warning to the message set if it is relevant.
     */
    public void addWarning(final int importance, final String text, final Object data, final CSTNode context, final SourceUnit source) {
        if (WarningMessage.isRelevant(importance, configuration.getWarningLevel())) {
            addWarning(new WarningMessage(importance, text, data, context, source));
        }
    }

    /**
     * @return the compiler configuration used to create this error collector
     */
    public CompilerConfiguration getConfiguration() {
        return configuration;
    }

    /**
     * Returns the number of errors.
     */
    public int getErrorCount() {
        return (hasErrors() ? errors.size() : 0);
    }

    /**
     * Returns the specified error message, or null.
     */
    public Message getError(final int index) {
        if (index < getErrorCount()) {
            return errors.get(index);
        }
        return null;
    }

    /**
     * Returns the list of errors, or null if there are none.
     */
    public List<? extends Message> getErrors() {
        return errors;
    }

    /**
     * Returns true if there are any errors pending.
     */
    public boolean hasErrors() {
        return (errors != null);
    }

    /**
     * Returns the number of warnings.
     */
    public int getWarningCount() {
        return (hasWarnings() ? warnings.size() : 0);
    }

    /**
     * Returns the specified warning message, or null.
     */
    public WarningMessage getWarning(final int index) {
        if (index < getWarningCount()) {
            return warnings.get(index);
        }
        return null;
    }

    /**
     * Returns the list of warnings, or null if there are none.
     */
    public List<WarningMessage> getWarnings() {
        return warnings;
    }

    /**
     * Returns true if there are any warnings pending.
     */
    public boolean hasWarnings() {
        return (warnings != null);
    }

    //

    /**
     * Returns the last error reported.
     */
    public Message getLastError() {
        return errors.getLast();
    }

    /**
     * Returns the specified error's underlying Exception, or null if it isn't one.
     */
    public Exception getException(final int index) {
        Exception exception = null;
        Message message = getError(index);
        if (message != null) {
            if (message instanceof ExceptionMessage) {
                exception = ((ExceptionMessage) message).getCause();
            } else if (message instanceof SyntaxErrorMessage) {
                exception = ((SyntaxErrorMessage) message).getCause();
            }
        }
        return exception;
    }

    /**
     * Returns the specified error's underlying SyntaxException, or null if it isn't one.
     */
    public SyntaxException getSyntaxError(final int index) {
        SyntaxException exception = null;
        Message message = getError(index);
        if (message instanceof SyntaxErrorMessage) {
            exception = ((SyntaxErrorMessage) message).getCause();
        }
        return exception;
    }

    /**
     * Causes the current phase to fail by throwing a CompilationFailedException.
     */
    protected void failIfErrors() throws CompilationFailedException {
        if (hasErrors()) {
            throw new MultipleCompilationErrorsException(this);
        }
    }

    //---------------------------------------------------------------------------
    // OUTPUT

    private void write(final PrintWriter writer, final Janitor janitor, final List<? extends Message> messages, final String txt) {
        if (messages == null || messages.isEmpty()) return;

        for (Message message : messages) {
            message.write(writer, janitor);
            if (configuration.getDebug() && (message instanceof SyntaxErrorMessage)) {
                ((SyntaxErrorMessage) message).getCause().printStackTrace(writer);
            }
            writer.println();
        }

        writer.print(messages.size());
        writer.print(" " + txt);
        if (messages.size() > 1) {
            writer.print("s");
        }
        writer.println();
    }

    /**
     * Writes error messages to the specified PrintWriter.
     */
    public void write(final PrintWriter writer, final Janitor janitor) {
        write(writer, janitor, warnings, "warning");
        write(writer, janitor, errors, "error");
    }
}
