/*******************************************************************************
 * GenRGenS v.1.0, Generation of Random Genomic Sequences                      *
 * Copyright (C) 2001  Alain Denise/Yann Ponty                                 *
 *                                                                             *
 * 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. *
 *******************************************************************************/

package GenRGenS.utility;

import java.io.*;
import java.util.*;
import GenRGenS.utility.callbacks.*;
import GenRGenS.*;

/**
 * Cette classe est destinée ?l'interprétation de la ligne de commande.
 * Elle est inspirée de la bibliothèque <code>Arg</code> implémentée pour
 * <code>OCaml</code> par l'<code>INRIA</code>.<P>
 * Après avoir créé une instance de cette classe, l'utilisateur utilise sa
 * méthode <code>addRule</code> pour ajouter une règle d'interprétation ?
 * appliquer ?la ligne de commande.<P>
 * Une fois les règles définies, l'utilisateur défini la règle par défaut avec
 * la méthode <code>setDefaultRule</code>.<P>
 * n.b. : La fonction enregistrée pour la règle générique est le type StringCallback.<P>
 * Par défaut, la fonction invoquée sur lecture d'une chaine de caractère non énumerée
 * dans la liste des règles est une fonction "vide" (i.e. sans effet particulier).
 * L'utilisateur peut définir une chaîne de caractère <code>usage</code> rappelant la
 * syntaxe d'appel du programme via la méthode <code>setUsage</code>.<P>
 * Une fois toutes ces manipulations effectuées, l'utilisateur déclenche l'analyse
 * de la ligne de commande via la méthode <code>parse</code>.<P>
 * Note : <code>parse</code> ne provoque pas de sortie de programme une fois les
 * différentes fonctions enregistrées appellées avec succés. Elle ne quitte en affichant
 * une aide que quand la syntaxe d'appel n'est pas respectée.
 * @author Yann Ponty
 * @see Callback
 * @see VoidCallback
 * @see IntCallback
 * @see CharCallback
 * @see StringCallback
 */


public class CommandLineParser
{
  private String _usage = "";
  private StringCallback _default =
      new StringCallback()
        {
          public void fun(String s)
          {
          }
        };
  private Hashtable _rules = new Hashtable(11);
  private Hashtable _category = new Hashtable(11);


  /**
   * Construit une instance de la classe <code>CommandLineParser</code>
   * o?le programme ?executer est appel?selon une syntaxe décrite ultérieurement.
   * @param usage Une description destinée ?l'utilisateur de la syntaxe d'appel
   */
  public CommandLineParser( String usage )
  {
    super();
    _usage = usage;
  }


  /**
   * Construit une instance de la classe <code>CommandLineParser</code>
   * o?le programme execut?est appel?selon une syntaxe décrite et avec
   * une fonction par défaut donnée.
   * @param usage Une description destinée ?l'utilisateur de la syntaxe d'appel
   * @param dflt  Une fonction enregistrée (Callback) prenant une <code>String</code>
   * comme argument
   */
  public CommandLineParser( String usage, StringCallback dflt )
  {
    super();
    _usage = usage;
    _default = dflt;
  }


  /**
   * Construit une instance de la classe <code>CommandLineParser</code>
   * o?le programme exécut?est appel?avec une fonction par défaut donnée.
   * @param dflt  Une fonction enregistrée (Callback) prenant une <code>String</code>
   * comme argument
   */
  public CommandLineParser( StringCallback dflt )
  {
    super();
    _default = dflt;
  }


  /**
   * Ajoute une règle ?l'analyse de la ligne de commande. Une règle est caractérisée par
   * la chaîne de caractères ?reconnaître, la fonction ?exécuter sur lecture de la chaine
   * et par la chaine de caractere décrivant la sémantique de l'action engendrée par la règle.<P>
   * n.b. : Si deux règles sont déclarées avec la même chaîne ?reconnaitre (param. lexeme), alors
   * seule la deuxième sera exécutée.
   * @param lexeme La chaîne dont la lecture déclenche l'execution de la règle
   * @param behaviour La fonction enregistrée ?appeler sur lecture de la chaîne <code>lexeme</code>
   * @param help La chaîne de caractères décrivant la sémantique de la règle
   */
  public void addRule( String lexeme, Callback behaviour, String help, String category)
  {
    _rules.put(lexeme, new Couple(behaviour,help));
    if (!_category.containsKey(category))
    {
      Vector v = new Vector();
      v.add(lexeme);
      _category.put(category,v);
    }
    else
    {
      Vector v = (Vector) _category.get(category);
      v.add(lexeme);
    }
  }

  /**
   * Ajoute une règle ?l'analyse de la ligne de commande. Une règle est caractérisée par
   * la chaîne de caractères ?reconnaître, la fonction ?exécuter sur lecture de la chaine
   * et par la chaine de caractere décrivant la sémantique de l'action engendrée par la règle.<P>
   * n.b. : Si deux règles sont déclarées avec la même chaîne ?reconnaitre (param. lexeme), alors
   * seule la deuxième sera exécutée.
   * @param rule La règle ?ajouter
   */
  public void addRule(ParseRule rule)
  {
    addRule(rule.getLexeme(), rule.getBehaviour(),rule.getHelp(),"");
  }

  /**
   * Ajoute une règle ?l'analyse de la ligne de commande. Une règle est caractérisée par
   * la chaîne de caractères ?reconnaître, la fonction ?exécuter sur lecture de la chaine
   * et par la chaine de caractere décrivant la sémantique de l'action engendrée par la règle.<P>
   * n.b. : Si deux règles sont déclarées avec la même chaîne ?reconnaitre (param. lexeme), alors
   * seule la deuxième sera exécutée.
   * @param rule La règle ?ajouter
   * @param category La catégorie à laquelle appartient cette règle.
   */
  public void addRule(ParseRule rule,String category)
  {
    addRule(rule.getLexeme(), rule.getBehaviour(),rule.getHelp(),category);
  }

  /**
   * Modifie la chaine de caracteres destinée ?eclairer l'utilisateur sur la syntaxe
   * d'appel du programme
   * @param usage La chaine de caractère décrivant la syntaxe d'appel du programme
   */
  public void setUsage( String usage )
  {
    _usage = usage;
  }


  /**
   * Modifie la fonction enregistrée appelée sur la découverte d'une chaine
   * de caractère n'étant pas la partie gauche d'une règle.
   * @param dflt La fonction de type enregistrée appelée par defaut sur
   * rencontre d'une chaîne
   */
  public void setDefaultRule( StringCallback dflt )
  {
    _default = dflt;
  }


  /**
   * Affiche la chaîne de caractères msg dans l'intervalle spécifiée.
   * Attends de l'utilisateur que le curseur soit plac?sur la première
   * colonne de l'intervalle.
   * @param msg La chaîne de caractères ?afficher
   * @param beg_box Le numéro de colonne gauche de l'intervalle
   * @param end_box Le numéro de colonne droite de l'intervalle (> beg_box)
   */
  private void boxPrint(String msg, int beg_box, int end_box)
  {
    int i = beg_box;
    char[] buffer = msg.toCharArray();
    char[] blank = new char[beg_box-1];

    for(int k = 0; k < blank.length;k++)
    { blank[k] = ' '; }

    for(int j = 0; j < buffer.length;j++)
    {
      GenRGenS.err.print(buffer[j]);
      i++;
      if (buffer[j] == '\n')
      {
        GenRGenS.err.print(new String(blank));
        i = beg_box;
      }
      else
      {
        if (i > end_box)
        {
          GenRGenS.err.print("\n"+new String(blank));
          i = beg_box;
        }
      }
    }
  }


  /**
   * Affiche l'aide. Cette procedure est appelee automatiquement
   * des qu'une erreur survient.
   */
  public void printHelp()
  {
    int max_size = 0;
    char[] blank;
    GenRGenS.err.println(_usage);
    GenRGenS.err.println("Options :");

    if(_category.containsKey(""))
    {
      Enumeration my_enum = ((Vector)_category.get("")).elements();
      while(my_enum.hasMoreElements())
      {
        String s = (String) my_enum.nextElement();
        if (s.length() + 2 > max_size)
        { max_size = s.length() + 2; }
      }

      my_enum = ((Vector)_category.get("")).elements();
      blank = new char[max_size];
      while(my_enum.hasMoreElements())
      {
        String s = (String) my_enum.nextElement();
        Couple regle = (Couple) _rules.get(s);
        for(int k = 0; k < blank.length;k++)
        {
          if ((k-1 >=0) && (k-1 < s.length()))
          { blank[k] = s.charAt(k-1); }
          else
          { blank[k] = ' '; }
        }
        String help  = (String) regle.snd();
        GenRGenS.err.print(new String(blank));
        boxPrint(help,max_size+1,79);
        GenRGenS.err.println();
      }
    }
      
    Enumeration lexemes = _category.keys();
    while(lexemes.hasMoreElements())
    {
      String lexeme = (String) lexemes.nextElement();
      if(!lexeme.equals(""))
      {
        GenRGenS.err.println(" "+lexeme+" :");
        Enumeration my_enum = ((Vector)_category.get(lexeme)).elements();
        while(my_enum.hasMoreElements())
        {
          String s = (String) my_enum.nextElement();
          if (s.length() + 2 > max_size)
          { max_size = s.length() + 2; }
        }
        my_enum = ((Vector)_category.get(lexeme)).elements();
        blank = new char[max_size];
        while(my_enum.hasMoreElements())
        {
          String s = (String) my_enum.nextElement();
          Couple regle = (Couple) _rules.get(s);
          for(int k = 0; k < blank.length;k++)
          {
            if ((k-1 >=0) && (k-1 < s.length()))
            { blank[k] = s.charAt(k-1); }
            else
            { blank[k] = ' '; }
          }
          String help  = (String) regle.snd();
          GenRGenS.err.print(new String(blank));
          boxPrint(help,max_size+1,79);
          GenRGenS.err.println();
        }
      }
    }
  }

  /**
   * Analyse la ligne de commande en fonction des caractéristiques du parser définie par
   * l'utilisateur de la classe.
   * Si l'analyse échoue (i.e. la syntaxe d'appel est erronée), provoque la sortie du programme
   * avec un code de sortie différent de 0 et l'affichage d'un message d'erreur puis de l'aide du programme.
   * @param argv La ligne de commande telle qu'elle est passée en paramètre au <code>main</code>
   */
  private void check(String[] argv)
  {
    int index_arg = 0;
    Couple applied_rule = null;
    try
    {
      while(index_arg < argv.length)
      {
        applied_rule = (Couple) _rules.get(argv[index_arg]);
        if (applied_rule == null)
        {
          if ((argv[index_arg].lastIndexOf("help")!=-1)||(argv[index_arg].lastIndexOf("?")!=-1))
          {
            printHelp();
            System.exit(1);
            index_arg++;
          }
          else
          {
            index_arg++;
          }
        } 
        else
        {
          Callback cb = (Callback) applied_rule.fst();
          switch(cb.getTypename())
          {
            case(Callback.TYPE_VOID) :
            {
              index_arg++;
            }
            break;
            case(Callback.TYPE_BOOLEAN) :
            {
              char first = argv[index_arg+1].toUpperCase().charAt(0);
              if ((argv[index_arg+1].length()>1)||((first != 'T')&&(first != 'F')))
              {
                GenRGenS.err.println('"' + argv[index_arg+1] + '"' + " is not a legal boolean value !\n( Only \'T\' and \'F\' are allowed)");
                printHelp();
                System.exit(1);
              }
              else
              {
                index_arg += 2;
              }
            }
            break;
            case(Callback.TYPE_CHAR) :
            {
              if (argv[index_arg+1].length()>1)
              {
                GenRGenS.err.println('"' + argv[index_arg+1] + '"' + " is not a character !");
                printHelp();
                System.exit(1);
              }
              else
              {
                index_arg += 2;
              }
            } 
            break;
            case(Callback.TYPE_INT) :
            {
              try
              {
                if (index_arg+1 > argv.length -1 )
                {throw(new ArrayIndexOutOfBoundsException());}
                Integer.parseInt(argv[index_arg+1]);
                index_arg += 2;
              }
              catch(NumberFormatException e)
              {
                GenRGenS.err.println('"' + argv[index_arg+1]+ '"' + " is not an integer !");
                printHelp();
                System.exit(1);
              }
            }
            break;
            case(Callback.TYPE_STRING) :
            {
              if (index_arg+1 > argv.length -1 )
              {throw(new ArrayIndexOutOfBoundsException());}
              index_arg += 2;
            }
            break;
            case(Callback.TYPE_REAL) :
            {
              if (index_arg+1 > argv.length -1 )
              {throw(new ArrayIndexOutOfBoundsException());}
              Double.parseDouble(argv[index_arg+1]);
              index_arg += 2;
            }
            break;
          }
        }
      } 
    }
    catch(ArrayIndexOutOfBoundsException e)
    {
       GenRGenS.err.println("Missing argument after \"" + argv[argv.length-1]+ "\" !");
       printHelp();
       System.exit(1);
    }
  }


  /**
   * Provoque l'analyse puis l'interprétation de la ligne de commande suivant les règles définies
   * par l'utilisateur au moyen des méthodes <code>addRule<code> et <code>setDefaultRule<code>.
   * Si l'analyse échoue, renvoie un message tentant d'expliquer l'origine du refus sur la sortie
   * <code>err<code>, puis y affiche la syntaxe d'appel du logiciel.
   * Si l'analyse réussit, appelle les fonctions enregistrées qui s'imposent ...
   * @param argv La ligne de commande ?interpréter.
   */

  public void parse(String[] argv)
  {
    int index_arg = 0;
    Couple applied_rule = null;
    check(argv);
    while(index_arg < argv.length)
    {
      applied_rule = (Couple) _rules.get(argv[index_arg]);
      if (applied_rule == null)
      {
        _default.fun(argv[index_arg]);
        index_arg++;
      }
      else
      {
        Callback cb = (Callback) applied_rule.fst();
        switch(cb.getTypename())
        {
          case(Callback.TYPE_VOID) :
          {
            ((VoidCallback)cb).fun();
            index_arg++;
          }
          break;
          case(Callback.TYPE_BOOLEAN) :
          {
            if (argv[index_arg+1].length()>1)
            {
              GenRGenS.err.println("This couldn't happen !");
            }
            else
            {
                if (argv[index_arg+1].toLowerCase().equals("t"))
              {
                ((BooleanCallback)cb).fun(true);
                index_arg += 2;
              }
              else
              {
                ((BooleanCallback)cb).fun(false);
                index_arg += 2;
              }
            }
          }
          break;
          case(Callback.TYPE_CHAR) :
          {
            if (argv[index_arg+1].length()>1)
            {
              GenRGenS.err.println("This couldn't happen !");
            }
            else
            {
              ((CharCallback)cb).fun(argv[index_arg+1].charAt(0));
              index_arg += 2;
            }
          }
          break;
          case(Callback.TYPE_INT) :
          {
            try
            {
              ((IntCallback)cb).fun(Integer.parseInt(argv[index_arg+1]));
              index_arg += 2;
            }
            catch(NumberFormatException e)
            {
              GenRGenS.err.println("This couldn't happen !");
            }
          }
          break;
          case(Callback.TYPE_STRING) :
          {
            ((StringCallback)cb).fun(argv[index_arg+1]);
            index_arg += 2;
          }
          break;
          case(Callback.TYPE_REAL) :
          {
              ((RealCallback)cb).fun(Double.parseDouble(argv[index_arg+1]));
            index_arg += 2;
          }
          break;
        }
      }
    }
  }

}