package com.thaiopensource.datatype.xsd;

import org.relaxng.datatype.ValidationContext;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Calendar;

class DurationDatatype extends RegexDatatype implements OrderRelation {
  static private final String PATTERN =
    "-?P([0-9]+Y)?([0-9]+M)?([0-9]+D)?(T([0-9]+H)?([0-9]+M)?(([0-9]+(\\.[0-9]*)?|\\.[0-9]+)S)?)?";

  DurationDatatype() {
    super(PATTERN);
  }

  public boolean lexicallyAllows(String str) {
    if (!super.lexicallyAllows(str))
      return false;
    char last = str.charAt(str.length()-1);
    // This enforces that there must be at least one component
    // and that T is omitted if all time components are omitted
    return last != 'P' && last != 'T';
  }

  String getLexicalSpaceKey() {
    return "duration";
  }

  static private class Duration {
    private final BigInteger years;
    private final BigInteger months;
    private final BigInteger days;
    private final BigInteger hours;
    private final BigInteger minutes;
    private final BigDecimal seconds;

    Duration(boolean negative,
             BigInteger years, BigInteger months, BigInteger days,
             BigInteger hours, BigInteger minutes, BigDecimal seconds) {
      if (negative) {
        this.years = years.negate();
        this.months = months.negate();
        this.days = days.negate();
        this.hours = hours.negate();
        this.minutes = minutes.negate();
        this.seconds = seconds.negate();
      }
      else {
        this.years = years;
        this.months = months;
        this.days = days;
        this.hours = hours;
        this.minutes = minutes;
        this.seconds = seconds;
      }
    }

    BigInteger getYears() {
      return years;
    }

    BigInteger getMonths() {
      return months;
    }

    BigInteger getDays() {
      return days;
    }

    BigInteger getHours() {
      return hours;
    }

    BigInteger getMinutes() {
      return minutes;
    }

    BigDecimal getSeconds() {
      return seconds;
    }

    public boolean equals(Object obj) {
      if (!(obj instanceof Duration))
        return false;
      Duration other = (Duration)obj;
      return (this.years.equals(other.years)
              && this.months.equals(other.months)
              && this.days.equals(other.days)
              && this.hours.equals(other.hours)
              && this.minutes.equals(other.minutes)
              && this.seconds.compareTo(other.seconds) == 0);
    }

    public int hashCode() {
      return (years.hashCode()
              ^ months.hashCode()
              ^ days.hashCode()
              ^ hours.hashCode()
              ^ minutes.hashCode()
              ^ seconds.hashCode());
    }
  }

  Object getValue(String str, ValidationContext vc) {
    int t = str.indexOf('T');
    if (t < 0)
      t = str.length();
    String date = str.substring(0, t);
    String time = str.substring(t);
    return new Duration(str.charAt(0) == '-',
                        getIntegerField(date, 'Y'),
                        getIntegerField(date, 'M'),
                        getIntegerField(date, 'D'),
                        getIntegerField(time, 'H'),
                        getIntegerField(time, 'M'),
                        getDecimalField(time, 'S'));

  }

  static private BigInteger getIntegerField(String str, char code) {
    int end = str.indexOf(code);
    if (end < 0)
      return BigInteger.valueOf(0);
    int start = end;
    while (Character.isDigit(str.charAt(start - 1)))
      --start;
    return new BigInteger(str.substring(start, end));
  }

  static private BigDecimal getDecimalField(String str, char code) {
    int end = str.indexOf(code);
    if (end < 0)
      return BigDecimal.valueOf(0);
    int start = end;
    while (!Character.isLetter(str.charAt(start - 1)))
      --start;
    return new BigDecimal(str.substring(start, end));
  }

  OrderRelation getOrderRelation() {
    return this;
  }

  private static final int[] REF_YEAR_MONTHS = { 1696, 9, 1697, 2, 1903, 3, 1903, 7 };

  public boolean isLessThan(Object obj1, Object obj2) {
    Duration d1 = (Duration)obj1;
    Duration d2 = (Duration)obj2;
    BigInteger months1 = computeMonths(d1);
    BigInteger months2 = computeMonths(d2);
    BigDecimal seconds1 = computeSeconds(d1);
    BigDecimal seconds2 = computeSeconds(d2);
    switch (months1.compareTo(months2)) {
    case -1:
      if (seconds1.compareTo(seconds2) <= 0)
        return true;
      break;
    case 0:
      return seconds1.compareTo(seconds2) < 0;
    case 1:
      if (seconds1.compareTo(seconds2) >= 0)
        return false;
      break;
    }
    for (int i = 0; i < REF_YEAR_MONTHS.length; i += 2) {
      BigDecimal total1 = daysPlusSeconds(computeDays(months1, REF_YEAR_MONTHS[i], REF_YEAR_MONTHS[i + 1]), seconds1);
      BigDecimal total2 = daysPlusSeconds(computeDays(months2, REF_YEAR_MONTHS[i], REF_YEAR_MONTHS[i + 1]), seconds2);
      if (total1.compareTo(total2) >= 0)
        return false;
    }
    return true;
  }

  /**
   * Returns the number of days spanned by a period of months starting with a particular
   * reference year and month.
   */
  private static BigInteger computeDays(BigInteger months, int refYear, int refMonth) {
    switch (months.signum()) {
    case 0:
      return BigInteger.valueOf(0);
    case -1:
      return computeDays(months.negate(), refYear, refMonth).negate();
    }
    // Complete cycle of Gregorian calendar is 400 years
    BigInteger[] tem = months.divideAndRemainder(BigInteger.valueOf(400*12));
    --refMonth; // use 0 base to match Java
    int total = 0;
    for (int rem = tem[1].intValue(); rem > 0; rem--) {
      total += daysInMonth(refYear, refMonth);
      if (++refMonth == 12) {
        refMonth = 0;
        refYear++;
      }
    }
    // In the Gregorian calendar, there are 97 (= 100 + 4 - 1) leap years every 400 years.
    return tem[0].multiply(BigInteger.valueOf(365*400 + 97)).add(BigInteger.valueOf(total));
  }

  private static int daysInMonth(int year, int month) {
    switch (month) {
    case Calendar.SEPTEMBER:
    case Calendar.APRIL:
    case Calendar.JUNE:
    case Calendar.NOVEMBER:
      return 30;
    case Calendar.FEBRUARY:
      return isLeapYear(year) ? 29 : 28;
    }
    return 31;
  }

  private static boolean isLeapYear(int year) {
    return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
  }

  /**
   * Returns the total number of seconds from a specified number of days and seconds.
   */
  private static BigDecimal daysPlusSeconds(BigInteger days, BigDecimal seconds) {
    return seconds.add(new BigDecimal(days.multiply(BigInteger.valueOf(24*60*60))));
  }

  /**
   * Returns the total number of months specified by the year and month fields of the duration
   */
  private static BigInteger computeMonths(Duration d) {
    return d.getYears().multiply(BigInteger.valueOf(12)).add(d.getMonths());
  }

  /**
   * Returns the total number of seconds specified by the days, hours, minuts and seconds fields of
   * the duration.
   */
  private static BigDecimal computeSeconds(Duration d) {
    return d.getSeconds().add(new BigDecimal(d.getDays().multiply(BigInteger.valueOf(24))
                                             .add(d.getHours()).multiply(BigInteger.valueOf(60))
                                             .add(d.getMinutes()).multiply(BigInteger.valueOf(60))));
  }

  public static void main(String[] args) {
    DurationDatatype dt = new DurationDatatype();
    System.err.println(dt.isLessThan(dt.getValue(args[0], null), dt.getValue(args[1], null)));
  }
}
