package nxm.ucl.lib;

import java.util.Vector;
import java.util.regex.Pattern;

/** This defines a abstract lexical parser (lexer) for parsing a file.
    Subclasses of this class can be made that will parse the files for various
    programming languages.

    @author  J. Czechowski IV
    @since   NeXtMidas 1.9.2
 */
public abstract class Lexer {
  /** The rules to match. */
  protected Rule[] rules;

  /** The input lines from the file. */
  private String[] inputLines;

  /** The current line number (0-based). */
  private int lineNum;

  /** The new-line token inserted after the end of each line. */
  public static final Token NEW_LINE = new Token("NEW_LINE", "\n");

  /** Creates a new instance of JavaParser
   *  @param rules      The rules to match
   *  @param inputLines The input lines
   */
  public Lexer(Rule[] rules, String[] inputLines) {
    this.rules      = rules;
    this.inputLines = inputLines;
    this.lineNum    = 0;
  }

  /** Processes a set of lines for a file. A {@link #NEW_LINE} token will be
      inserted into the results tokens after every line.

      @return The results tokens.
   */
  public synchronized Token[] getTokens() {
    Vector<Token> tokens = new Vector<>();

    for(this.lineNum=0; this.lineNum < this.inputLines.length; this.lineNum++) {
      String line = this.inputLines[this.lineNum];

      if(line.length() > 0) {
        if(!parseLine(tokens, line, 0, line.length())) {
          throw new SyntaxError("Syntax error near line " + (this.lineNum + 1));
        }
      }

      tokens.add(NEW_LINE);
    }

    return tokens.toArray(new Token[0]);
  }


  /** Parses a line from the file.

      @param tokens    The set of tokens to add any new tokens to.
      @param line      The line to parse.
      @param index     The start position in the line.
      @param length    The  end  position in the line.

      @return true if successful and false otherwise.
   */
  private boolean parseLine(final Vector<Token> tokens,
                            final String line,
                            final int index,
                            final int length) {
    final boolean atStart = (index  == 0);
    final boolean atEnd   = (length == (line.length() - index));

    if (length == 0) {
      return false;
    }

    final String str = line.substring(index, index+length);

    for (Rule rule : rules) {
      //System.out.println("TESTING RULE #" + i + " WITH '" + str + "' --> "
      //                 + rule.matches(str, atStart, atEnd));
      if (rule.matches(str, atStart, atEnd)) {
        tokens.add(rule.makeToken(str, atStart, atEnd));

        if(!atEnd) {
          final int start = index + length;

          return parseLine(tokens, line, start, (line.length() - start));
        } else {
          return true;
        }
      }
    }

    /* No Matches, move shorter. */
    return parseLine(tokens, line, index, (length - 1));
  }


  /** A syntax error. */
  public class SyntaxError extends RuntimeException {
    /** NOTE: this should be changed if the structure/fields of this class changes. */
    private static final long serialVersionUID = 2012122802331L; // for Serializable interface

    /** Creates a new instance.
        @param msg The message.
     */
    public SyntaxError(String msg) {
      super(msg);
    }
  }

  /** A token. */
  public static class Token {
    /** The name of the token. */
    protected String name;

    /** The string that makes up the token. */
    protected String string;

    /** Creates a new instance.
        @param name   The name of the token.
        @param string The string that makes up the token.
     */
    public Token(String name, String string) {
      this.name   = name;
      this.string = string;
    }

    /** Gets the name of the token.
        @return The name of the token.
     */
    public String getName() {
      return this.name;
    }

    /** Gets the string that makes up the token.
        @return The string that makes up the token.
     */
    public String getString() {
      return this.string;
    }
  }

  /** A rule that the lexer can match. */
  protected static interface Rule {
    /** Indicates if this pattern matches the given string.
        @param string    The string.
        @param lineStart Indicates if the beginning of the string matches the
                         beginning of the line.
        @param lineEnd   Indicates if the beginning of the string matches the
                         beginning of the line.
        @return <code>true</code> if it is a match and <code>false</code> otherwise.
     */
    public boolean matches(String  string, boolean lineStart, boolean lineEnd);

    /** The action to take when a matching string is found.
        @param string    The string.
        @param lineStart Indicates if the beginning of the string matches the
                         beginning of the line.
        @param lineEnd   Indicates if the beginning of the string matches the
                         beginning of the line.
        @return <code>true</code> if it is a match and <code>false</code> otherwise.
     */
    public Token makeToken(String  string, boolean lineStart, boolean lineEnd);
  }

  /** A simple rule. */
  protected static class SimpleRule implements Rule {
    /** The name of the pattern. */
    protected final String name;

    /** The pattern. */
    private final Pattern pattern;

    /** Creates a new instance.
        @param name The name of the pattern.
     */
    public SimpleRule(String name, String regex) {
      this.name    = name;
      this.pattern = Pattern.compile(regex);
    }

    public boolean matches(String string, boolean lineStart, boolean lineEnd) {
      return this.pattern.matcher(string).matches();
    }

    public Token makeToken(String  string, boolean lineStart, boolean lineEnd) {
      return new Token(this.name, string);
    }
  }
}
