/*   Copyright 2004 The Apache Software Foundation
 *
 *   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 org.apache.xmlbeans;

import javax.xml.stream.Location;
import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.MessageFormat;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;

/**
 * Represents a message at a specific XML location.
 * <p>
 * The message can be an error, warning, or simple information, and
 * it may optionally be associated with a specific location in
 * an XML document.  The class includes methods for extracting
 * the location as a line number, XmlCursor, or XmlObject, as
 * well as for obtaining and message and severity of the
 * error.
 *
 * @see XmlOptions#setErrorListener
 * @see XmlException
 */
public class XmlError implements java.io.Serializable {
    private static final long serialVersionUID = 1L;

    private static final ResourceBundle _bundle = PropertyResourceBundle.getBundle("org.apache.xmlbeans.message", Locale.ROOT);

    private final String _message;
    private final String _code;
    private final String _source;
    private final int _severity;
    private final int _line;
    private final int _column;
    private int _offset = -1;

    private transient XmlCursor _cursor;

    /**
     * Copy constructor.
     *
     * @param src The original XmlError to copy.
     */
    public XmlError(XmlError src) {
        _message = src.getMessage();
        _code = src.getErrorCode();
        _severity = src.getSeverity();
        _source = src.getSourceName();
        _line = src.getLine();
        _column = src.getColumn();
        _offset = src.getOffset();
        _cursor = src.getCursorLocation();
    }

    /**
     * The static factory methods should be used instead of
     * this constructor.
     */
    private XmlError(String message, String code, int severity,
                     String source, int line, int column, int offset, XmlCursor cursor) {
        _message = message;
        _code = code;
        _severity = severity;
        _source = source;
        _line = line;
        _column = column;
        _offset = offset;
        _cursor = cursor;
    }

    private XmlError(String code, Object[] args, int severity,
                     String source, int line, int column, int offset, XmlCursor cursor) {
        this(XmlError.formattedMessage(code, args), code, severity, source, line, column, offset, cursor);
    }

    /**
     * The static factory methods should be used instead of
     * this constructor.
     */
    protected XmlError(String message, String code, int severity, XmlCursor cursor) {
        String source = null;
        int line = -1;
        int column = -1;
        int offset = -1;

        if (cursor != null) {
            // Hunt down the line/column/offset
            source = cursor.documentProperties().getSourceName();

            try (XmlCursor c = cursor.newCursor()) {
                XmlLineNumber ln =
                    (XmlLineNumber) c.getBookmark(XmlLineNumber.class);

                if (ln == null) {
                    ln = (XmlLineNumber) c.toPrevBookmark(XmlLineNumber.class);
                }

                if (ln != null) {
                    line = ln.getLine();
                    column = ln.getColumn();
                    offset = ln.getOffset();
                }
            }
        }

        _message = message;
        _code = code;
        _severity = severity;
        _source = source;
        _line = line;
        _column = column;
        _offset = offset;
        _cursor = cursor;
    }

    protected XmlError(String code, Object[] args, int severity, XmlCursor cursor) {
        this(XmlError.formattedMessage(code, args), code, severity, cursor);
    }

    /**
     * The static factory methods should be used instead of
     * this constructor.
     */
    protected XmlError(String message, String code, int severity, Location loc) {
        String source = null;
        int line = -1;
        int column = -1;

        if (loc != null) {
            line = loc.getLineNumber();
            column = loc.getColumnNumber();
            source = loc.getPublicId();
            if (source == null) {
                source = loc.getSystemId();
            }
        }

        _message = message;
        _code = code;
        _severity = severity;
        _source = source;
        _line = line;
        _column = column;
    }

    protected XmlError(String code, Object[] args, int severity, Location loc) {
        this(XmlError.formattedMessage(code, args), code, severity, loc);
    }

    /**
     * Returns an XmlError for the given message, with no location and {@link #SEVERITY_ERROR}.
     *
     * @param message the error message
     */
    public static XmlError forMessage(String message) {
        return forMessage(message, SEVERITY_ERROR);
    }

    /**
     * Returns an XmlError for the given message, with no location and the given severity.
     *
     * @param message  the error message
     * @param severity the severity ({@link #SEVERITY_ERROR}, {@link #SEVERITY_WARNING}, or {@link #SEVERITY_INFO})
     */
    public static XmlError forMessage(String message, int severity) {
        return forSource(message, severity, null);
    }

    /**
     * Returns an XmlError for the given message, with no location and the given severity.
     *
     * @param code the error code
     * @param args the arguments to use in formatting the error message
     */
    public static XmlError forMessage(String code, Object[] args) {
        return forSource(code, args, SEVERITY_ERROR, null);
    }

    /**
     * Returns an XmlError for the given message, with no location and the given severity.
     *
     * @param code     the error code
     * @param args     the arguments to use in formatting the error message
     * @param severity the severity ({@link #SEVERITY_ERROR}, {@link #SEVERITY_WARNING}, or {@link #SEVERITY_INFO})
     */
    public static XmlError forMessage(String code, Object[] args, int severity) {
        return forSource(code, args, severity, null);
    }

    /**
     * Returns an XmlError for the given message, located in the given file and {@link #SEVERITY_ERROR}.
     *
     * @param message    the error message
     * @param sourceName the URL or other name for the file
     */
    public static XmlError forSource(String message, String sourceName) {
        return forLocation(message, SEVERITY_ERROR, sourceName, -1, -1, -1);
    }

    /**
     * Returns an XmlError for the given message, with the given severity, located in the given file.
     *
     * @param message    the error message
     * @param severity   the severity ({@link #SEVERITY_ERROR}, {@link #SEVERITY_WARNING}, or {@link #SEVERITY_INFO})
     * @param sourceName the URL or other name for the file
     */
    public static XmlError forSource(String message, int severity, String sourceName) {
        return forLocation(message, severity, sourceName, -1, -1, -1);
    }

    /**
     * Returns an XmlError for the given message, with the given severity, located in the given file.
     *
     * @param code       the error code
     * @param args       the arguments to use in formatting the error message
     * @param severity   the severity ({@link #SEVERITY_ERROR}, {@link #SEVERITY_WARNING}, or {@link #SEVERITY_INFO})
     * @param sourceName the URL or other name for the file
     */
    public static XmlError forSource(String code, Object[] args, int severity, String sourceName) {
        return forLocation(code, args, severity, sourceName, -1, -1, -1);
    }

    /**
     * Returns an XmlError for the given message, located at a specific point in the given file and {@link #SEVERITY_ERROR}.
     *
     * @param message    the error message
     * @param sourceName the URL or other name for the file
     * @param location   the location from an xml stream
     */
    public static XmlError forLocation(String message, String sourceName, Location location) {
        return new XmlError(message, (String) null, SEVERITY_ERROR, sourceName,
            location.getLineNumber(), location.getColumnNumber(), -1, null);
    }

    /**
     * Returns an XmlError for the given message, located at a specific point in the given file and {@link #SEVERITY_ERROR}.
     *
     * @param message    the error message
     * @param sourceName the URL or other name for the file
     * @param line       the 1-based line number, or -1 if not known
     * @param column     the 1-based column number, or -1 if not known
     * @param offset     the 0-base file character offset, or -1 if not known
     */
    public static XmlError forLocation(String message, String sourceName, int line, int column, int offset) {
        return new XmlError(message, (String) null, SEVERITY_ERROR, sourceName, line, column, offset, null);
    }

    /**
     * Returns an XmlError for the given message, with the given severity, located at a specific point in the given file.
     *
     * @param code       the error code
     * @param args       the arguments to use in formatting the error message
     * @param severity   the severity ({@link #SEVERITY_ERROR}, {@link #SEVERITY_WARNING}, or {@link #SEVERITY_INFO})
     * @param sourceName the URL or other name for the file
     * @param line       the 1-based line number, or -1 if not known
     * @param column     the 1-based column number, or -1 if not known
     * @param offset     the 0-base file character offset, or -1 if not known
     */
    public static XmlError forLocation(String code, Object[] args, int severity, String sourceName, int line, int column, int offset) {
        return new XmlError(code, args, severity, sourceName, line, column, offset, null);
    }

    /**
     * Returns an XmlError for the given message, with the given severity, located at a specific point in the given file.
     *
     * @param message    the error message
     * @param severity   the severity ({@link #SEVERITY_ERROR}, {@link #SEVERITY_WARNING}, or {@link #SEVERITY_INFO})
     * @param sourceName the URL or other name for the file
     * @param line       the 1-based line number, or -1 if not known
     * @param column     the 1-based column number, or -1 if not known
     * @param offset     the 0-base file character offset, or -1 if not known
     */
    public static XmlError forLocation(String message, int severity, String sourceName, int line, int column, int offset) {
        return new XmlError(message, (String) null, severity, sourceName, line, column, offset, null);
    }

    /**
     * Returns an XmlError for the given message, with the given severity, located at the given physcial location and XmlCursor.
     *
     * @param message    the error message
     * @param severity   the severity ({@link #SEVERITY_ERROR}, {@link #SEVERITY_WARNING}, or {@link #SEVERITY_INFO})
     * @param sourceName the URL or other name for the file
     * @param line       the 1-based line number, or -1 if not known
     * @param column     the 1-based column number, or -1 if not known
     * @param offset     the 0-base file character offset, or -1 if not known
     * @param cursor     the XmlCursor representing the location of the error
     */
    public static XmlError forLocationAndCursor(String message, int severity, String sourceName, int line, int column, int offset, XmlCursor cursor) {
        return new XmlError(message, (String) null, severity, sourceName, line, column, offset, cursor);
    }

    /**
     * Returns an XmlError for the given message, located at the XmlObject, with {@link #SEVERITY_ERROR}.
     *
     * @param message the error message
     * @param xobj    the XmlObject representing the location of the error
     */
    public static XmlError forObject(String message, XmlObject xobj) {
        return forObject(message, SEVERITY_ERROR, xobj);
    }

    /**
     * Returns an XmlError for the given message, located at the XmlObject, with {@link #SEVERITY_ERROR}.
     *
     * @param code the error code
     * @param args the arguments to use in formatting the error message
     * @param xobj the XmlObject representing the location of the error
     */
    public static XmlError forObject(String code, Object[] args, XmlObject xobj) {
        return forObject(code, args, SEVERITY_ERROR, xobj);
    }

    /**
     * Returns an XmlError for the given message, with the given severity, located at the XmlObject.
     *
     * @param message  the error message
     * @param severity the severity ({@link #SEVERITY_ERROR}, {@link #SEVERITY_WARNING}, or {@link #SEVERITY_INFO})
     * @param xobj     the XmlObject representing the location of the error
     */
    public static XmlError forObject(String message, int severity, XmlObject xobj) {
        if (xobj == null) {
            return forMessage(message, severity);
        }

        XmlCursor cur = xobj.newCursor();
        return forCursor(message, severity, cur);
    }

    /**
     * Returns an XmlError for the given message, with the given severity, located at the XmlObject.
     *
     * @param code     the error code
     * @param args     the arguments to use in formatting the error message
     * @param severity the severity ({@link #SEVERITY_ERROR}, {@link #SEVERITY_WARNING}, or {@link #SEVERITY_INFO})
     * @param xobj     the XmlObject representing the location of the error
     */
    public static XmlError forObject(String code, Object[] args, int severity, XmlObject xobj) {
        if (xobj == null) {
            return forMessage(code, args, severity);
        }

        XmlCursor cur = xobj.newCursor();
        return forCursor(code, args, severity, cur);
    }

    /**
     * Returns an XmlError for the given message, located at the XmlCursor, with {@link #SEVERITY_ERROR}.
     *
     * @param message the error message
     * @param cursor  the XmlCursor representing the location of the error
     */
    public static XmlError forCursor(String message, XmlCursor cursor) {
        return forCursor(message, SEVERITY_ERROR, cursor);
    }

    /**
     * Returns an XmlError for the given message, located at the XmlCursor, with {@link #SEVERITY_ERROR}.
     *
     * @param code   the error code
     * @param args   the arguments to use in formatting the error message
     * @param cursor the XmlCursor representing the location of the error
     */
    public static XmlError forCursor(String code, Object[] args, XmlCursor cursor) {
        return forCursor(code, args, SEVERITY_ERROR, cursor);
    }

    /**
     * Returns an XmlError for the given message, with the given severity, located at the XmlCursor.
     *
     * @param message  the error message
     * @param severity the severity ({@link #SEVERITY_ERROR}, {@link #SEVERITY_WARNING}, or {@link #SEVERITY_INFO})
     * @param cursor   the XmlCursor representing the location of the error
     */
    public static XmlError forCursor(String message, int severity, XmlCursor cursor) {
        return new XmlError(message, (String) null, severity, cursor);
    }

    /**
     * Returns an XmlError for the given message, with the given severity, located at the XmlCursor.
     *
     * @param code     the error code
     * @param args     the arguments to use in formatting the error message
     * @param severity the severity ({@link #SEVERITY_ERROR}, {@link #SEVERITY_WARNING}, or {@link #SEVERITY_INFO})
     * @param cursor   the XmlCursor representing the location of the error
     */
    public static XmlError forCursor(String code, Object[] args, int severity, XmlCursor cursor) {
        return new XmlError(code, args, severity, cursor);
    }

    /**
     * Tries to produce a nicely formatted filename from the given string.
     */
    protected static String formattedFileName(String rawString, URI base) {
        if (rawString == null) {
            return null;
        }

        URI uri;

        try {
            // if it looks like an absolute URI, treat it as such
            uri = new URI(rawString);

            // otherwise, treat it like a filename
            if (!uri.isAbsolute()) {
                uri = null;
            }
        } catch (URISyntaxException e) {
            uri = null;
        }

        // looks like a filename; convert it to uri for relativization
        if (uri == null) {
            uri = new File(rawString).toURI();
        }

        if (base != null) {
            uri = base.relativize(uri);
        }

        // filenames get their file: stripped off and their /'s turned into \'s (MSDOS)
        if (uri.isAbsolute() ? uri.getScheme().compareToIgnoreCase("file") == 0 :
            base != null && base.isAbsolute() && base.getScheme().compareToIgnoreCase("file") == 0) {
            try {
                return (new File(uri)).toString();
            } catch (Exception ignored) {
            }
        }

        return uri.toString();
    }

    /**
     * Tries to format a message using the error code.
     */
    public static String formattedMessage(String code, Object[] args) {
        if (code == null) {
            return null;
        }

        try {
            return new MessageFormat(_bundle.getString(code), Locale.ROOT).format(args);
        } catch (MissingResourceException | IllegalArgumentException e) {
            String bnd = (e instanceof MissingResourceException) ? "message.missing.resource" : "message.pattern.invalid";
            return new MessageFormat(_bundle.getString(bnd), Locale.ROOT).format(e.getMessage());
        }
    }

    /**
     * An error. See {@link #getSeverity}.
     */
    public static final int SEVERITY_ERROR = 0;
    /**
     * A warning. See {@link #getSeverity}.
     */
    public static final int SEVERITY_WARNING = 1;
    /**
     * An informational message. See {@link #getSeverity}.
     */
    public static final int SEVERITY_INFO = 2;

    /**
     * Returns the severity.  Either {@link #SEVERITY_ERROR}, {@link #SEVERITY_WARNING}, or {@link #SEVERITY_INFO}.
     */
    public int getSeverity() {
        return _severity;
    }

    /**
     * Returns the error message without location information.
     */
    public String getMessage() {
        return _message;
    }

    /**
     * Returns the error code or null. See {@link XmlErrorCodes}.
     */
    public String getErrorCode() {
        return _code;
    }

    /**
     * Returns the URL (or other name) of the file with the error, if available.
     */
    public String getSourceName() {
        return _source;
    }

    /**
     * Returns the line number of the error, if available, -1 if not.
     */
    public int getLine() {
        return _line;
    }

    /**
     * Returns the column number of the error, if available, -1 if not.
     */
    public int getColumn() {
        return _column;
    }

    /**
     * Returns the file character offset of the error, if available, -1 if not.
     */
    public int getOffset() {
        return _offset;
    }

    /**
     * Returns a location object of the given type.  XmlCursor.class and
     * XmlObject.class can be passed, for example.  Null if not available.
     */
    public Object getLocation(Object type) {
        if (type == XmlCursor.class) {
            return _cursor;
        }
        if (type == XmlObject.class && _cursor != null) {
            return _cursor.getObject();
        }
        return null;
    }

    /**
     * Returns a location of the error as an {@link XmlCursor}, null if
     * not available.
     */
    public XmlCursor getCursorLocation() {
        return (XmlCursor) getLocation(XmlCursor.class);
    }

    /**
     * Returns a location of the error as an {@link XmlObject}, null if
     * not available.
     */
    public XmlObject getObjectLocation() {
        return (XmlObject) getLocation(XmlObject.class);
    }

    /**
     * Produces a standard string for the error message, complete with
     * filename and location offsets if available.
     */
    public String toString() {
        return toString(null);
    }

    /**
     * Produces a standard string with the error message.  If a non-null
     * URI is supplied, source names are relativized against the given
     * URI.
     */
    public String toString(URI base) {
        // modified to carefully match the IDE's
        // workshop.workspace.ant.AntLogger regex
        // which also matches javac (davidbau)

        StringBuilder sb = new StringBuilder();

        String source = formattedFileName(getSourceName(), base);

        if (source != null) {
            sb.append(source);
            int line = getLine();
            if (line < 0) {
                line = 0;
            }

            sb.append(':');
            sb.append(line);
            sb.append(':');
            if (getColumn() > 0) {
                sb.append(getColumn());
                sb.append(':');
            }
            sb.append(' ');
        }

        switch (getSeverity()) {
            case SEVERITY_ERROR:
                sb.append("error: ");
                break;
            case SEVERITY_WARNING:
                sb.append("warning: ");
                break;
            case SEVERITY_INFO:
                break;
        }

        if (getErrorCode() != null) {
            sb.append(getErrorCode()).append(": ");
        }

        String msg = getMessage();

        sb.append(msg == null ? "<Unspecified message>" : msg);

        return sb.toString();
    }

    public static String severityAsString(int severity) {
        switch (severity) {
            case SEVERITY_ERROR:
                return ("error");
            case SEVERITY_WARNING:
                return ("warning");
            case SEVERITY_INFO:
                return "info";
            default:
                throw new IllegalArgumentException("unknown severity");
        }
    }
}
