package org.checkerframework.framework.util.dependenttypes;

import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.framework.source.Result;
import org.checkerframework.framework.util.FlowExpressionParseUtil.FlowExpressionParseException;
import org.checkerframework.javacutil.BugInCF;

/**
 * Helper class for creating dependent type annotation error strings.
 *
 * <p>IMPORTANT: This is not an Exception. It is a regular class that is returned, not thrown. The
 * errors are not thrown so that they are only reported once rather than every time the annotation
 * is parsed. See {@link DependentTypesHelper} for more details.
 */
public class DependentTypesError {

    /** How elements of this class are formatted. */
    private static final String FORMAT_STRING = "[error for expression: %s; error: %s]";
    /** Regular expression for unparsing string representations of this class (gross). */
    private static final Pattern ERROR_PATTERN =
            Pattern.compile("\\[error for expression: (.*); error: (.*)\\]");
    /**
     * Returns whether or not the given expression string is an error. That is, whether it is a
     * string that was generated by this class.
     *
     * @param expression expression string to test
     * @return whether or not the given expressions string is an error
     */
    public static boolean isExpressionError(String expression) {
        return expression.startsWith("[error");
    }

    /** How to format warnings about use of formal parameter name. */
    public static final String FORMAL_PARAM_NAME_STRING = "Use \"#%d\" rather than \"%s\"";
    /** Matches warnings about use of formal parameter name. */
    private static final Pattern FORMAL_PARAM_NAME_PATTERN =
            Pattern.compile(
                    "^'([a-zA-Z_$][a-zA-Z0-9_$]*)' because (Use \"#\\d+\" rather than \"\\1\")$");

    /** The expression that is unparseable or otherwise problematic. */
    public final String expression;
    /** An error message about that expression. */
    public final String error;

    /**
     * Create a DependentTypesError for the given expression and error message.
     *
     * @param expression the incorrect Java expression
     * @param error an error message about the expression
     */
    public DependentTypesError(String expression, String error) {
        this.expression = expression;
        this.error = error;
    }

    /** Create a DependentTypesError for the given expression and exception. */
    public DependentTypesError(String expression, FlowExpressionParseException e) {
        this.expression = expression;
        StringBuilder buf = new StringBuilder();
        List<Result.DiagMessage> msgs = e.getResult().getDiagMessages();

        for (Result.DiagMessage msg : msgs) {
            buf.append(msg.getArgs()[0]);
        }
        this.error = buf.toString();
    }

    /**
     * Create a DependentTypesError by parsing a printed one.
     *
     * @param formattedError the toString() representation of a DependentTypesError
     */
    public static DependentTypesError unparse(String formattedError) {
        Matcher matcher = ERROR_PATTERN.matcher(formattedError);
        if (matcher.matches()) {
            assert matcher.groupCount() == 2;
            return new DependentTypesError(matcher.group(1), matcher.group(2));
        } else {
            throw new BugInCF("Cannot unparse: " + formattedError);
        }
    }

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

        DependentTypesError that = (DependentTypesError) o;

        return expression.equals(that.expression) && error.equals(that.error);
    }

    @Override
    public int hashCode() {
        return Objects.hash(expression, error);
    }

    @Override
    public String toString() {
        return String.format(FORMAT_STRING, expression, error);
    }

    /**
     * Like toString, but uses better formatting sometimes. Use this only for the final output,
     * because of the design that hides error messages in toString().
     */
    public String format() {
        Matcher m = FORMAL_PARAM_NAME_PATTERN.matcher(error);
        if (m.matches()) {
            return m.group(2);
        }
        return toString();
    }
}
