/*
 * 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.commons.jexl2;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.UndeclaredThrowableException;
import org.apache.commons.jexl2.parser.JexlNode;
import org.apache.commons.jexl2.parser.ParseException;
import org.apache.commons.jexl2.parser.TokenMgrError;

/**
 * Wraps any error that might occur during interpretation of a script or expression.
 * @since 2.0
 */
public class JexlException extends RuntimeException {
    /** The point of origin for this exception. */
    protected final transient JexlNode mark;
    /** The debug info. */
    protected final transient JexlInfo info;
    /** A marker to use in NPEs stating a null operand error. */
    public static final String NULL_OPERAND = "jexl.null";
    /** Minimum number of characters around exception location. */
    private static final int MIN_EXCHARLOC = 5;
    /** Maximum number of characters around exception location. */
    private static final int MAX_EXCHARLOC = 10;

    /**
     * Creates a new JexlException.
     * @param node the node causing the error
     * @param msg the error message
     */
    public JexlException(JexlNode node, String msg) {
        super(msg);
        mark = node;
        info = node != null ? node.debugInfo() : null;

    }

    /**
     * Creates a new JexlException.
     * @param node the node causing the error
     * @param msg the error message
     * @param cause the exception causing the error
     */
    public JexlException(JexlNode node, String msg, Throwable cause) {
        super(msg, unwrap(cause));
        mark = node;
        info = node != null ? node.debugInfo() : null;
    }

    /**
     * Creates a new JexlException.
     * @param dbg the debugging information associated
     * @param msg the error message
     */
    public JexlException(JexlInfo dbg, String msg) {
        super(msg);
        mark = null;
        info = dbg;
    }

    /**
     * Creates a new JexlException.
     * @param dbg the debugging information associated
     * @param msg the error message
     * @param cause the exception causing the error
     */
    public JexlException(JexlInfo dbg, String msg, Throwable cause) {
        super(msg, unwrap(cause));
        mark = null;
        info = dbg;
    }

    /**
     * Unwraps the cause of a throwable due to reflection. 
     * @param xthrow the throwable
     * @return the cause
     */
    private static Throwable unwrap(Throwable xthrow) {
        if (xthrow instanceof InvocationTargetException) {
            return ((InvocationTargetException) xthrow).getTargetException();
        } else if (xthrow instanceof UndeclaredThrowableException) {
            return ((UndeclaredThrowableException) xthrow).getUndeclaredThrowable();
        } else {
            return xthrow;
        }
    }

    /**
     * Accesses detailed message.
     * @return  the message
     * @since 2.1
     */
    protected String detailedMessage() {
        return super.getMessage();
    }

    /**
     * Formats an error message from the parser.
     * @param prefix the prefix to the message
     * @param expr the expression in error
     * @return the formatted message
     * @since 2.1
     */
    protected String parserError(String prefix, String expr) {
        int begin = info.debugInfo().getColumn();
        int end = begin + MIN_EXCHARLOC;
        begin -= MIN_EXCHARLOC;
        if (begin < 0) {
            end += MIN_EXCHARLOC;
            begin = 0;
        }
        int length = expr.length();
        if (length < MAX_EXCHARLOC) {
            return prefix + " error in '" + expr + "'";
        } else {
            return prefix + " error near '... "
                    + expr.substring(begin, end > length ? length : end) + " ...'";
        }
    }

    /**
     * Thrown when tokenization fails.
     * @since 2.1
     */
    public static class Tokenization extends JexlException {
        /**
         * Creates a new Tokenization exception instance.
         * @param node the location info
         * @param expr the expression
         * @param cause the javacc cause
         */
        public Tokenization(JexlInfo node, CharSequence expr, TokenMgrError cause) {
            super(merge(node, cause), expr.toString(), cause);
        }

        /**
         * Merge the node info and the cause info to obtain best possible location.
         * @param node the node
         * @param cause the cause
         * @return the info to use
         */
        private static DebugInfo merge(JexlInfo node, TokenMgrError cause) {
            DebugInfo dbgn = node != null ? node.debugInfo() : null;
            if (cause == null) {
                return dbgn;
            } else if (dbgn == null) {
                return new DebugInfo("", cause.getLine(), cause.getColumn());
            } else {
                return new DebugInfo(dbgn.getName(), cause.getLine(), cause.getColumn());
            }
        }

        /**
         * @return the expression
         */
        public String getExpression() {
            return super.detailedMessage();
        }

        @Override
        protected String detailedMessage() {
            return parserError("tokenization", getExpression());
        }
    }

    /**
     * Thrown when parsing fails.
     * @since 2.1
     */
    public static class Parsing extends JexlException {
        /**
         * Creates a new Variable exception instance.
         * @param node the offending ASTnode
         * @param expr the offending source
         * @param cause the javacc cause
         */
        public Parsing(JexlInfo node, CharSequence expr, ParseException cause) {
            super(merge(node, cause), expr.toString(), cause);
        }

        /**
         * Merge the node info and the cause info to obtain best possible location.
         * @param node the node
         * @param cause the cause
         * @return the info to use
         */
        private static DebugInfo merge(JexlInfo node, ParseException cause) {
            DebugInfo dbgn = node != null ? node.debugInfo() : null;
            if (cause == null) {
                return dbgn;
            } else if (dbgn == null) {
                return new DebugInfo("", cause.getLine(), cause.getColumn());
            } else {
                return new DebugInfo(dbgn.getName(), cause.getLine(), cause.getColumn());
            }
        }

        /**
         * @return the expression
         */
        public String getExpression() {
            return super.detailedMessage();
        }

        @Override
        protected String detailedMessage() {
            return parserError("parsing", getExpression());
        }
    }

    /**
     * Thrown when a variable is unknown.
     * @since 2.1
     */
    public static class Variable extends JexlException {
        /**
         * Creates a new Variable exception instance.
         * @param node the offending ASTnode
         * @param var the unknown variable
         */
        public Variable(JexlNode node, String var) {
            super(node, var);
        }

        /**
         * @return the variable name
         */
        public String getVariable() {
            return super.detailedMessage();
        }

        @Override
        protected String detailedMessage() {
            return "undefined variable " + getVariable();
        }
    }

    /**
     * Thrown when a property is unknown.
     * @since 2.1
     */
    public static class Property extends JexlException {
        /**
         * Creates a new Property exception instance.
         * @param node the offending ASTnode
         * @param var the unknown variable
         */
        public Property(JexlNode node, String var) {
            super(node, var);
        }

        /**
         * @return the property name
         */
        public String getProperty() {
            return super.detailedMessage();
        }

        @Override
        protected String detailedMessage() {
            return "inaccessible or unknown property " + getProperty();
        }
    }

    /**
     * Thrown when a method or ctor is unknown, ambiguous or inaccessible.
     * @since 2.1
     */
    public static class Method extends JexlException {
        /**
         * Creates a new Method exception instance.
         * @param node the offending ASTnode
         * @param name the unknown method
         */
        public Method(JexlNode node, String name) {
            super(node, name);
        }

        /**
         * @return the method name
         */
        public String getMethod() {
            return super.detailedMessage();
        }

        @Override
        protected String detailedMessage() {
            return "unknown, ambiguous or inaccessible method " + getMethod();
        }
    }

    /**
     * Thrown to return a value.
     * @since 2.1
     */
    protected static class Return extends JexlException {
        /** The returned value. */
        private final Object result;

        /**
         * Creates a new instance of Return.
         * @param node the return node
         * @param msg the message
         * @param value the returned value
         */
        protected Return(JexlNode node, String msg, Object value) {
            super(node, msg);
            this.result = value;
        }

        /**
         * @return the returned value
         */
        public Object getValue() {
            return result;
        }
    }

    /**
     * Thrown to cancel a script execution.
     * @since 2.1
     */
    protected static class Cancel extends JexlException {
        /**
         * Creates a new instance of Cancel.
         * @param node the node where the interruption was detected
         */
        protected Cancel(JexlNode node) {
            super(node, "execution cancelled", null);
        }
    }

    /**
     * Gets information about the cause of this error.
     * <p>
     * The returned string represents the outermost expression in error.
     * The info parameter, an int[2] optionally provided by the caller, will be filled with the begin/end offset
     * characters of the precise error's trigger.
     * </p>
     * @param offsets character offset interval of the precise node triggering the error
     * @return a string representation of the offending expression, the empty string if it could not be determined
     */
    public String getInfo(int[] offsets) {
        Debugger dbg = new Debugger();
        if (dbg.debug(mark)) {
            if (offsets != null && offsets.length >= 2) {
                offsets[0] = dbg.start();
                offsets[1] = dbg.end();
            }
            return dbg.data();
        }
        return "";
    }

    /**
     * Detailed info message about this error.
     * Format is "debug![begin,end]: string \n msg" where:
     * - debug is the debugging information if it exists (@link JexlEngine.setDebug)
     * - begin, end are character offsets in the string for the precise location of the error
     * - string is the string representation of the offending expression
     * - msg is the actual explanation message for this error
     * @return this error as a string
     */
    @Override
    public String getMessage() {
        Debugger dbg = new Debugger();
        StringBuilder msg = new StringBuilder();
        if (info != null) {
            msg.append(info.debugString());
        }
        if (dbg.debug(mark)) {
            msg.append("![");
            msg.append(dbg.start());
            msg.append(",");
            msg.append(dbg.end());
            msg.append("]: '");
            msg.append(dbg.data());
            msg.append("'");
        }
        msg.append(' ');
        msg.append(detailedMessage());
        Throwable cause = getCause();
        if (cause != null && NULL_OPERAND == cause.getMessage()) {
            msg.append(" caused by null operand");
        }
        return msg.toString();
    }
}