package org.basex.query.item;

import static org.basex.query.util.Err.*;
import java.math.BigDecimal;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.Duration;
import javax.xml.datatype.XMLGregorianCalendar;
import org.basex.query.QueryException;
import org.basex.util.InputInfo;
import org.basex.util.Token;
import org.basex.util.Util;

/**
 * Date container.
 *
 * @author BaseX Team 2005-11, BSD License
 * @author Christian Gruen
 */
public abstract class Date extends Item {
  /** Date pattern. */
  protected static final String ZONE = "((\\+|-)([0-9]{2}):([0-9]{2})|Z)?";
  /** Day per months. */
  protected static final byte[] DAYS = {
    31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
  /** Date pattern. */
  private static final Pattern DAT = Pattern.compile(
      "(-?)([0-9]{4})-([0-9]{2})-([0-9]{2})" + ZONE);
  /** Time pattern. */
  private static final Pattern TIM = Pattern.compile(
      "([0-9]{2}):([0-9]{2}):([0-9]{2})(\\.([0-9]+))?" + ZONE);
  /** Data factory. */
  public static DatatypeFactory df;
  /** Calendar instance. */
  public XMLGregorianCalendar xc;

  static {
    try {
      df = DatatypeFactory.newInstance();
    } catch(final Exception ex) {
      Util.notexpected();
    }
  }

  /**
   * Constructor.
   * @param typ data type
   * @param d date reference
   */
  protected Date(final Type typ, final Date d) {
    super(typ);
    xc = (XMLGregorianCalendar) d.xc.clone();
  }

  /**
   * Constructor.
   * @param typ data type
   * @param d date reference
   * @param e expected format
   * @param ii input info
   * @throws QueryException query exception
   */
  protected Date(final Type typ, final byte[] d, final String e,
      final InputInfo ii) throws QueryException {
    super(typ);
    try {
      xc = df.newXMLGregorianCalendar(Token.string(d).trim());
      if(xc.getHour() == 24) xc.add(df.newDuration(0));
    } catch(final IllegalArgumentException ex) {
      throw dateErr(d, e, ii);
    }
  }

  /**
   * Checks the date format.
   * @param d input format
   * @param e expected format
   * @param ii input info
   * @throws QueryException query exception
   */
  protected final void date(final byte[] d, final String e, final InputInfo ii)
      throws QueryException {

    final Matcher mt = DAT.matcher(Token.string(d).trim());
    if(!mt.matches()) dateErr(d, e, ii);
    zone(mt, 5, d, ii);
  }

  /**
   * Checks the time format.
   * @param d input format
   * @param e expected format
   * @param ii input info
   * @throws QueryException query exception
   */
  protected final void time(final byte[] d, final String e, final InputInfo ii)
      throws QueryException {

    final Matcher mt = TIM.matcher(Token.string(d).trim());
    if(!mt.matches()) dateErr(d, e, ii);

    final int h = Token.toInt(mt.group(1));
    final int s = Token.toInt(mt.group(3));
    if(s > 59) DATERANGE.thrw(ii, type, d);
    final double ms = mt.group(4) != null ? Double.parseDouble(mt.group(4)) : 0;
    if(h == 24 && ms > 0) dateErr(d, e, ii);
    zone(mt, 6, d, ii);
  }

  /**
   * Evaluates the timezone.
   * @param mt matcher
   * @param p matching position
   * @param val value
   * @param ii input info
   * @throws QueryException query exception
   */
  protected final void zone(final Matcher mt, final int p, final byte[] val,
      final InputInfo ii) throws QueryException {

    if(mt.group(p) == null || mt.group(p).equals("Z")) return;
    final int th = Token.toInt(mt.group(p + 2));
    final int tm = Token.toInt(mt.group(p + 3));
    if(th > 14 || tm > 59 || th == 14 && tm != 0) INVALIDZONE.thrw(ii, val);
  }

  /**
   * Add/subtract the specified duration.
   * @param a duration
   * @param p plus/minus flag
   * @param ii input info
   * @throws QueryException query exception
   */
  protected final void calc(final Dur a, final boolean p, final InputInfo ii)
      throws QueryException {

    if(xc.getYear() + a.mon / 12 > 9999) DATERANGE.thrw(ii, type, a.atom(ii));
    final Duration dur = a.toJava();
    xc.add(p ? dur : dur.negate());
    if(xc.getYear() == 0) xc.setYear(p ^ dur.getSign() < 0 ? 1 : -1);
  }

  @Override
  public final byte[] atom(final InputInfo ii) {
    String str = xc.toXMLFormat();
    str = str.replaceAll("\\.0+(Z|-.*|\\+.*)?$", "$1");
    str = str.replaceAll("(\\.\\d+?)0+(Z|-.*|\\+.*)?$", "$1$2");
    return Token.token(str);
  }

  @Override
  public final boolean eq(final InputInfo ii, final Item it)
      throws QueryException {
    final long d1 = days();
    final Date d = (Date) (it.date() ? it : type.e(it, null, ii));
    final long d2 = d.days();
    return d1 == d2 && seconds().doubleValue() == d.seconds().doubleValue();
  }

  @Override
  public int diff(final InputInfo ii, final Item it) throws QueryException {
    final long d1 = days();
    final Date d = (Date) (it.date() ? it : type.e(it, null, ii));
    final long d2 = d.days();
    if(d1 != d2) return (int) (d1 - d2);
    return seconds().subtract(d.seconds()).signum();
  }

  @Override
  public final XMLGregorianCalendar toJava() {
    return xc;
  }

  /**
   * Returns the date in seconds.
   * @return seconds
   */
  protected final BigDecimal seconds() {
    final int h = xc.getHour() == UNDEF ? 0 : xc.getHour();
    final int m = xc.getMinute() == UNDEF ? 0 : xc.getMinute();
    final int s = xc.getSecond() == UNDEF ? 0 : xc.getSecond();
    final int z = xc.getTimezone() == UNDEF ? 0 : xc.getTimezone();
    BigDecimal bd = xc.getFractionalSecond();
    if(bd == null) bd = BigDecimal.valueOf(0);
    return bd.add(BigDecimal.valueOf(h * 3600 + m * 60 - z * 60 + s));
  }

  /**
   * Returns the number of days since AD.
   * @return days
   */
  protected final long days() {
    final int y = xc.getYear() == UNDEF ? 0 : xc.getYear();
    final int m = xc.getMonth() == UNDEF ? 0 : xc.getMonth() - 1;
    final int d = xc.getDay() == UNDEF ? 0 : xc.getDay() - 1;
    final long s = days(y, m, d);
    return y > 0 ? s : -s;
  }

  /**
   * Returns days per month, considering leap years.
   * @param y year
   * @param m month
   * @return days
   */
  private static long dpm(final int y, final int m) {
    return DAYS[m] + (m == 1 ? leap(y) : 0);
  }

  /**
   * Adds an offset for a leap year.
   * @param y year
   * @return result of check
   */
  private static int leap(final int y) {
    return y % 4 == 0 && (y % 100 != 0 || y % 400 == 0) ? 1 : 0;
  }

  /**
   * Returns the number of days since AD for the specified years,
   * months and days.
   * @param y year
   * @param m month
   * @param d days
   * @return days
   */
  public static long days(final int y, final int m, final int d) {
    long n = 0;
    final int yy = Math.abs(y);
    for(int i = 0; i < yy; ++i) n += 365 + leap(i);
    for(int i = 0; i < m; ++i) n += dpm(y, i);
    return n + d;
  }

  @Override
  public final String toString() {
    return Util.info("\"%\"", atom(null));
  }
}
