/*
 * Copyright 2007-2012 Scott C. Gray
 *
 * 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.sqsh;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.LogManager;

import org.sqsh.commands.Help;
import org.sqsh.options.Argv;
import org.sqsh.options.OptionProperty;
import org.sqsh.options.OptionException;
import org.sqsh.options.OptionProcessor;

import static org.sqsh.options.ArgumentRequired.REQUIRED;
import static org.sqsh.options.ArgumentRequired.NONE;

/**
 * This implements the public command line interface to kicking off
 * sqsh.
 */
public class JSqsh {

    private static final String LOGGING_CONFIG
        = "org/sqsh/logging.properties";
    
    private static class Options
        extends ConnectionDescriptor {
        
       @OptionProperty(
           option='i', longOption="input-file", arg=REQUIRED, argName="file",
           description="Name of file to read as input. This option may be repeated")
       public List<String> inputFiles = new ArrayList<String>();
       
       @OptionProperty(
           option='o', longOption="output-file", arg=REQUIRED, argName="file",
           description="Name of file send output instead of stdout")
       public String outputFile = null;
       
       @OptionProperty(
           option='e', longOption="echo", arg=NONE, 
           description="Echoes all input back. Useful for running scripts.")
       public boolean isInputEchoed = false;
       
       @OptionProperty(
           option='n', longOption="non-interactive", arg=NONE,
           description="Disables recording of input history, and line editing functionality")
       public boolean isInteractive = true;

       @OptionProperty(
           option='b', longOption="debug", arg=REQUIRED, argName="class",
           description="Turn on debugging for a java class or package")
       public List<String> debug = new ArrayList<String>();
       
       @OptionProperty(
           option='C', longOption="config-dir", arg=REQUIRED, argName="dir",
           description="Configuration directory in addition to $HOME/.jsqsh.")
       public List<String> configDirectories = new ArrayList<String>();

       @OptionProperty(
           option='R', longOption="drivers", arg=REQUIRED, argName="file",
           description="Specifies additional drivers.xml files to be loaded")
       public List<String> driverFiles = new ArrayList<String>();
       
       @OptionProperty(
           option='v', longOption="var", arg=REQUIRED, argName="name=value",
           description="Sets a jsqsh variable. This option may be repeated")
       public List<String> vars = new ArrayList<String>();
       
       @OptionProperty(
           option='W', longOption="width", arg=REQUIRED, argName="cols",
           description="Sets the display width of output")
       public int width = -1;
       
       @OptionProperty(
           option='t', longOption="topic", arg=REQUIRED, argName="topic",
           description="Displays detailed help on specific topics")
       public String topic = null;
       
       @OptionProperty(
           option='z', longOption="setup", arg=NONE, 
           description="Enters jsqsh connection setup wizard")
       public boolean doSetup = false;
       
       @OptionProperty(
           option='X', longOption="exit", arg=REQUIRED, argName="exit-type",
           description="Determines how exit status is computed (\"total\" failures or \"last\" failure")
       public String exitType = null;
       
       @Argv(program="jsqsh", min=0, max=1, usage="[options] [connection-name]")
       public List<String> arguments = new ArrayList<String>();
    }
   
    public static void main (String argv[]) {
        
        Options options = new Options();
        OptionProcessor optParser = new OptionProcessor(options);
        
        try {
            
            optParser.parseOptions(argv);
        }
        catch (OptionException e) {
            
            System.err.println(e.getMessage());
            System.err.println(optParser.getUsage());
            System.err.println("Type \"help jsqsh\" at the jsqsh prompt for "
                + "additional information");
            
            System.exit(1);
        }
        
        /*
         * Display help if requested.
         */
        if (options.doHelp) {
            
            System.out.println(optParser.getUsage());
            System.out.println("Type \"help jsqsh\" at the jsqsh prompt for "
                + "additional information");
            System.exit(0);
        }
        
        
        configureLogging(options.debug);
        
        /*
         * No input files, then add a "null" input file.
         */
        if (options.inputFiles.size() == 0) {
            
            options.inputFiles.add(null);
        }
        
        PrintStream out = getOutputStream(options);
        PrintStream err = System.err;
        
        if (out == null || err == null) {
            
            System.exit(1);
        }
        
        /*
         * In non-interative mode, we force pure-java input. I have found
         * all sorts of conflicts when using jline against input that doesn't
         * come from the console.
         */
        boolean isInteractive = options.isInteractive;

        /*
         * If the first input is a file, then we don't use a line reader
         */
        SqshContext sqsh = SqshContext.getThreadLocal();
        
        int rc = 0;
        
        if (options.width > 0) {
            
            sqsh.setScreenWidth(options.width);
        }
        
        /*
         * Configure how errors are reported.
         */
        if (options.exitType != null) {
            
            if (options.exitType.equalsIgnoreCase("total")) {
                
                sqsh.setExitStatus(SqshContext.ExitStatus.TOTAL_FAILURES);
            }
            else if (options.exitType.equalsIgnoreCase("last")) {
                
                sqsh.setExitStatus(SqshContext.ExitStatus.LAST_FAILURE);
            }
            else {
                
                System.err.println("--exit (-X): Legal values are \"total\" or \"last\"");
                System.exit(1);
            }
        }
        
        for (String dir : options.configDirectories) {

            sqsh.addConfigurationDirectory(dir);
        }

        for (String file : options.driverFiles) {

            sqsh.addDriverFile(file);
        }
        
        sqsh.setInputEchoed(options.isInputEchoed);
        
        setVariables(sqsh, options.vars);
        
        InputStream in = null;
        try {
            
            Session session = sqsh.newSession(isInteractive);
            
            if (options.doSetup) {
                
                Command command = session.getCommandManager().getCommand("\\setup");
                command.execute(session, new String [] { });
            }
            
            if (options.topic != null) {
            
                System.exit(Help.displayHelpText(session, options.topic));
            }
            
            if (!doConnect(session, options)) {
                
                rc = 1;
            }
            
            if (rc != 0) {
                
                System.exit(rc);
            }
            
            for (int i = 0; i < options.inputFiles.size(); i++) {
                
                if (i > 0) {
                    
                    session.getBufferManager().getCurrent().clear();
                }
                
                in = getInputStream(options.inputFiles.get(i));
                if (in == null) {
                    
                    break;
                }
                
                session.setIn(in, (options.inputFiles != null), 
                    isInteractive && (in == System.in));
                session.setOut(out, options.outputFile != null);
                
                /*
                 * If we are forcibly non-interactive then just leave the context
                 * untouched--it is that way by default.
                 */
                if (isInteractive) {
                    
                    /*
                     * It is possible that we will implicitly switch between 
                     * non-interactive and active mode when there is more than 
                     * one input.  The use of stdin is our trigger for 
                     * interactive mode.
                     */
                    if (in == System.in) {
                        
                        session.setInteractive(true);
                    }
                    else {
                        
                        session.setInteractive(false);
                    }
                }
                
                int curRc = sqsh.run(session);
                if (curRc != 0) {
                    
                    rc = curRc;
                }
                
                if (in != System.in) {
                    
                    in.close();
                    in = null;
                }
            }
        }
        catch (Throwable e) {
            
            e.printStackTrace(System.err);
            rc = 1;
        }
        finally {
            
            out.flush();
            err.flush();
            
            if (out != System.out && out != System.err) {
                
                out.close();
            }
            if (err != System.err && err != System.out) {
                
                err.close();
            }
            
            if (in != null && in != System.in) {
                
                try {
                    
                    in.close();
                }
                catch (IOException e) {
                    
                    /* IGNORED */
                }
            }

            sqsh.close();
        }
        
        System.exit(rc);
    }

    static private void configureLogging(List<String> loggers) {
        
        InputStream in = 
            JSqsh.class.getClassLoader().getResourceAsStream(LOGGING_CONFIG);
        if (in == null) {
            
            System.err.println("WARNING: Cannot find resource " 
                + LOGGING_CONFIG);
            return;
        }
        
        try {
            
            LogManager logMan = LogManager.getLogManager();
            logMan.readConfiguration(in);
            in.close();
        }
        catch (IOException e) {
            
            System.err.println("WARNING: Unable to read logging "
                + "properties " + LOGGING_CONFIG + ": " + e.getMessage());
        }

        /*
         * Turn on debugging if requested.
         */
        for (String logger : loggers) {
            
            Logger log = Logger.getLogger(logger);
            if (log != null) {
                
                log.setLevel(Level.FINE);
                System.out.println("Debugging level for '"
                    + log.getName() + "' is now '"
                    + log.getLevel().getName() + "'");
            }
            else {
                
                System.err.println("--debug: Unable to find logger '"
                    + logger + "'");
                System.exit(1);
            }
        }
    }
    
    /**
     * Sets any variables that were assigned with the "-v" argument.
     * @param ctx The context to apply them to
     * @param vars The variables to set
     */
    private static void setVariables(SqshContext ctx, List<String> vars) {
        
        if (vars == null || vars.size() == 0) {
            
            return;
        }

        for (int i = 0; i < vars.size(); i++) {

            ctx.getVariableManager().put(vars.get(i));
        }
    }
    
    /**
     * Returns the input stream to be used by the session.
     * 
     * @param filename If the value is "-", then the System's stdin is returned,
     *                 otherwise an input stream for the filename specified is returned.
     * @return The input stream or null if the requested one cannot be
     *    opened.
     */
    private static InputStream getInputStream(String filename) {
        
        if (filename != null && !"-".equals(filename)) {
            
            try {
                
                InputStream in = new BufferedInputStream(
                    new FileInputStream(filename));
                
                return in;
            }
            catch (IOException e) {
                
                System.err.println("Unable to open input file '" 
                    + filename + "' for read: "
                    + e.getMessage());
                
                return null;
            }
        }
        
        return System.in;
    }
    
    /**
     * Returns the output stream to be used by the session.
     * 
     * @param options Configuration options.
     * @return The input stream or null if the requested one cannot be
     *    opened.
     */
    private static PrintStream getOutputStream(Options options) {
        
        if (options.outputFile != null) {
            
            try {
                
                PrintStream out = new PrintStream(options.outputFile);
                return out;
            }
            catch (IOException e) {
                
                System.err.println("Unable to open output file '" 
                    + options.outputFile + "' for write: "
                    + e.getMessage());
                
                return null;
            }
        }
        
        return System.out;
    }

    /**
     * 
     * Ok, I'm lazy. Since most of the command line options are there
     * to allow the caller to pre-connect sqsh, I am actually just
     * going to build a connect command and execute it rather than
     * messing with API calls to do the dirty work for me.
     * 
     * @param options The command line options that were passed it.
     * @return A string containing a connect command, or null if no
     *   connection is necessary.
     */
    private static boolean doConnect(Session session, Options options) {
        
        ConnectionDescriptor connDesc = (ConnectionDescriptor)options;
        
        boolean ok = true;
        
        /*
         * If any one of our options having to do with establish a connection
         * have been provided, then connect!
         */
        if (options.getServer() != null
                || options.getPort() != -1
                || options.getCatalog() != null
                || options.getUsername() != null
                || options.getPassword() != null
                || options.getJdbcClass() != null
                || options.getDriver() != null
                || options.getDomain() != null
                || options.arguments.size() > 0) {
            
            String connName = null;
            if (options.arguments.size() > 0) {
                
                connName = options.arguments.get(0);
            }
            
            if (connName != null) {
                
                ConnectionDescriptorManager connDescMan = 
                    session.getConnectionDescriptorManager();
                
                ConnectionDescriptor savedOptions = connDescMan.get(connName);
                if (savedOptions == null) {
                    
                    session.err.println("There is no saved connection "
                        + "information named '" + connName + "'.");
                    
                    ok = false;
                }
                else {
                
                    connDesc = connDescMan.merge(savedOptions, connDesc);
                }
            }
            
            if (ok) {
                
                try {
                    
                    ConnectionContext ctx = 
                        session.getDriverManager().connect(session, connDesc);
                    session.setConnectionContext(ctx);
                }
                catch (SQLException e) {
                    
                    SQLTools.printException(session, e);
                    ok = false;
                }
            }
        }
        else {
            
            connDesc = 
                session.getConnectionDescriptorManager().getAutoconnectDescriptor();
            if (connDesc != null) {
                
                session.out.println("Automatically connecting with connection \"" 
                    + connDesc.getName() + "\". Run with --setup to disable autoconnect if necessary.");
                
                try {
                    
                    ConnectionContext ctx = 
                        session.getDriverManager().connect(session, connDesc);
                    session.setConnectionContext(ctx);
                }
                catch (SQLException e) {
                    
                    SQLTools.printException(session, e);
                    ok = false;
                }
            }
        }
        
        return ok;
    }
}