//
// This software is now distributed according to
// the Lesser Gnu Public License.  Please see
// http://www.gnu.org/copyleft/lesser.txt for
// the details.
//    -- Happy Computing!
//
package com.stevesoft.pat;

import java.util.Hashtable;

/**
 * ReplaceRule is a singly linked list of Objects which describe how to replace
 * the matched portion of a String. The only member method that you absolutely
 * need to define to use this class is apply(StringBuffer,RegRes) -- although
 * you may want define toString1() and clone1() (if you are unhappy with the
 * default methods) that are needed by the clone() or toString() methods on this
 * class. During the replacement process, each ReplaceRule tells the replacer
 * what to add to StringBuffer and uses the contents of the Regular expression
 * result to get the information it needs to do this. Here is an
 * <a href="http://javaregex.com/code/fancy.java.html">example</a>
 * 
 * @see com.stevesoft.pat.NullRule
 * @see com.stevesoft.pat.AmpersandRule
 * @see com.stevesoft.pat.BackRefRule
 * @see com.stevesoft.pat.LeftRule
 * @see com.stevesoft.pat.RightRule
 * @see com.stevesoft.pat.StringRule
 */
public abstract class ReplaceRule
{
  /** points to the next ReplaceRule in the linked list. */
  protected ReplaceRule next = null;

  /**
   * This function appends to the StringBufferLike the text you want to replaced
   * the portion of the String last matched.
   */
  public abstract void apply(StringBufferLike sb, RegRes r);

  /**
   * A rule describing how to clone only the current ReplaceRule, and none of
   * the others in this linked list. It is called by clone() for each item in
   * the list.
   */
  public Object clone1()
  {
    return new RuleHolder(this);
  }

  public final Object clone()
  {
    ReplaceRule x = (ReplaceRule) clone1();
    ReplaceRule xsav = x;
    ReplaceRule y = this;
    while (y.next != null)
    {
      x.next = (ReplaceRule) y.next.clone1();
      x.name = y.name;
      x = x.next;
      y = y.next;
    }
    return xsav;
  }

  static ReplaceRule add(ReplaceRule head, ReplaceRule adding)
  {
    if (head == null)
    {
      return head = adding;
    }
    head.addRule(adding);
    return head;
  }

  public ReplaceRule add(ReplaceRule adding)
  {
    return add(this, adding);
  }

  /** Add another ReplaceRule to the linked list. */
  public void addRule(ReplaceRule r)
  {
    if (next == null)
    {
      next = r;
    }
    else
    {
      next.addRule(r);
    }
  }

  static Regex getvar = null;

  final static Regex getv()
  {
    // Thanks to Michael Jimenez for pointing out the need
    // to clone getvar rather than simply returning it.
    // Previously this was not thread safe.
    // if(getvar != null) return getvar;
    if (getvar != null)
    {
      return (Regex) getvar.clone();
    }
    getvar = new Regex("(?:\\\\(\\d+)|" + // ref 1
            "\\$(?:" + "(\\d+)|" + // ref 2
            "(\\w+)|" + // ref 3
            "([&'`])|" + // ref 4
            "\\{(?:(\\d+)|" + // ref 5
            "([^\n}\\\\]+))}" + // ref 6
            ")|" + "\\\\([nrbtaef])|" + // ref 7
            "\\\\c([\u0000-\uFFFF])|" + // ref 8
            "\\\\x([A-Fa-f0-9]{2})|" + // ref 9
            "\\\\([\u0000-\uFFFF])" + // ref 10
            ")");
    getvar.optimize();
    return getvar;
  }

  /**
   * Compile a ReplaceRule using the text that would go between the second and
   * third /'s in a typical substitution pattern in Perl: s/ ... / <i>The
   * argument to ReplaceRule.perlCode</i> /.
   */
  public static ReplaceRule perlCode(String s)
  {
    // String sav_backGs = Regex.backGs;
    // int sav_backGto = Regex.backGto;
    try
    {
      int mf = 0, mt = 0;
      Regex gv = getv();
      ReplaceRule head = null;
      Object tmp = null;
      while (gv.searchFrom(s, mt))
      {
        int off = Regex.BackRefOffset - 1;
        mf = gv.matchedFrom();
        if (mf > mt)
        {
          head = add(head, new StringRule(s.substring(mt, mf)));
        }
        String var = null;
        if ((var = gv.stringMatched(1 + off)) != null
                || (var = gv.stringMatched(2 + off)) != null
                || (var = gv.stringMatched(5 + off)) != null)
        {
          int d = 0;
          for (int i = 0; i < var.length(); i++)
          {
            d = 8 * d + (var.charAt(i) - '0');
          }
          if (var.length() == 1)
          {
            head = add(head, new BackRefRule(d));
          }
          else
          {
            head = new StringRule("" + (char) d);
          }
        }
        else if ((var = gv.stringMatched(10 + off)) != null)
        {
          if ("QELlUu".indexOf(var) >= 0)
          {
            head = add(head, new CodeRule(var.charAt(0)));
          }
          else
          {
            head = add(head, new StringRule(var));
          }
        }
        else if ((var = gv.stringMatched(3 + off)) != null
                || (var = gv.stringMatched(4 + off)) != null
                || (var = gv.stringMatched(6 + off)) != null)
        {
          String arg = "";
          int pc;
          if ((pc = var.indexOf(':')) > 0)
          {
            arg = var.substring(pc + 1);
            var = var.substring(0, pc);
          }
          if (var.equals("&") || var.equals("MATCH"))
          {
            head = add(head, new AmpersandRule());
          }
          else if (var.equals("`") || var.equals("PREMATCH"))
          {
            head = add(head, new LeftRule());
          }
          else if (var.equals("'") || var.equals("POSTMATCH"))
          {
            head = add(head, new RightRule());
          }
          else if (var.equals("WANT_MORE_TEXT"))
          {
            head = add(head, new WantMoreTextReplaceRule());
          }
          else if (var.equals("POP"))
          {
            head = add(head, new PopRule());
          }
          else if (var.startsWith("+")
                  && (tmp = defs.get(var.substring(1))) != null)
          {
            if (tmp instanceof Regex)
            {
              head = add(head, new PushRule(var.substring(1), (Regex) tmp));
            }
            else if (tmp instanceof Transformer)
            {
              head = add(head,
                      new PushRule(var.substring(1), (Transformer) tmp));
            }
            else
            {
              head = add(head, new StringRule("${" + var + "}"));
            }
          }
          else if (var.startsWith("=")
                  && (tmp = defs.get(var.substring(1))) != null)
          {
            if (tmp instanceof Regex)
            {
              head = add(head,
                      new ChangeRule(var.substring(1), (Regex) tmp));
            }
            else if (tmp instanceof Transformer)
            {
              head = add(head,
                      new ChangeRule(var.substring(1), (Transformer) tmp));
            }
            else
            {
              head = add(head, new StringRule("${" + var + "}"));
            }
          }
          else if ((tmp = defs.get(var)) != null)
          {
            if (tmp instanceof ReplaceRule)
            {
              ReplaceRule alt = ((ReplaceRule) tmp).arg(arg);
              if (alt == null)
              {
                alt = ((ReplaceRule) tmp);
              }
              head = add(head, (ReplaceRule) (alt.clone()));
            }
          }
          else
          // can't figure out how to transform this thing...
          {
            head = add(head, new StringRule("${" + var + "}"));
          }
        }
        else if ((var = gv.stringMatched(7 + off)) != null)
        {
          char c = var.charAt(0);
          if (c == 'n')
          {
            head = add(head, new StringRule("\n"));
          }
          else if (c == 't')
          {
            head = add(head, new StringRule("\t"));
          }
          else if (c == 'r')
          {
            head = add(head, new StringRule("\r"));
          }
          else if (c == 'b')
          {
            head = add(head, new StringRule("\r"));
          }
          else if (c == 'a')
          {
            head = add(head, new StringRule("" + (char) 7));
          }
          else if (c == 'e')
          {
            head = add(head, new StringRule("" + (char) 27));
          }
          else if (c == 'f')
          {
            head = add(head, new StringRule("" + (char) 12));
          }
        }
        else if ((var = gv.stringMatched(8 + off)) != null)
        {
          char c = var.charAt(0);
          if (c < Ctrl.cmap.length)
          {
            c = Ctrl.cmap[c];
          }
          head = add(head, new StringRule("" + c));
        }
        else if ((var = gv.stringMatched(9 + off)) != null)
        {
          int d = 16 * getHexDigit(var.charAt(0))
                  + getHexDigit(var.charAt(1));
          head = add(head, new StringRule("" + (char) d));
        }
        mt = gv.matchedTo();
      }
      if (mt <= s.length())
      {
        head = add(head, new StringRule(s.substring(mt)));
      }
      return head;
    } finally
    {
      // Regex.backGs = sav_backGs;
      // Regex.backGto = sav_backGto;
    }
  }

  static Hashtable defs = new Hashtable();

  public static boolean isDefined(String s)
  {
    return defs.get(s) != null;
  }

  public static void define(String s, Regex r)
  {
    defs.put(s, r);
  }

  public static void define(String s, ReplaceRule r)
  {
    defs.put(s, r);
    r.name = s;
  }

  String name = getClass().getName();

  public static void define(String s, Transformer t)
  {
    defs.put(s, t);
  }

  public static void undefine(String s)
  {
    defs.remove(s);
  }

  /**
   * This tells how to convert just the current element (and none of the other
   * items in the linked list) to a String. This method is called by toString()
   * for each item in the linked list.
   */
  public String toString1()
  {
    return "${" + name + "}";
  }

  /** Convert to a String. */
  public final String toString()
  {
    StringBuffer sb = new StringBuffer();
    sb.append(toString1());
    ReplaceRule rr = this.next;
    while (rr != null)
    {
      sb.append(rr.toString1());
      rr = rr.next;
    }
    return sb.toString();
  }

  /**
   * Modified the behavior of a ReplaceRule by supplying an argument. If a
   * ReplaceRule named "foo" is defined and the pattern "s/x/${foo:5}/" is given
   * to Regex.perlCode, then the "foo" the definition of "foo" will be retrieved
   * and arg("5") will be called. If the result is non-null, that is the
   * ReplaceRule that will be used. If the result is null, then the pattern
   * works just as if it were "s/x/${foo}/".
   * 
   * @see com.stevesoft.pat.Validator#arg(java.lang.String)
   */
  public ReplaceRule arg(String s)
  {
    return null;
  }

  static int getHexDigit(char c)
  {
    if (c >= '0' && c <= '9')
    {
      return c - '0';
    }
    if (c >= 'a' && c <= 'f')
    {
      return c - 'a' + 10;
    }
    return c - 'A' + 10;
  }
}
