/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *   
 *   (C) 2004-2006, Geotools Project Managment Committee (PMC)
 *   (C) 2004, Institut de Recherche pour le Développement
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation; either
 *    version 2.1 of the License, or (at your option) any later version.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotools.referencing.wkt;

// J2SE dependencies
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.PrintWriter;
import java.io.Writer;
import java.text.Format;
import java.text.ParseException;

// OpenGIS dependencies
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.MathTransform;

// Geotools dependencies
import org.geotools.util.logging.Logging;
import org.geotools.resources.Arguments;
import org.geotools.resources.Utilities;


/**
 * Base class for application performing operations on WKT objects from the command line.
 * Instructions are usually read from the {@linkplain System#in standard input stream} and
 * results sent to the {@linkplain System#out standard output stream}, but those streams can
 * be redirected. The set of allowed instructions depends on the subclass used.
 *
 * @since 2.1
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/referencing/src/main/java/org/geotools/referencing/wkt/AbstractConsole.java $
 * @version $Id: AbstractConsole.java 27848 2007-11-12 13:10:32Z desruisseaux $
 * @author Martin Desruisseaux
 */
public abstract class AbstractConsole implements Runnable {
    /**
     * The input stream, usually the {@linkplain System#in standard one}.
     */
    protected final LineNumberReader in;

    /**
     * The output stream, usually the {@linkplain System#out standard one}.
     */
    protected final Writer out;

    /**
     * The error stream, usually the {@linkplain System#err standard one}.
     */
    protected final PrintWriter err;

    /**
     * The line separator, usually the system default.
     */
    protected final String lineSeparator;

    /**
     * The WKT parser, usually a {@link Preprocessor} object.
     */
    protected final Format parser;

    /**
     * The command-line prompt.
     */
    private String prompt = "crs>";

    /**
     * The last line read, or {@code null} if none.
     */
    private transient String line;

    /**
     * Set to {@code true} if {@link #stop()} was invoked.
     */
    private transient volatile boolean stop;
    
    /**
     * Creates a new console instance using {@linkplain System#in standard input stream},
     * {@linkplain System#out standard output stream}, {@linkplain System#err error output stream}
     * and the system default line separator.
     *
     * @param parser The WKT parser, usually a {@link Preprocessor} object.
     */
    public AbstractConsole(final Format parser) {
        this(parser, new LineNumberReader(Arguments.getReader(System.in)));
    }
    
    /**
     * Creates a new console instance using the specified input stream.
     *
     * @param parser The WKT parser, usually a {@link Preprocessor} object.
     * @param in  The input stream.
     */
    public AbstractConsole(final Format        parser,
                           final LineNumberReader  in)
    {
        this(parser, in, Arguments.getWriter(System.out),
         new PrintWriter(Arguments.getWriter(System.err), true),
                    System.getProperty("line.separator", "\n"));
    }
    
    /**
     * Creates a new console instance using the specified streams and line separator.
     *
     * @param parser The WKT parser, usually a {@link Preprocessor} object.
     * @param in  The input stream.
     * @param out The output stream.
     * @param err The error stream.
     * @param lineSeparator The line separator.
     */
    public AbstractConsole(final Format        parser,
                           final LineNumberReader  in,
                           final Writer           out,
                           final PrintWriter      err,
                           final String lineSeparator)
    {
        this.parser        = parser;
        this.in            = in;
        this.out           = out;
        this.err           = err;
        this.lineSeparator = lineSeparator;
    }

    /**
     * Parses the specified text. The default implementation delegates the work to the
     * {@linkplain #parser}.
     *
     * @param  text The text, as a name, a WKT to parse, or an authority code.
     * @param  type The expected type for the object to be parsed (usually a
     *         <code>{@linkplain CoordinateReferenceSystem}.class</code> or
     *         <code>{@linkplain MathTransform}.class</code>).
     * @return The object.
     * @throws ParseException if parsing the specified WKT failed.
     * @throws FactoryException if the object is not of the expected type.
     */
    public Object parseObject(final String text, final Class type)
            throws ParseException, FactoryException
    {
        if (parser instanceof Preprocessor) {
            final Preprocessor parser = (Preprocessor) this.parser;
            parser.offset = (line!=null) ? Math.max(0, line.indexOf(text)) : 0;
            return parser.parseObject(text, type);
        } else {
            return parser.parseObject(text);
        }
    }

    /**
     * Adds a predefined Well Know Text (WKT). The {@code value} argument given to this method
     * can contains itself other definitions specified in some previous calls to this method. This
     * method do nothing if the {@linkplain #parser} is not an instance of {@link Preprocessor}.
     *
     * @param  name The name for the definition to be added.
     * @param  value The Well Know Text (WKT) represented by the name.
     * @throws IllegalArgumentException if the name is invalid.
     * @throws ParseException if the WKT can't be parsed.
     */
    public void addDefinition(final String name, final String value) throws ParseException {
        if (parser instanceof Preprocessor) {
            ((Preprocessor) parser).addDefinition(name, value);
        }
    }

    /**
     * Load all definitions from the specified stream. Definitions are key-value pairs
     * in the form {@code name = wkt} (without the {@code SET} keyword). The
     * result is the same than invoking the {@code SET} instruction for each line
     * in the specified stream. This method is used for loading predefined objects like
     * the database used by {@link org.geotools.referencing.factory.PropertyAuthorityFactory}.
     *
     * @param  in The input stream.
     * @throws IOException if an input operation failed.
     * @throws ParseException if a well know text (WKT) can't be parsed.
     */
    public void loadDefinitions(final LineNumberReader in) throws IOException, ParseException {
        while ((line=readLine(in)) != null) {
            String name=line, value=null;
            final int i = line.indexOf('=');
            if (i >= 0) {
                name  = line.substring(0,i).trim();
                value = line.substring(i+1).trim();
            }
            addDefinition(name, value);
        }
    }

    /**
     * Prints to the {@linkplain #out output stream} a table of all definitions. The content of
     * this table is inferred from the values given to the {@link #addDefinition} method. This
     * method print nothing if the {@linkplain #parser} is not an instance of {@link Preprocessor}.
     *
     * @throws IOException if an error occured while writting to the output stream.
     */
    public void printDefinitions() throws IOException {
        if (parser instanceof Preprocessor) {
            ((Preprocessor) parser).printDefinitions(out);
        }
    }

    /**
     * Returns the command-line prompt, or {@code null} if there is none.
     */
    public String getPrompt() {
        return prompt;
    }

    /**
     * Set the command-line prompt, or {@code null} for none.
     */
    public void setPrompt(final String prompt) {
        this.prompt = prompt;
    }

    /**
     * Read the next line from the specified input stream. Empty lines
     * and comment lines are skipped. If there is no more line to read,
     * then this method returns {@code null}.
     *
     * @param  in The input stream to read from.
     * @return The next non-empty and non-commented line, or {@code null} if none.
     * @throws IOException if the reading failed.
     */
    private static String readLine(final LineNumberReader in) throws IOException {
        String line;
        while ((line=in.readLine()) != null) {
            line = line.trim();
            if (line.length() == 0) {
                // Ignore empty lines.
                continue;
            }
            if (line.startsWith("//")) {
                // Ignore comment lines.
                continue;
            }
            break;
        }
        return line;
    }

    /**
     * Process instructions from the {@linkplain #in input stream} specified at construction
     * time. All lines are read until the end of stream ({@code [Ctrl-Z]} for input from
     * the keyboard), or until {@link #stop()} is invoked. Non-empty and non-comment lines are
     * given to the {@link #execute} method. Errors are catched and printed to the
     * {@linkplain #err error stream}.
     */
    public void run() {
        try {
            while (!stop) {
                if (prompt != null) {
                    out.write(prompt);
                }
                out.flush();
                line = readLine(in);
                if (line == null) {
                    break;
                }
                try {
                    execute(line);
                } catch (Exception exception) {
                    reportError(exception);
                }
            }
            out.flush();
            stop = false;
        } catch (IOException exception) {
            reportError(exception);
        }
    }

    /**
     * Executes all instructions (like {@link #run}), but stop at the first error.
     *
     * @throws Exception if an instruction failed.
     */
    public void executeAll() throws Exception {
        while ((line=readLine(in)) != null) {
            execute(line);
            out.flush();
        }
    }

    /**
     * Execute the specified instruction.
     *
     * @param  instruction The instruction to execute.
     * @throws Exception if the instruction failed.
     */
    protected abstract void execute(String instruction) throws Exception;

    /**
     * Stops the {@link #run} method. This method can been invoked from any thread.
     * If a line is in process, it will be finished before the {@link #run} method
     * stops.
     */
    public void stop() {
        this.stop = true;
    }

    /**
     * Print an exception message to the {@linkplain System#err standard error stream}.
     * The error message includes the line number, and the column where the failure
     * occured in the exception is an instance of {@link ParseException}.
     *
     * @param exception The exception to report.
     * @todo Localize
     */
    protected void reportError(final Exception exception) {
        try {
            out.flush();
        } catch (IOException ignore) {
            Logging.unexpectedException("org.geotools.referencing.wkt",
                                        AbstractConsole.class, "reportError", ignore);
        }
        err.print(Utilities.getShortClassName(exception));
        err.print(" at line ");
        err.print(in.getLineNumber());
        Throwable cause = exception;
        while (true) {
            String message = cause.getLocalizedMessage();
            if (message != null) {
                err.print(": ");
                err.print(message);
            }
            err.println();
            cause = cause.getCause();
            if (cause == null) {
                break;
            }
            err.print("Caused by ");
            err.print(Utilities.getShortClassName(cause));
        }
        err.println("Type 'stacktrace' for stack trace information.");
        if (line!=null && exception instanceof ParseException) {
            AbstractParser.reportError(err, line, ((ParseException)exception).getErrorOffset());
        }
        err.println();
    }
}
