package com.dbxml.db.client.tools;

/*
 * dbXML - Native XML Databasein
 * Copyright (C) 1999-2004  The dbXML Group, L.L.C.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * $Id: CommandLine.java,v 1.9 2004/08/22 04:18:41 bradford Exp $
 */

import java.io.*;
import java.util.*;

import com.dbxml.db.client.CollectionClient;
import com.dbxml.db.client.dbXMLClient;
import com.dbxml.db.server.dbXML;
import com.dbxml.util.Configuration;
import com.dbxml.util.ConfigurationCallback;
import com.dbxml.util.Stopwatch;
import com.dbxml.util.StringUtilities;
import com.dbxml.util.dbXMLException;
import com.dbxml.xml.dom.DOMHelper;
import org.w3c.dom.Document;

/**
 * CommandLine
 */

public final class CommandLine {
   private static final String[] EmptyStrings = new String[0];

   private static final String SET = "set";
   private static final String CATEGORY = "category";
   private static final String COMMAND = "command";
   private static final String CLASS = "class";
   private static final String VERB = "verb";
   private static final String NAME = "name";
   private static final String VALUE = "value";
   private static final String USAGE = "usage";
   private static final String DESCRIPTION = "description";
   private static final String HELP = "help";

   public static final String COLLECTION = "collection";
   public static final String VERBOSE = "verbose";
   public static final String INTERACTIVE = "interactive";
   public static final String TOTALS = "totals";
   public static final String DURATIONS = "durations";
   public static final String CONFIRM = "confirm";

   private static final int TOKEN_VALUE = 0;
   private static final int TOKEN_WHITESPACE = 1;
   private static final int TOKEN_OPERATOR = 2;
   private static final int TOKEN_STRING = 3;

   private dbXMLClient client;
   private Map commands = new HashMap();
   private Map props = new TreeMap();

   private BufferedReader in;
   private StringTokenizer st;
   private String token;
   private PrintWriter out;
   private PrintWriter err;

   public CommandLine() {
      setProperty(TOTALS, Boolean.TRUE);
      setProperty(DURATIONS, Boolean.TRUE);
      setProperty(CONFIRM, Boolean.FALSE);

      try {
         OutputStreamWriter osw = new OutputStreamWriter(System.out, "UTF8");
         BufferedWriter bw = new BufferedWriter(osw, 4096);
         setWriter(bw);

         osw = new OutputStreamWriter(System.err, "UTF8");
         bw = new BufferedWriter(osw, 4096);
         setErrorWriter(bw);

         readCommands();
      }
      catch ( Exception e ) {
         e.printStackTrace(System.err);
         System.exit(1);
      }
   }

   private void readCommands() throws Exception {
      String home = System.getProperty(dbXML.PROP_DBXML_HOME);

      File homeFile = new File(home);
      File cfgFile = new File(homeFile, "config/commands.xml");
      Document doc = DOMHelper.parse(cfgFile);

      Configuration cfg = new Configuration(doc);
      final String[] lastCatName = new String[1];
      final Map helpMap = new HashMap();
      final List catList = new ArrayList();

      setProperties(cfg);

      cfg.processChildren(CATEGORY, new ConfigurationCallback() {
         public void process(Configuration cc) {
            String name = cc.getAttribute(NAME);
            lastCatName[0] = name;
            String desc = cc.getAttribute(DESCRIPTION);
            catList.add(StringUtilities.leftJustify(name, 15) + " " + desc);

            final StringBuffer sbHelp = new StringBuffer();
            sbHelp.append(cc.getAttribute(DESCRIPTION) + ":\n");
            cc.processChildren(COMMAND, new ConfigurationCallback() {
               public void process(Configuration c) {
                  try {
                     String cName = c.getAttribute(CLASS);
                     Class cl = Class.forName(cName);
                     Command cmd = (Command)cl.newInstance();
                     cmd.setCommandLine(CommandLine.this);

                     String verb = c.getAttribute(VERB).toUpperCase();
                     String usage = c.getChild(USAGE).getValue().trim();
                     String desc = c.getChild(DESCRIPTION).getValue().trim();
                     String help = c.getChild(HELP).getValue();

                     StringBuffer sb = new StringBuffer();
                     sb.append("Command: "+verb+"\n");
                     sb.append("Purpose: "+desc+"\n");
                     sb.append("  Usage: "+usage+"\n");
                     sb.append(help);

                     CommandInfo info = new CommandInfo(verb, cmd, desc, sb.toString());
                     commands.put(verb, info);

                     sbHelp.append("\n   " + StringUtilities.leftJustify(verb, 15) + " " + desc);
                  }
                  catch ( Exception e ) {
                     e.printStackTrace(System.err);
                     System.exit(1);
                  }
               }
            });
            helpMap.put(name.toUpperCase(), sbHelp.toString());
         }
      });

      StringBuffer sbHelp = new StringBuffer();
      sbHelp.append("There are several help categories to choose from:\n\n");
      for ( int i = 0; i < catList.size(); i++ )
         sbHelp.append("   " + catList.get(i) + "\n");
      sbHelp.append("\nExample: help " + lastCatName[0]);

      // Add the hardcoded help Command
      commands.put("HELP", new CommandInfo("HELP", new Help(helpMap), "", sbHelp.toString()));
   }

   private void setProperties(Configuration cfg) {
      cfg.processChildren(SET, new ConfigurationCallback() {
         public void process(Configuration cc) {
            String name = cc.getAttribute(NAME);
            String value = cc.getAttribute(VALUE);
            try {
               setProperty(name, value);
            }
            catch ( Exception e ) {
               err.println("Error: " + e.getMessage());
               if ( isPropertyTrue(VERBOSE) )
                  e.printStackTrace(err);
               if ( isPropertyTrue(INTERACTIVE) )
                  err.println();
            }
         }
      });
   }

   public void setProperty(String name, Object value) {
      props.put(name, value);
   }

   public void setProperty(String name, String value) throws dbXMLException {
      Object obj = props.get(name);
      Object newObj = null;

      if ( value == null ) {
         props.remove(name);
         return;
      }

      // Settings for boolean values
      if ( value.equalsIgnoreCase("true") || value.equalsIgnoreCase("yes") )
         newObj = Boolean.TRUE;
      else if ( value.equals("1") ) {
         if ( obj instanceof Boolean )
            newObj = Boolean.TRUE;
         else
            newObj = new Integer(1);
      }
      else if ( value.equalsIgnoreCase("false") || value.equalsIgnoreCase("no") )
         newObj = Boolean.FALSE;
      else if ( value.equals("0") ) {
         if ( obj instanceof Boolean )
            newObj = Boolean.FALSE;
         else
            newObj = new Integer(0);
      }

      // Settings for integer values
      if ( newObj == null ) {
         try {
            newObj = new Integer(value);
         }
         catch ( Exception e ) {
            newObj = null;
         }
      }

      // String value
      if ( newObj == null )
         newObj = value;

      if ( obj != null && (obj.getClass() != newObj.getClass()) )
         throw new dbXMLException("Cannot change type of property '" + name + "'");

      props.put(name, newObj);
   }

   public Object getProperty(String name) {
      return props.get(name);
   }

   public String[] listProperties() {
      return (String[])props.keySet().toArray(EmptyStrings);
   }

   public BufferedReader getReader() {
      return in;
   }

   public PrintWriter getWriter() {
      return out;
   }

   public PrintWriter getErrorWriter() {
      return err;
   }

   public void setClient(dbXMLClient client) {
      this.client = client;
   }

   public dbXMLClient getClient() throws dbXMLException {
      if ( client == null )
         throw new dbXMLException("Not connected");
      return client;
   }

   public boolean isPropertyTrue(String name) {
      Object obj = getProperty(name);
      return obj instanceof Boolean && ((Boolean)obj).booleanValue();
   }

   public String readLine(String prefix) throws IOException {
      if ( getProperty(INTERACTIVE) == Boolean.TRUE ) {
         out.print(prefix);
         out.flush();
      }
      return in.readLine();
   }

   private void unknownVerb(String verb) {
      err.println();
      err.println("Unknown command verb '" + verb + "'");
      err.println("Type 'HELP' for a list of command categories");
      err.println();
   }

   public void setReader(Reader in) {
      if ( in instanceof BufferedReader )
         this.in = (BufferedReader)in;
      else
         this.in = new BufferedReader(in, 4096);
   }

   public void setWriter(Writer out) {
      if ( out instanceof PrintWriter )
         this.out = (PrintWriter)out;
      else
         this.out = new PrintWriter(out, true);
   }

   public void setErrorWriter(Writer err) {
      if ( err instanceof PrintWriter )
         this.err = (PrintWriter)err;
      else
         this.err = new PrintWriter(err, true);
   }

   public void process() {
      String line = null;
      CollectionClient col = null;
      String colName = "";

      if ( getProperty(INTERACTIVE) == Boolean.TRUE ) {
         out.println(dbXML.Title + " " + dbXML.Version + " Interactive Mode");
         out.println("Type 'HELP' for a list of command categories");
      }

      do {
         try {
            CollectionClient nc = (CollectionClient)getProperty(COLLECTION);
            if ( nc != col ) {
               col = nc;
               if ( col != null )
                  colName = col.getCanonicalName();
               else
                  colName = "";
            }

            line = readLine(colName + ">");
            if ( line != null ) {
               st = new StringTokenizer(line, "=\" \t\n\r", true);
               fetchNextToken();
               if ( hasMoreTokens() ) {
                  String verb = getNextToken().toUpperCase();
                  CommandInfo info = (CommandInfo)commands.get(verb);
                  if ( info != null ) {
                     if ( isPropertyTrue(INTERACTIVE) )
                        out.println();
                     Stopwatch sw = new Stopwatch("Execution", true);
                     info.command.process();
                     sw.stop();
                     if ( isPropertyTrue(DURATIONS) )
                        out.println("(" + sw.toString() + ")");
                     if ( isPropertyTrue(INTERACTIVE) )
                        out.println();
                  }
                  else
                     unknownVerb(verb);
               }
            }
         }
         catch ( Exception e ) {
            err.println("Error: " + e.getMessage());
            if ( isPropertyTrue(VERBOSE) )
               e.printStackTrace(err);
            if ( isPropertyTrue(INTERACTIVE) )
               err.println();
         }
      }
      while ( line != null );

      try {
         if ( client != null )
            client.disconnect();
      }
      catch ( Exception e ) {
         // Whatever
      }
   }

   public static void main(String[] args) {
      CommandLine cl = new CommandLine();

      try {
         String userHome = System.getProperty("user.home");
         File cfgFile = new File(userHome, ".dbxml.config");
         if ( cfgFile.exists() && cfgFile.isFile() ) {
            Document cfgDoc = DOMHelper.parse(cfgFile);
            Configuration cfg = new Configuration(cfgDoc);
            cl.setProperties(cfg);
         }
      }
      catch ( Exception e ) {
         System.err.println("Error: "+e.getMessage());
         e.printStackTrace(System.err);
      }

      try {
         if ( args.length == 0 ) {
            cl.setProperty(INTERACTIVE, Boolean.TRUE);
            InputStreamReader isr = new InputStreamReader(System.in, "UTF8");
            cl.setReader(isr);
         }
         else {
            cl.setProperty(INTERACTIVE, Boolean.FALSE);
            FileInputStream fis = new FileInputStream(args[0]);
            BufferedInputStream bis = new BufferedInputStream(fis, 4096);
            InputStreamReader isr = new InputStreamReader(bis, "UTF8");
            cl.setReader(isr);
         }
         cl.process();
      }
      catch ( Exception e ) {
         System.err.println("Error: "+e.getMessage());
         e.printStackTrace(System.err);
      }
   }

   /**
    * parseProperties parses a list of name/value pairs.
    */
   public Properties parseProperties() throws dbXMLException {
      Properties properties = new Properties();

      while ( st.hasMoreTokens() ) {
         String name = getNextToken();
         if ( getTokenType(name) != TOKEN_VALUE )
            throw new dbXMLException("Name Token Expected");

         String operator = getNextToken();
         if ( getTokenType(operator) != TOKEN_OPERATOR )
            throw new dbXMLException("Operator Token Expected");

         String value = getNextToken();
         switch ( getTokenType(value) ) {
            case TOKEN_VALUE:
               properties.setProperty(name, value);
               break;

            default:
               throw new dbXMLException("Value Token Expected");
         }
      }
      return properties;
   }

   public int getTokenType(String token) {
      if ( token.length() == 1 ) {
         switch ( token.charAt(0) ) {
            case '=':
               return TOKEN_OPERATOR;

            case '\"':
               return TOKEN_STRING;

            case ' ':
            case '\t':
            case '\n':
            case '\r':
               return TOKEN_WHITESPACE;

            default:
               return TOKEN_VALUE;
         }
      }
      else
         return TOKEN_VALUE;
   }

   public boolean hasMoreTokens() {
      return token != null;
   }

   public void fetchNextToken() throws dbXMLException {
      while ( true )  {
         if ( !st.hasMoreTokens() ) {
            token = null;
            return;
         }

         token = st.nextToken();
         switch ( getTokenType(token) ) {
            case TOKEN_STRING:
               StringBuffer sb = new StringBuffer();
               boolean done = false;
               while ( !done ) {
                  if ( st.hasMoreTokens() ) {
                     String value = st.nextToken();
                     switch ( getTokenType(value) ) {
                        case TOKEN_STRING:
                           done = true;
                           break;
                        default:
                           sb.append(value);
                           break;
                     }
                  }
                  else
                     throw new dbXMLException("String Token Not Terminated");
               }
               token = sb.toString();
               return;

            case TOKEN_WHITESPACE:
               continue;

            default:
               return;
         }
      }
   }

   public String getNextToken(String tokenName) throws dbXMLException {
      if ( token == null )
         throw new dbXMLException(tokenName+" Expected");

      String result = token;
      fetchNextToken();
      return result;
   }

   public String getNextToken() throws dbXMLException {
      return getNextToken("Token");
   }


   /**
    * CommandInfo
    */

   private class CommandInfo {
      public String verb;
      public Command command;
      public String description;
      public String help;

      public CommandInfo(String verb, Command command, String description, String help) {
         this.verb = verb;
         this.command = command;
         this.description = description;
         this.help = help;
      }
   }


   /**
    * Help
    */

   private class Help implements Command {
      private Map helpMap;

      public Help(Map helpMap) {
         this.helpMap = helpMap;
      }

      public CommandLine getCommandLine() {
         return CommandLine.this;
      }

      public void setCommandLine(CommandLine cl) {
         // NOOP
      }

      public void process() throws dbXMLException {
         String verb = "HELP";
         if ( hasMoreTokens() )
            verb = getNextToken().toUpperCase();
         String catHelp = (String)helpMap.get(verb);
         if ( catHelp != null )
            out.println(catHelp);
         else {
            CommandInfo info = (CommandInfo)commands.get(verb);
            if ( info != null )
               out.println(info.help);
            else
               unknownVerb(verb);
         }
      }
   }
}


