public class Handler {
  private static Map<Handler.HandlerType, List<Handler>> _definitions;

  public static enum HandlerType {
    TIME, DATE, ANCHOR, ARROW, NARROW
  }

  private HandlerPattern[] _patterns;
  private IHandler _handler;
  private boolean _compatible;

  public Handler(IHandler handler, HandlerPattern... patterns) {
    this(handler, true, patterns);
  }
  
  public Handler(IHandler handler, boolean compatible, HandlerPattern... patterns) {
    _handler = handler;
    _compatible = compatible;
    _patterns = patterns;
  }
  
  public boolean isCompatible(Options options) {
    return !options.isCompatibilityMode() || _compatible;
  }

  public IHandler getHandler() {
    return _handler;
  }

  public boolean match(List<Token> tokens, Map<Handler.HandlerType, List<Handler>> definitions) {
    // System.out.println("Handler.match: " + this);
    int tokenIndex = 0;
    for (HandlerPattern pattern : _patterns) {
      boolean optional = pattern.isOptional();
      if (pattern instanceof TagPattern) {
        boolean match = (tokenIndex < tokens.size() && tokens.get(tokenIndex).getTags(((TagPattern) pattern).getTagClass()).size() > 0);
        // System.out.println("Handler.match:   " + pattern + "=" + match);
        if (!match && !optional) {
          return false;
        }
        if (match) {
          tokenIndex++;
        }
        // next if !match && optional ?
      }
      else if (pattern instanceof HandlerTypePattern) {
        if (optional && tokenIndex == tokens.size()) {
          return true;
        }
        List<Handler> subHandlers = definitions.get(((HandlerTypePattern) pattern).getType());
        for (Handler subHandler : subHandlers) {
          if (subHandler.match(tokens.subList(tokenIndex, tokens.size()), definitions)) {
            return true;
          }
        }
        return false;
      }
    }
    if (tokenIndex != tokens.size()) {
      return false;
    }
    return true;
  }

  @Override
  public String toString() {
    return "[Handler: " + _handler + "]";
  }

  public static synchronized Map<Handler.HandlerType, List<Handler>> definitions() {
    if (_definitions == null) {
      Map<Handler.HandlerType, List<Handler>> definitions = new HashMap<Handler.HandlerType, List<Handler>>();

      List<Handler> timeHandlers = new LinkedList<Handler>();
      timeHandlers.add(new Handler(null, new TagPattern(RepeaterTime.class), new TagPattern(RepeaterDayPortion.class, true)));
      definitions.put(Handler.HandlerType.TIME, timeHandlers);

      List<Handler> dateHandlers = new LinkedList<Handler>();
      dateHandlers.add(new Handler(new RdnRmnSdTTzSyHandler(), new TagPattern(RepeaterDayName.class), new TagPattern(RepeaterMonthName.class), new TagPattern(ScalarDay.class), new TagPattern(RepeaterTime.class), new TagPattern(TimeZone.class), new TagPattern(ScalarYear.class)));
      // DIFF: We add an optional comma to MDY
      dateHandlers.add(new Handler(new RmnSdSyHandler(), new TagPattern(RepeaterMonthName.class), new TagPattern(ScalarDay.class), new TagPattern(SeparatorComma.class, true), new TagPattern(ScalarYear.class)));
      dateHandlers.add(new Handler(new RmnSdSyHandler(), new TagPattern(RepeaterMonthName.class), new TagPattern(ScalarDay.class), new TagPattern(ScalarYear.class), new TagPattern(SeparatorAt.class, true), new HandlerTypePattern(Handler.HandlerType.TIME, true)));
      dateHandlers.add(new Handler(new RmnSdHandler(), new TagPattern(RepeaterMonthName.class), new TagPattern(ScalarDay.class), new TagPattern(SeparatorAt.class, true), new HandlerTypePattern(Handler.HandlerType.TIME, true)));
      dateHandlers.add(new Handler(new RmnOdHandler(), new TagPattern(RepeaterMonthName.class), new TagPattern(OrdinalDay.class), new TagPattern(SeparatorAt.class, true), new HandlerTypePattern(Handler.HandlerType.TIME, true)));
      dateHandlers.add(new Handler(new RmnSyHandler(), new TagPattern(RepeaterMonthName.class), new TagPattern(ScalarYear.class)));
      dateHandlers.add(new Handler(new SdRmnSyHandler(), new TagPattern(ScalarDay.class), new TagPattern(RepeaterMonthName.class), new TagPattern(ScalarYear.class), new TagPattern(SeparatorAt.class, true), new HandlerTypePattern(Handler.HandlerType.TIME, true)));
      dateHandlers.add(new Handler(new SmSdSyHandler(), new TagPattern(ScalarMonth.class), new TagPattern(SeparatorSlashOrDash.class), new TagPattern(ScalarDay.class), new TagPattern(SeparatorSlashOrDash.class), new TagPattern(ScalarYear.class), new TagPattern(SeparatorAt.class, true), new HandlerTypePattern(Handler.HandlerType.TIME, true)));
      dateHandlers.add(new Handler(new SdSmSyHandler(), new TagPattern(ScalarDay.class), new TagPattern(SeparatorSlashOrDash.class), new TagPattern(ScalarMonth.class), new TagPattern(SeparatorSlashOrDash.class), new TagPattern(ScalarYear.class), new TagPattern(SeparatorAt.class, true), new HandlerTypePattern(Handler.HandlerType.TIME, true)));
      dateHandlers.add(new Handler(new SySmSdHandler(), new TagPattern(ScalarYear.class), new TagPattern(SeparatorSlashOrDash.class), new TagPattern(ScalarMonth.class), new TagPattern(SeparatorSlashOrDash.class), new TagPattern(ScalarDay.class), new TagPattern(SeparatorAt.class, true), new HandlerTypePattern(Handler.HandlerType.TIME, true)));
      // DIFF: We make 05/06 interpret as month/day before month/year
      dateHandlers.add(new Handler(new SmSdHandler(), false, new TagPattern(ScalarMonth.class), new TagPattern(SeparatorSlashOrDash.class), new TagPattern(ScalarDay.class)));
      dateHandlers.add(new Handler(new SmSyHandler(), new TagPattern(ScalarMonth.class), new TagPattern(SeparatorSlashOrDash.class), new TagPattern(ScalarYear.class)));
      definitions.put(Handler.HandlerType.DATE, dateHandlers);

      // tonight at 7pm
      List<Handler> anchorHandlers = new LinkedList<Handler>();
      anchorHandlers.add(new Handler(new RHandler(), new TagPattern(Grabber.class, true), new TagPattern(Repeater.class), new TagPattern(SeparatorAt.class, true), new TagPattern(Repeater.class, true), new TagPattern(Repeater.class, true)));
      anchorHandlers.add(new Handler(new RHandler(), new TagPattern(Grabber.class, true), new TagPattern(Repeater.class), new TagPattern(Repeater.class), new TagPattern(SeparatorAt.class, true), new TagPattern(Repeater.class, true), new TagPattern(Repeater.class, true)));
      anchorHandlers.add(new Handler(new RGRHandler(), new TagPattern(Repeater.class), new TagPattern(Grabber.class), new TagPattern(Repeater.class)));
      definitions.put(Handler.HandlerType.ANCHOR, anchorHandlers);

      // 3 weeks from now, in 2 months
      List<Handler> arrowHandlers = new LinkedList<Handler>();
      arrowHandlers.add(new Handler(new SRPHandler(), new TagPattern(Scalar.class), new TagPattern(Repeater.class), new TagPattern(Pointer.class)));
      arrowHandlers.add(new Handler(new PSRHandler(), new TagPattern(Pointer.class), new TagPattern(Scalar.class), new TagPattern(Repeater.class)));
      arrowHandlers.add(new Handler(new SRPAHandler(), new TagPattern(Scalar.class), new TagPattern(Repeater.class), new TagPattern(Pointer.class), new HandlerTypePattern(Handler.HandlerType.ANCHOR)));
      definitions.put(Handler.HandlerType.ARROW, arrowHandlers);

      // 3rd week in march
      List<Handler> narrowHandlers = new LinkedList<Handler>();
      narrowHandlers.add(new Handler(new ORSRHandler(), new TagPattern(Ordinal.class), new TagPattern(Repeater.class), new TagPattern(SeparatorIn.class), new TagPattern(Repeater.class)));
      narrowHandlers.add(new Handler(new ORGRHandler(), new TagPattern(Ordinal.class), new TagPattern(Repeater.class), new TagPattern(Grabber.class), new TagPattern(Repeater.class)));
      definitions.put(Handler.HandlerType.NARROW, narrowHandlers);
      _definitions = definitions;
    }
    return _definitions;
  }

  public static Span tokensToSpan(List<Token> tokens, Options options) {
    if (options.isDebug()) {
      System.out.println("Chronic.tokensToSpan: " + tokens);
    }

    // maybe it's a specific date
    Map<Handler.HandlerType, List<Handler>> definitions = definitions();
    for (Handler handler : definitions.get(Handler.HandlerType.DATE)) {
      if (handler.isCompatible(options) && handler.match(tokens, definitions)) {
        if (options.isDebug()) {
          System.out.println("Chronic.tokensToSpan: date");
        }
        List<Token> goodTokens = new LinkedList<Token>();
        for (Token token : tokens) {
          if (token.getTag(Separator.class) == null) {
            goodTokens.add(token);
          }
        }
        return handler.getHandler().handle(goodTokens, options);
      }
    }

    // I guess it's not a specific date, maybe it's just an anchor
    for (Handler handler : definitions.get(Handler.HandlerType.ANCHOR)) {
      if (handler.isCompatible(options) && handler.match(tokens, definitions)) {
        if (options.isDebug()) {
          System.out.println("Chronic.tokensToSpan: anchor");
        }
        List<Token> goodTokens = new LinkedList<Token>();
        for (Token token : tokens) {
          if (token.getTag(Separator.class) == null) {
            goodTokens.add(token);
          }
        }
        return handler.getHandler().handle(goodTokens, options);
      }
    }

    // not an anchor, perhaps it's an arrow
    for (Handler handler : definitions.get(Handler.HandlerType.ARROW)) {
      if (handler.isCompatible(options) && handler.match(tokens, definitions)) {
        if (options.isDebug()) {
          System.out.println("Chronic.tokensToSpan: arrow");
        }
        List<Token> goodTokens = new LinkedList<Token>();
        for (Token token : tokens) {
          if (token.getTag(SeparatorAt.class) == null && token.getTag(SeparatorSlashOrDash.class) == null && token.getTag(SeparatorComma.class) == null) {
            goodTokens.add(token);
          }
        }
        return handler.getHandler().handle(goodTokens, options);
      }
    }

    // not an arrow, let's hope it's a narrow
    for (Handler handler : definitions.get(Handler.HandlerType.NARROW)) {
      if (handler.isCompatible(options) && handler.match(tokens, definitions)) {
        if (options.isDebug()) {
          System.out.println("Chronic.tokensToSpan: narrow");
        }
        //List<Token> goodTokens = new LinkedList<Token>();
        //for (Token token : tokens) {
        //if (token.getTag(Separator.class) == null) {
        //  goodTokens.add(token);
        //}
        //}
        return handler.getHandler().handle(tokens, options);
      }
    }

    // I guess you're out of luck!
    if (options.isDebug()) {
      System.out.println("Chronic.tokensToSpan: none");
    }
    return null;
  }

  public static List<Repeater<?>> getRepeaters(List<Token> tokens) {
    List<Repeater<?>> repeaters = new LinkedList<Repeater<?>>();
    for (Token token : tokens) {
      Repeater<?> tag = token.getTag(Repeater.class);
      if (tag != null) {
        repeaters.add(tag);
      }
    }
    Collections.sort(repeaters);
    Collections.reverse(repeaters);
    return repeaters;
  }

  public static Span getAnchor(List<Token> tokens, Options options) {
    Grabber grabber = new Grabber(Grabber.Relative.THIS);
    Pointer.PointerType pointer = Pointer.PointerType.FUTURE;

    List<Repeater<?>> repeaters = getRepeaters(tokens);
    for (int i = 0; i < repeaters.size(); i++) {
      tokens.remove(tokens.size() - 1);
    }

    if (!tokens.isEmpty() && tokens.get(0).getTag(Grabber.class) != null) {
      grabber = tokens.get(0).getTag(Grabber.class);
      tokens.remove(tokens.size() - 1);
    }

    Repeater<?> head = repeaters.remove(0);
    head.setStart((Calendar) options.getNow().clone());

    Span outerSpan;
    Grabber.Relative grabberType = grabber.getType();
    if (grabberType == Grabber.Relative.LAST) {
      outerSpan = head.nextSpan(Pointer.PointerType.PAST);
    }
    else if (grabberType == Grabber.Relative.THIS) {
      if (repeaters.size() > 0) {
        outerSpan = head.thisSpan(PointerType.NONE);
      }
      else {
        outerSpan = head.thisSpan(options.getContext());
      }
    }
    else if (grabberType == Grabber.Relative.NEXT) {
      outerSpan = head.nextSpan(Pointer.PointerType.FUTURE);
    }
    else {
      throw new IllegalArgumentException("Invalid grabber type " + grabberType + ".");
    }

    if (options.isDebug()) {
      System.out.println("Chronic.getAnchor: outerSpan = " + outerSpan + "; repeaters = " + repeaters);
    }

    Span anchor = findWithin(repeaters, outerSpan, pointer, options);
    return anchor;
  }

  public static Span dayOrTime(Calendar dayStart, List<Token> timeTokens, Options options) {
    Span outerSpan = new Span(dayStart, Time.cloneAndAdd(dayStart, Calendar.DAY_OF_MONTH, 1));
    if (!timeTokens.isEmpty()) {

//      /** SUPER HACK MODE FOR TIMES **/
//      Tag<?> dayPortionTag = null;
//      Tag<?> timeTag = null;
//      for (Token token : timeTokens) {
//        Tag<?> tempDayPortionTag = token.getTag(RepeaterDayPortion.class);
//        if (tempDayPortionTag != null) {
//          dayPortionTag = tempDayPortionTag;
//        }
//
//        Tag<?> tempTimeTag = token.getTag(RepeaterTime.class);
//        if (tempTimeTag != null) {
//          timeTag = tempTimeTag;
//        }
//      }
//      
//      if (timeTag != null && dayPortionTag != null) {
//        Tick tick = (Tick)timeTag.getType();
//        RepeaterDayPortion.DayPortion dayPortion = (RepeaterDayPortion.DayPortion)dayPortionTag.getType();
//        if (tick.intValue() <= (RepeaterDay.DAY_SECONDS / 2)) {
//          if (dayPortion == RepeaterDayPortion.DayPortion.PM) {
//            if (tick.intValue() == (12 * 60 * 60)) {
//              Calendar exactTime = Time.cloneAndAdd(dayStart, Calendar.SECOND, tick.intValue());
//              return new Span(exactTime, exactTime);
//            }
//            Calendar exactTime = Time.cloneAndAdd(dayStart, Calendar.SECOND, tick.intValue() + RepeaterDay.DAY_SECONDS / 2);
//            return new Span(exactTime, exactTime);
//          }
//          else if (dayPortion == RepeaterDayPortion.DayPortion.AM) {
//            if (tick.intValue() == (12 * 60 * 60)) {
//              Calendar exactTime = dayStart;
//              return new Span(exactTime, exactTime);
//            }
//            Calendar exactTime = Time.cloneAndAdd(dayStart, Calendar.SECOND, tick.intValue());
//            return new Span(exactTime, exactTime);
//          }
//        }
//      }
//      /** SUPER HACK MODE FOR TIMES **/

      options.setNow(outerSpan.getBeginCalendar());
      Span time = getAnchor(dealiasAndDisambiguateTimes(timeTokens, options), options);
      return time;
    }
    return outerSpan;
  }

  /**
   * Recursively finds repeaters within other repeaters.
   * Returns a Span representing the innermost time span
   * or nil if no repeater union could be found
   */
  public static Span findWithin(List<Repeater<?>> tags, Span span, Pointer.PointerType pointer, Options options) {
    if (options.isDebug()) {
      System.out.println("Chronic.findWithin: " + tags + " in " + span);
    }
    if (tags.isEmpty()) {
      return span;
    }
    Repeater<?> head = tags.get(0);
    List<Repeater<?>> rest = (tags.size() > 1) ? tags.subList(1, tags.size()) : new LinkedList<Repeater<?>>();
    head.setStart((pointer == Pointer.PointerType.FUTURE) ? span.getBeginCalendar() : span.getEndCalendar());
    Span h = head.thisSpan(PointerType.NONE);

    if (span.contains(h.getBegin()) || span.contains(h.getEnd())) {
      return findWithin(rest, h, pointer, options);
    }
    return null;
  }

  @SuppressWarnings("unchecked")
  public static List<Token> dealiasAndDisambiguateTimes(List<Token> tokens, Options options) {
    // handle aliases of am/pm
    // 5:00 in the morning => 5:00 am
    // 7:00 in the evening => 7:00 pm

    int dayPortionIndex = -1;
    int tokenSize = tokens.size();
    for (int i = 0; dayPortionIndex == -1 && i < tokenSize; i++) {
      Token t = tokens.get(i);
      if (t.getTag(RepeaterDayPortion.class) != null) {
        dayPortionIndex = i;
      }
    }

    int timeIndex = -1;
    for (int i = 0; timeIndex == -1 && i < tokenSize; i++) {
      Token t = tokens.get(i);
      if (t.getTag(RepeaterTime.class) != null) {
        timeIndex = i;
      }
    }

    if (dayPortionIndex != -1 && timeIndex != -1) {
      Token t1 = tokens.get(dayPortionIndex);
      Tag<RepeaterDayPortion<?>> t1Tag = t1.getTag(RepeaterDayPortion.class);

      Object t1TagType = t1Tag.getType();
      if (RepeaterDayPortion.DayPortion.MORNING.equals(t1TagType)) {
        if (options.isDebug()) {
          System.out.println("Chronic.dealiasAndDisambiguateTimes: morning->am");
        }
        t1.untag(RepeaterDayPortion.class);
        t1.tag(new EnumRepeaterDayPortion(RepeaterDayPortion.DayPortion.AM));
      }
      else if (RepeaterDayPortion.DayPortion.AFTERNOON.equals(t1TagType) || RepeaterDayPortion.DayPortion.EVENING.equals(t1TagType) || RepeaterDayPortion.DayPortion.NIGHT.equals(t1TagType)) {
        if (options.isDebug()) {
          System.out.println("Chronic.dealiasAndDisambiguateTimes: " + t1TagType + "->pm");
        }
        t1.untag(RepeaterDayPortion.class);
        t1.tag(new EnumRepeaterDayPortion(RepeaterDayPortion.DayPortion.PM));
      }
    }

//    int tokenSize = tokens.size();
//    for (int i = 0; i < tokenSize; i++) {
//      Token t0 = tokens.get(i);
//      if (i < tokenSize - 1) {
//        Token t1 = tokens.get(i + 1);
//        RepeaterDayPortion<?> t1Tag = t1.getTag(RepeaterDayPortion.class);
//        if (t1Tag != null && t0.getTag(RepeaterTime.class) != null) {
//          if (t1Tag.getType() == RepeaterDayPortion.DayPortion.MORNING) {
//            t1.untag(RepeaterDayPortion.class);
//            t1.tag(new EnumRepeaterDayPortion(RepeaterDayPortion.DayPortion.AM));
//          }
//          else if (t1Tag.getType() == RepeaterDayPortion.DayPortion.AFTERNOON || t1Tag.getType() == RepeaterDayPortion.DayPortion.EVENING || t1Tag.getType() == RepeaterDayPortion.DayPortion.NIGHT) {
//            t1.untag(RepeaterDayPortion.class);
//            t1.tag(new EnumRepeaterDayPortion(RepeaterDayPortion.DayPortion.PM));
//          }
//        }
//      }
//    }

    // handle ambiguous times if :ambiguous_time_range is specified
    if (options.getAmbiguousTimeRange() != 0) {
      List<Token> ttokens = new LinkedList<Token>();
      for (int i = 0; i < tokenSize; i++) {
        Token t0 = tokens.get(i);
        ttokens.add(t0);
        Token t1 = null;
        if (i < tokenSize - 1) {
          t1 = tokens.get(i + 1);
        }
        if (t0.getTag(RepeaterTime.class) != null && t0.getTag(RepeaterTime.class).getType().isAmbiguous() && (t1 == null || t1.getTag(RepeaterDayPortion.class) == null)) {
          Token distoken = new Token("disambiguator");
          distoken.tag(new IntegerRepeaterDayPortion(Integer.valueOf(options.getAmbiguousTimeRange())));
          ttokens.add(distoken);
        }
      }
      tokens = ttokens;
    }

    if (options.isDebug()) {
      System.out.println("Chronic.dealiasAndDisambiguateTimes: " + tokens);
    }

    return tokens;
  }
} public class Numerizer {
  protected static class DirectNum {
    private Pattern _name;
    private String _number;
    
    public DirectNum(String name, String number) {
      _name = Pattern.compile(name, Pattern.CASE_INSENSITIVE);
      _number = number;
    }
    
    public Pattern getName() {
      return _name;
    }
    
    public String getNumber() {
      return _number;
    }
  }
  
  protected static class Prefix {
    private Pattern _name;
    private long _number;
    
    public Prefix(Pattern name, long number) {
      _name = name;
      _number = number;
    }
    
    public Pattern getName() {
      return _name;
    }
    
    public long getNumber() {
      return _number;
    }
  }

  protected static class TenPrefix extends Prefix {
    public TenPrefix(String name, long number) {
      super(Pattern.compile("(?:" + name + ")( *\\d(?=\\D|$))*", Pattern.CASE_INSENSITIVE), number);
    }
  }

  protected static class BigPrefix extends Prefix {
    public BigPrefix(String name, long number) {
      super(Pattern.compile("(\\d*) *" + name, Pattern.CASE_INSENSITIVE), number);
    }
  }
  
  protected static DirectNum[] DIRECT_NUMS;
  protected static TenPrefix[] TEN_PREFIXES;
  protected static BigPrefix[] BIG_PREFIXES;
   
  static {
    List<DirectNum> directNums = new LinkedList<DirectNum>();
    directNums.add(new DirectNum("eleven", "11"));
    directNums.add(new DirectNum("twelve", "12"));
    directNums.add(new DirectNum("thirteen", "13"));
    directNums.add(new DirectNum("fourteen", "14"));
    directNums.add(new DirectNum("fifteen", "15"));
    directNums.add(new DirectNum("sixteen", "16"));
    directNums.add(new DirectNum("seventeen", "17"));
    directNums.add(new DirectNum("eighteen", "18"));
    directNums.add(new DirectNum("nineteen", "19"));
    directNums.add(new DirectNum("ninteen", "19")); // Common mis-spelling
    directNums.add(new DirectNum("zero", "0"));
    directNums.add(new DirectNum("one", "1"));
    directNums.add(new DirectNum("two", "2"));
    directNums.add(new DirectNum("three", "3"));
    directNums.add(new DirectNum("four(\\W|$)", "4$1")); // The weird regex is so that it matches four but not fourty
    directNums.add(new DirectNum("five", "5"));
    directNums.add(new DirectNum("six(\\W|$)", "6$1"));
    directNums.add(new DirectNum("seven(\\W|$)", "7$1"));
    directNums.add(new DirectNum("eight(\\W|$)", "8$1"));
    directNums.add(new DirectNum("nine(\\W|$)", "9$1"));
    directNums.add(new DirectNum("ten", "10"));
    directNums.add(new DirectNum("\\ba\\b", "1"));
    Numerizer.DIRECT_NUMS = directNums.toArray(new DirectNum[directNums.size()]);
    
    List<TenPrefix> tenPrefixes = new LinkedList<TenPrefix>();
    tenPrefixes.add(new TenPrefix("twenty", 20));
    tenPrefixes.add(new TenPrefix("thirty", 30));
    tenPrefixes.add(new TenPrefix("fourty", 40));
    tenPrefixes.add(new TenPrefix("fifty", 50));
    tenPrefixes.add(new TenPrefix("sixty", 60));
    tenPrefixes.add(new TenPrefix("seventy", 70));
    tenPrefixes.add(new TenPrefix("eighty", 80));
    tenPrefixes.add(new TenPrefix("ninety", 90));
    tenPrefixes.add(new TenPrefix("ninty", 90)); // Common mis-spelling
    Numerizer.TEN_PREFIXES = tenPrefixes.toArray(new TenPrefix[tenPrefixes.size()]);
    
    List<BigPrefix> bigPrefixes = new LinkedList<BigPrefix>();
    bigPrefixes.add(new BigPrefix("hundred", 100L));
    bigPrefixes.add(new BigPrefix("thousand", 1000L));
    bigPrefixes.add(new BigPrefix("million", 1000000L));
    bigPrefixes.add(new BigPrefix("billion", 1000000000L));
    bigPrefixes.add(new BigPrefix("trillion", 1000000000000L));
    Numerizer.BIG_PREFIXES = bigPrefixes.toArray(new BigPrefix[bigPrefixes.size()]);
  }

  private static final Pattern DEHYPHENATOR = Pattern.compile(" +|(\\D)-(\\D)");
  private static final Pattern DEHALFER = Pattern.compile("a half", Pattern.CASE_INSENSITIVE);
  private static final Pattern DEHAALFER = Pattern.compile("(\\d+)(?: | and |-)*haAlf", Pattern.CASE_INSENSITIVE);
  private static final Pattern ANDITION_PATTERN = Pattern.compile("(\\d+)( | and )(\\d+)(?=\\W|$)");
  
  // FIXES
  //string.gsub!(/ +|([^\d])-([^d])/, '\1 \2') # will mutilate hyphenated-words but shouldn't matter for date extraction
  //string.gsub!(/ +|([^\d])-([^\\d])/, '\1 \2') # will mutilate hyphenated-words but shouldn't matter for date extraction
  
  public static String numerize(String str) {
    String numerizedStr = str;
    
    // preprocess
    numerizedStr = Numerizer.DEHYPHENATOR.matcher(numerizedStr).replaceAll("$1 $2"); // will mutilate hyphenated-words but shouldn't matter for date extraction
    numerizedStr = Numerizer.DEHALFER.matcher(numerizedStr).replaceAll("haAlf"); // take the 'a' out so it doesn't turn into a 1, save the half for the end
    
    // easy/direct replacements
    for (DirectNum dn : Numerizer.DIRECT_NUMS) {
      numerizedStr = dn.getName().matcher(numerizedStr).replaceAll(dn.getNumber());
    }
    
    // ten, twenty, etc.
    for (Prefix tp : Numerizer.TEN_PREFIXES) {
      Matcher matcher = tp.getName().matcher(numerizedStr);
      if (matcher.find()) {
        StringBuffer matcherBuffer = new StringBuffer();
        do {
          if (matcher.group(1) == null) {
            matcher.appendReplacement(matcherBuffer, String.valueOf(tp.getNumber()));
          }
          else {
            matcher.appendReplacement(matcherBuffer, String.valueOf(tp.getNumber() + Long.parseLong(matcher.group(1).trim())));
          }
        } while (matcher.find());
        matcher.appendTail(matcherBuffer);
        numerizedStr = matcherBuffer.toString();
      }
    }
    
    // hundreds, thousands, millions, etc.
    for (Prefix bp : Numerizer.BIG_PREFIXES) {
      Matcher matcher = bp.getName().matcher(numerizedStr);
      if (matcher.find()) {
        StringBuffer matcherBuffer = new StringBuffer();
        do {
          if (matcher.group(1) == null) {
            matcher.appendReplacement(matcherBuffer, String.valueOf(bp.getNumber()));
          }
          else {
            matcher.appendReplacement(matcherBuffer, String.valueOf(bp.getNumber() * Long.parseLong(matcher.group(1).trim())));
          }
        } while (matcher.find());
        matcher.appendTail(matcherBuffer);
        numerizedStr = matcherBuffer.toString();
        numerizedStr = Numerizer.andition(numerizedStr);
        // combine_numbers(string) // Should to be more efficient way to do this
      }
    }

    // fractional addition
    // I'm not combining this with the previous block as using float addition complicates the strings
    // (with extraneous .0's and such )
    Matcher matcher = Numerizer.DEHAALFER.matcher(numerizedStr);
    if (matcher.find()) {
      StringBuffer matcherBuffer = new StringBuffer();
      do {
        matcher.appendReplacement(matcherBuffer, String.valueOf(Float.parseFloat(matcher.group(1).trim()) + 0.5f));
      } while (matcher.find());
      matcher.appendTail(matcherBuffer);
      numerizedStr = matcherBuffer.toString();
    }
    //string.gsub!(/(\d+)(?: | and |-)*haAlf/i) { ($1.to_f + 0.5).to_s }

    return numerizedStr;
  }

  public static String andition(String str) {
    StringBuffer anditionStr = new StringBuffer(str);
    Matcher matcher = Numerizer.ANDITION_PATTERN.matcher(anditionStr);
    while (matcher.find()) {
      if (matcher.group(2).equalsIgnoreCase(" and ") || matcher.group(1).length() > matcher.group(3).length()) {
        anditionStr.replace(matcher.start(), matcher.end(), String.valueOf(Integer.parseInt(matcher.group(1).trim()) + Integer.parseInt(matcher.group(3).trim())));
        matcher = Numerizer.ANDITION_PATTERN.matcher(anditionStr);
      }
    }
    return anditionStr.toString(); 
  }
} public abstract class Repeater<T> extends Tag<T> implements Comparable<Repeater<?>> {
  public Repeater(T type) {
    super(type);
  }

  public static List<Token> scan(List<Token> tokens) {
    return Repeater.scan(tokens, new Options());
  }

  public static List<Token> scan(List<Token> tokens, Options options) {
    for (Token token : tokens) {
      Tag<?> t;
      t = RepeaterMonthName.scan(token);
      if (t != null) {
        token.tag(t);
      }
      t = RepeaterDayName.scan(token);
      if (t != null) {
        token.tag(t);
      }
      t = RepeaterDayPortion.scan(token);
      if (t != null) {
        token.tag(t);
      }
      t = RepeaterTime.scan(token, tokens, options);
      if (t != null) {
        token.tag(t);
      }
      t = RepeaterUnit.scan(token);
      if (t != null) {
        token.tag(t);
      }
    }
    return tokens;
  }

  public int compareTo(Repeater<?> other) {
    return Integer.valueOf(getWidth()).compareTo(Integer.valueOf(other.getWidth()));
  }

  /**
   * returns the width (in seconds or months) of this repeatable.
   */
  public abstract int getWidth();

  /** 
   * returns the next occurance of this repeatable.
   */
  public Span nextSpan(Pointer.PointerType pointer) {
    if (getNow() == null) {
      throw new IllegalStateException("Start point must be set before calling #next");
    }
    return _nextSpan(pointer);
  }

  protected abstract Span _nextSpan(Pointer.PointerType pointer);

  public Span thisSpan(Pointer.PointerType pointer) {
    if (getNow() == null) {
      throw new IllegalStateException("Start point must be set before calling #this");
    }
    return _thisSpan(pointer);
  }

  protected abstract Span _thisSpan(Pointer.PointerType pointer);

  public abstract Span getOffset(Span span, int amount, Pointer.PointerType pointer);

  @Override
  public String toString() {
    return "repeater";
  }
} public class Grabber extends Tag<Grabber.Relative> {
  private static final Pattern THIS_PATTERN = Pattern.compile("this");
  private static final Pattern NEXT_PATTERN = Pattern.compile("next");
  private static final Pattern LAST_PATTERN = Pattern.compile("last");

  public static enum Relative {
    LAST, NEXT, THIS
  }

  public Grabber(Grabber.Relative type) {
    super(type);
  }

  public static List<Token> scan(List<Token> tokens, Options options) {
    for (Token token : tokens) {
      Grabber t = Grabber.scanForAll(token, options);
      if (t != null) {
        token.tag(t);
      }
    }
    return tokens;
  }

  public static Grabber scanForAll(Token token, Options options) {
    Map<Pattern, Grabber.Relative> scanner = new HashMap<Pattern, Grabber.Relative>();
    scanner.put(Grabber.LAST_PATTERN, Grabber.Relative.LAST);
    scanner.put(Grabber.NEXT_PATTERN, Grabber.Relative.NEXT);
    scanner.put(Grabber.THIS_PATTERN, Grabber.Relative.THIS);
    for (Pattern scannerItem : scanner.keySet()) {
      if (scannerItem.matcher(token.getWord()).matches()) {
        return new Grabber(scanner.get(scannerItem));
      }
    }
    return null;
  }

  @Override
  public String toString() {
    return "grabber-" + getType();
  }
} public class Ordinal extends Tag<Integer> {
  public static Pattern ORDINAL_PATTERN = Pattern.compile("^(\\d*)(st|nd|rd|th)$");

  public Ordinal(Integer type) {
    super(type);
  }

  public static List<Token> scan(List<Token> tokens, Options options) {
    for (Token token : tokens) {
      Ordinal t;
      t = Ordinal.scan(token, options);
      if (t != null) {
        token.tag(t);
      }
      t = OrdinalDay.scan(token);
      if (t != null) {
        token.tag(t);
      }
    }
    return tokens;
  }

  public static Ordinal scan(Token token, Options options) {
    Matcher ordinalMatcher = ORDINAL_PATTERN.matcher(token.getWord());
    if (ordinalMatcher.find()) {
      return new Ordinal(Integer.valueOf(ordinalMatcher.group(1)));
    }
    return null;
  }

  @Override
  public String toString() {
    return "ordinal";
  }
} public class Pointer extends Tag<Pointer.PointerType> {
  private static final Pattern IN_PATTERN = Pattern.compile("\\bin\\b");
  private static final Pattern FUTURE_PATTERN = Pattern.compile("\\bfuture\\b");
  private static final Pattern PAST_PATTERN = Pattern.compile("\\bpast\\b");

  public enum PointerType {
    PAST, FUTURE, NONE
  }
  
  public Pointer(Pointer.PointerType type) {
    super(type);
  }

  public static List<Token> scan(List<Token> tokens, Options options) {
    for (Token token : tokens) {
      Pointer t = Pointer.scanForAll(token, options);
      if (t != null) {
        token.tag(t);
      }
    }
    return tokens;
  }

  public static Pointer scanForAll(Token token, Options options) {
    Map<Pattern, Pointer.PointerType> scanner = new HashMap<Pattern, Pointer.PointerType>();
    scanner.put(Pointer.PAST_PATTERN, Pointer.PointerType.PAST);
    scanner.put(Pointer.FUTURE_PATTERN, Pointer.PointerType.FUTURE);
    scanner.put(Pointer.IN_PATTERN, Pointer.PointerType.FUTURE);
    for (Pattern scannerItem : scanner.keySet()) {
      if (scannerItem.matcher(token.getWord()).matches()) { 
        return new Pointer(scanner.get(scannerItem));
      }
    }
    return null;
  }

  @Override
  public String toString() {
    return "pointer-" + getType();
  }
} public class Scalar extends Tag<Integer> {
  private static final Pattern SCALAR_PATTERN = Pattern.compile("^\\d*$");
  public static Set<String> TIMES = new HashSet<String>();

  static {
    Scalar.TIMES.add("am");
    Scalar.TIMES.add("pm");
    Scalar.TIMES.add("morning");
    Scalar.TIMES.add("afternoon");
    Scalar.TIMES.add("evening");
    Scalar.TIMES.add("night");
  }

  public Scalar(Integer type) {
    super(type);
  }

  public static List<Token> scan(List<Token> tokens, Options options) {
    for (int i = 0; i < tokens.size(); i++) {
      Token token = tokens.get(i);
      Token postToken = null;
      if (i < tokens.size() - 1) {
        postToken = tokens.get(i + 1);
      }
      Scalar t;
      t = Scalar.scan(token, postToken, options);
      if (t != null) {
        token.tag(t);
      }
      t = ScalarDay.scan(token, postToken, options);
      if (t != null) {
        token.tag(t);
      }
      t = ScalarMonth.scan(token, postToken, options);
      if (t != null) {
        token.tag(t);
      }
      t = ScalarYear.scan(token, postToken, options);
      if (t != null) {
        token.tag(t);
      }
    }
    return tokens;
  }

  public static Scalar scan(Token token, Token postToken, Options options) {
    if (Scalar.SCALAR_PATTERN.matcher(token.getWord()).matches()) {
      if (token.getWord() != null && token.getWord().length() > 0 && !(postToken != null && Scalar.TIMES.contains(postToken.getWord()))) {
        return new Scalar(Integer.valueOf(token.getWord()));
      }
    }
    else {
      Integer intStrValue = StringUtils.integerValue(token.getWord());
      if (intStrValue != null) {
        return new Scalar(intStrValue);
      }
    }
    return null;
  }

  @Override
  public String toString() {
    return "scalar";
  }
} public class Separator extends Tag<Separator.SeparatorType> {
  public static enum SeparatorType {
    COMMA, DASH, SLASH, AT, NEWLINE, IN
  }

  public Separator(Separator.SeparatorType type) {
    super(type);
  }

  public static List<Token> scan(List<Token> tokens, Options options) {
    for (Token token : tokens) {
      Separator t;
      t = SeparatorComma.scan(token, options);
      if (t != null) {
        token.tag(t);
      }
      t = SeparatorSlashOrDash.scan(token, options);
      if (t != null) {
        token.tag(t);
      }
      t = SeparatorAt.scan(token, options);
      if (t != null) {
        token.tag(t);
      }
      t = SeparatorIn.scan(token, options);
      if (t != null) {
        token.tag(t);
      }
    }
    return tokens;
  }

  @Override
  public String toString() {
    return "separator";
  }
} public class TimeZone extends Tag<Object> {
  private static final Pattern TIMEZONE_PATTERN = Pattern.compile("[pmce][ds]t");
  public static final Object TZ = new Object();
  
  public TimeZone() {
    super(null);
  }

  public static List<Token> scan(List<Token> tokens, Options options) {
    for (Token token : tokens) {
      TimeZone t = TimeZone.scanForAll(token, options);
      if (t != null) {
        token.tag(t);
      }
    }
    return tokens;
  }

  public static TimeZone scanForAll(Token token, Options options) {
    Map<Pattern, Object> scanner = new HashMap<Pattern, Object>();
    scanner.put(TimeZone.TIMEZONE_PATTERN, null);
    for (Pattern scannerItem : scanner.keySet()) {
      if (scannerItem.matcher(token.getWord()).matches()) {
        return new TimeZone();
      }
    }
    return null;
  }

  @Override
  public String toString() {
    return "timezone";
  }
} public class Span extends Range {

  public Span(Calendar begin, int field, long amount) {
    this(begin, Time.cloneAndAdd(begin, field, amount));
  }

  public Span(Calendar begin, Calendar end) {
    this(begin.getTimeInMillis() / 1000, end.getTimeInMillis() / 1000);
  }

  public Span(long begin, long end) {
    super(begin, end);
  }

  public Calendar getBeginCalendar() {
    Calendar cal = Calendar.getInstance();
    cal.setTimeInMillis(getBegin() * 1000);
    return cal;
  }

  public Calendar getEndCalendar() {
    Calendar cal = Calendar.getInstance();
    cal.setTimeInMillis(getEnd() * 1000);
    return cal;
  }

  /**
   * Add a number of seconds to this span, returning the 
   * resulting Span
   */
  public Span add(long seconds) {
    return new Span(getBegin() + seconds, getEnd() + seconds);
  }

  /**
   * Subtract a number of seconds to this span, returning the 
   * resulting Span
   */
  public Span subtract(long seconds) {
    return add(-seconds);
  }

  @Override
  public String toString() {
    return "(" + DateFormat.getDateTimeInstance().format(getBeginCalendar().getTime()) + ".." + DateFormat.getDateTimeInstance().format(getEndCalendar().getTime()) + ")";
  }
} public class Token {
  private String _word;
  private List<Tag<?>> _tags;

  public Token(String word) {
    _word = word;
    _tags = new LinkedList<Tag<?>>();
  }

  public String getWord() {
    return _word;
  }

  /**
   * Tag this token with the specified tag
   */
  public void tag(Tag<?> newTag) {
    _tags.add(newTag);
  }

  /**
   * Remove all tags of the given class
   */
  public void untag(Class<?> tagClass) {
    Iterator<Tag<?>> tagIter = _tags.iterator();
    while (tagIter.hasNext()) {
      Tag<?> tag = tagIter.next();
      if (tagClass.isInstance(tag)) {
        tagIter.remove();
      }
    }
  }

  /**
   * Return true if this token has any tags
   */
  public boolean isTagged() {
    return !_tags.isEmpty();
  }

  /**
   * Return the Tag that matches the given class
   */
  @SuppressWarnings("unchecked")
  public <T extends Tag> T getTag(Class<T> tagClass) {
    List<T> matches = getTags(tagClass);
    T matchingTag = null;
    if (matches.size() > 0) {
      matchingTag = matches.get(0);
    }
//    if (matches.size() >= 2) {
//      throw new IllegalStateException("Multiple identical tags found (" + matches + ")");
//    }
//    else if (matches.size() == 1) {
//      matchingTag = matches.get(0);
//    }
    return matchingTag;
  }

  public List<Tag<?>> getTags() {
    return _tags;
  }
  
  /**
   * Return the Tag that matches the given class
   */
  @SuppressWarnings("unchecked")
  public <T extends Tag<?>> List<T> getTags(Class<T> tagClass) {
    List<T> matches = new LinkedList<T>();
    Iterator<Tag<?>> tagIter = _tags.iterator();
    while (tagIter.hasNext()) {
      Tag<?> tag = tagIter.next();
      if (tagClass.isInstance(tag)) {
        matches.add((T)tag);
      }
    }
    return matches;
  }

  @Override
  public String toString() {
    return _word + " " + _tags;
  }
}
import java.util.LinkedList;
import java.util.List;
import com.mdimension.jchronic.handlers.Handler;
import com.mdimension.jchronic.numerizer.Numerizer;
import com.mdimension.jchronic.repeaters.Repeater;
import com.mdimension.jchronic.tags.Grabber;
import com.mdimension.jchronic.tags.Ordinal;
import com.mdimension.jchronic.tags.Pointer;
import com.mdimension.jchronic.tags.Scalar;
import com.mdimension.jchronic.tags.Separator;
import com.mdimension.jchronic.tags.TimeZone;
import com.mdimension.jchronic.utils.Span;
import com.mdimension.jchronic.utils.Token;
package com.mdimension.jchronic;



public class Chronic {
  public static final String VERSION = "0.2.3";

  private Chronic() {
    // DO NOTHING
  }

  public static Span parse(String text) {
    return Chronic.parse(text, new Options());
  }

  /**
   * Parses a string containing a natural language date or time. If the parser
   * can find a date or time, either a Time or Chronic::Span will be returned 
   * (depending on the value of <tt>:guess</tt>). If no date or time can be found,
   * +nil+ will be returned.
   *
   * Options are:
   *
   * [<tt>:context</tt>]
   *     <tt>:past</tt> or <tt>:future</tt> (defaults to <tt>:future</tt>)
   *
   *     If your string represents a birthday, you can set <tt>:context</tt> to <tt>:past</tt> 
   *     and if an ambiguous string is given, it will assume it is in the 
   *     past. Specify <tt>:future</tt> or omit to set a future context.
   *
   * [<tt>:now</tt>]
   *     Time (defaults to Time.now)
   *
   *     By setting <tt>:now</tt> to a Time, all computations will be based off
   *     of that time instead of Time.now
   *
   * [<tt>:guess</tt>]
   *     +true+ or +false+ (defaults to +true+)
   *
   *     By default, the parser will guess a single point in time for the
   *     given date or time. If you'd rather have the entire time span returned,
   *     set <tt>:guess</tt> to +false+ and a Chronic::Span will be returned.
   *     
   * [<tt>:ambiguous_time_range</tt>]
   *     Integer or <tt>:none</tt> (defaults to <tt>6</tt> (6am-6pm))
   *
   *     If an Integer is given, ambiguous times (like 5:00) will be 
   *     assumed to be within the range of that time in the AM to that time
   *     in the PM. For example, if you set it to <tt>7</tt>, then the parser will
   *     look for the time between 7am and 7pm. In the case of 5:00, it would
   *     assume that means 5:00pm. If <tt>:none</tt> is given, no assumption
   *     will be made, and the first matching instance of that time will 
   *     be used.
   */
  @SuppressWarnings("unchecked")
  public static Span parse(String text, Options options) {
    // store now for later =)
    //_now = options.getNow();

    // put the text into a normal format to ease scanning
    String normalizedText = Chronic.preNormalize(text);

    // get base tokens for each word
    List<Token> tokens = Chronic.baseTokenize(normalizedText);

    List<Class> optionScannerClasses = new LinkedList<Class>();
    optionScannerClasses.add(Repeater.class);
    for (Class optionScannerClass : optionScannerClasses) {
      try {
        tokens = (List<Token>) optionScannerClass.getMethod("scan", List.class, Options.class).invoke(null, tokens, options);
      }
      catch (Throwable e) {
        throw new RuntimeException("Failed to scan tokens.", e);
      }
    }

    List<Class> scannerClasses = new LinkedList<Class>();
    scannerClasses.add(Grabber.class);
    scannerClasses.add(Pointer.class);
    scannerClasses.add(Scalar.class);
    scannerClasses.add(Ordinal.class);
    scannerClasses.add(Separator.class);