package caevo;

import java.util.Calendar;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.jdom.Namespace;

import caevo.util.Pair;
import caevo.util.TimeValueParser;

/**
 * This class represents a sequence of tokens for a Timex. It holds all TimeML
 * attributes.
 * 
 * sid is 0 indexed, just like the InfoFile specifies. The first sentence entry
 * is 0. offset is 1 indexed, just like Stanford parse trees. The first token is
 * 1.
 *
 * @author chambers
 */
public class Timex {
  public static final String TIMEX_ELEM = "timex";
  public static final String TID_ATT = "tid";
  public static final String ANCHORTID_ATT = "anchortid";
  public static final String TYPE_ATT = "type";
  public static final String MOD_ATT = "mod";
  public static final String VALUE_ATT = "value";
  public static final String DOCFUNC_ATT = "docFunction";
  public static final String DOCFUNC_ATT_ALT = "functionInDocument";
  public static final String TEMPFUNC_ATT = "temporalFunction";
  public static final String TEXT_ATT = "text";
  public static final String OFFSET_ATT = "offset";
  public static final String LENGTH_ATT = "length";

  public enum Type {
    DATE, TIME, DURATION, SET
  };

  public enum DocumentFunction {
    CREATION_TIME, EXPIRATION_TIME, MODIFICATION_TIME, PUBLICATION_TIME, RELEASE_TIME, RECEPTION_TIME, NONE
  };

  public enum Mod {
    BEFORE, AFTER, ON_OR_BEFORE, ON_OR_AFTER, LESS_THAN, MORE_THAN, EQUAL_OR_LESS, EQUAL_OR_MORE, START, MID, END, APPROX
  };

  private String text;
  private int tokenOffset;
  private int tokenLength;
  private int sid;
  private String tid;
  private String anchorTid;
  private Type type;
  private Mod mod;
  private String value;
  private DocumentFunction documentFunction;
  private boolean temporalFunction = false;
  private String preposition; // feature not in Timebank
  private Pair<Calendar, Calendar> timeRange;

  public Timex() {

  }

  /**
   * Copy Constructor
   * 
   * @param timex
   */
  public Timex(Timex timex) {
    this.tid = timex.getTid();
    this.text = timex.getText();
    this.tokenOffset = timex.getTokenOffset();
    this.tokenLength = timex.getTokenLength();
    this.anchorTid = timex.getAnchorTid();
    this.type = timex.getType();
    this.value = timex.getValue();
    this.mod = timex.getMod();
    this.documentFunction = timex.getDocumentFunction();
    this.temporalFunction = timex.getTemporalFunction();
  }

  /**
   * Copy Constructor with new value specified
   * 
   * @param timex
   */
  public Timex(Timex timex, String newVal) {
    this.tid = timex.getTid();
    this.text = timex.getText();
    this.tokenOffset = timex.getTokenOffset();
    this.tokenLength = timex.getTokenLength();
    this.anchorTid = timex.getAnchorTid();
    this.type = timex.getType();
    this.value = timex.getValue();
    this.mod = timex.getMod();
    this.documentFunction = timex.getDocumentFunction();
    this.temporalFunction = timex.getTemporalFunction();
  }

  /**
   * Copy Constructor with new value specified
   * 
   * @param timex
   */
  public Timex(String val) {
    this.value = val;
  }

  public Timex(org.jdom.Element el) {
    this.tid = el.getAttributeValue(Timex.TID_ATT);
    this.text = el.getAttributeValue(Timex.TEXT_ATT);
    this.tokenOffset = Integer.valueOf(el.getAttributeValue(Timex.OFFSET_ATT));
    this.tokenLength = Integer.valueOf(el.getAttributeValue(Timex.LENGTH_ATT));
    this.anchorTid = el.getAttributeValue(Timex.ANCHORTID_ATT);
    this.type = Type.valueOf(el.getAttributeValue(Timex.TYPE_ATT));
    this.value = el.getAttributeValue(Timex.VALUE_ATT);

    String modStr = el.getAttributeValue(Timex.MOD_ATT);
    if (modStr != null && !modStr.isEmpty())
      this.mod = Mod.valueOf(el.getAttributeValue(Timex.MOD_ATT));

    String docFunStr = el.getAttributeValue(Timex.DOCFUNC_ATT);
    if (docFunStr != null && !docFunStr.isEmpty())
      this.documentFunction = DocumentFunction.valueOf(docFunStr);

    this.temporalFunction = el.getAttributeValue(Timex.TEMPFUNC_ATT)
        .equalsIgnoreCase("true");
  }

  // <TIMEX3 mod="APPROX" endPoint="t42" tid="t39" temporalFunction="false"
  // type="DURATION" functionInDocument="NONE" value="P2Y" >
  public void saveAttributes(org.w3c.dom.Element el) {
    this.tid = el.getAttribute(Timex.TID_ATT);
    this.anchorTid = el.getAttribute(Timex.ANCHORTID_ATT);
    this.type = Type.valueOf(el.getAttribute(Timex.TYPE_ATT));
    this.value = el.getAttribute(Timex.VALUE_ATT);

    String modStr = el.getAttribute(Timex.MOD_ATT);
    if (modStr != null && !modStr.isEmpty())
      this.mod = Mod.valueOf(modStr);

    String docFunStr = el.getAttribute(Timex.DOCFUNC_ATT);
    // If not found, try with the alternate name.
    if (docFunStr == null || docFunStr.isEmpty())
      docFunStr = el.getAttribute(Timex.DOCFUNC_ATT_ALT);
    // If found, grab the value.
    if (docFunStr != null && !docFunStr.isEmpty())
      this.documentFunction = DocumentFunction.valueOf(docFunStr);

    this.temporalFunction = el.getAttribute(Timex.TEMPFUNC_ATT)
        .equalsIgnoreCase("true");

    String text = el.getAttribute(Timex.TEXT_ATT);
    if (text != null && text.length() > 0)
      this.text = text;
    else {
      text = el.getTextContent();
      if (text != null && text.length() > 0)
        this.text = text;
    }
  }

  public void setText(String text) {
    this.text = text;
  }

  public void setSid(int sid) {
    this.sid = sid;
  }

  public void setTid(String tid) {
    this.tid = tid;
  }

  public void setAnchorTid(String anchorTid) {
    this.anchorTid = anchorTid;
  }

  /**
   * @param s
   *          The token integer offset where the timex starts within the
   *          sentence
   * @param e
   *          The token integer offset where the timex ends, exclusive within
   *          the sentence
   */
  public void setSpan(int s, int e) {
    this.tokenOffset = s;
    this.tokenLength = e - s;
  }

  public void setType(Type type) {
    this.type = type;
  }

  public void setValue(String value) {
    this.value = value;
    this.timeRange = null;
  }

  public void setPrep(String prep) {
    this.preposition = prep;
  }

  public void setDocumentFunction(DocumentFunction func) {
    this.documentFunction = func;
  }

  public int getTokenOffset() {
    return this.tokenOffset;
  }

  public int getTokenLength() {
    return this.tokenLength;
  }

  public int getSid() {
    return this.sid;
  }

  public String getTid() {
    return this.tid;
  }

  public String getAnchorTid() {
    return this.anchorTid;
  }

  public Type getType() {
    return this.type;
  }

  public Mod getMod() {
    return this.mod;
  }

  public String getValue() {
    return this.value;
  }

  public String getText() {
    return this.text;
  }

  public DocumentFunction getDocumentFunction() {
    return this.documentFunction;
  }

  public boolean getTemporalFunction() {
    return this.temporalFunction;
  }

  public String getPrep() {
    return this.preposition;
  }

  public Pair<Calendar, Calendar> getRange() {
    return getRange(null);
  }

  public Pair<Calendar, Calendar> getRange(Timex documentCreation) {
    if (this.type == Timex.Type.DURATION || this.type == Timex.Type.SET
        || isPastReference() || isFutureReference())
      return null;

    if (this.timeRange == null) {
      TimeValueParser timeParse = new TimeValueParser(this.value);
      this.timeRange = timeParse.getRange();
    }

    return this.timeRange;
  }

  public boolean isReference() {
    return isPastReference() || isPresentReference() || isFutureReference();
  }

  public boolean isPastReference() {
    return this.value.equalsIgnoreCase("PAST_REF");
  }

  public boolean isPresentReference() {
    return this.value.equalsIgnoreCase("PRESENT_REF");
  }

  public boolean isFutureReference() {
    return this.value.equalsIgnoreCase("FUTURE_REF");
  }

  public boolean isDCT() {
    return documentFunction == DocumentFunction.CREATION_TIME;
  }

  /**
   * Determines if this timex occurs before the other in the document. It
   * assumes both timexes are from the same document.
   * 
   * @param other
   *          Another timex to compare against.
   * @return True if this timex occurs before the other in the document.
   */
  public boolean isBeforeInText(Timex other) {
    if (sid < other.getSid())
      return true;
    else if (sid == other.getSid() && tokenOffset < other.getTokenOffset())
      return true;
    else
      return false;
  }

  /**
   * @desc Looks in the "value" attribute of the timexes, and compares the
   *       ordering if they are both DATEs or TIMEs. The value is a date string
   *       "YYYY-MM-DD".
   * @param other
   *          The date/time with which to compare this object
   * @return True if this timex occurs before the given timex
   */
  public boolean before(Timex other) {
    // don't bother if we're not a date or time
    if ((this.type == Type.DATE || this.type == Type.TIME)
        && (other.getType() == Type.DATE || other.getType() == Type.TIME)) {
      String ovalue = other.getValue();

      // make sure there are values to compare
      if (this.value != null && this.value.length() > 0
          && this.value.matches("\\d.*") && ovalue != null
          && ovalue.length() > 0 && ovalue.matches("\\d.*")) {

        if (this.value.equalsIgnoreCase(ovalue))
          return false;

        try {
          // System.out.println("Checking " + value + " and " + ovalue);
          // check years
          int year = Integer.valueOf(this.value.substring(0, 4));
          int oyear = Integer.valueOf(ovalue.substring(0, 4));
          if (year < oyear)
            return true;
          else if (oyear < year)
            return false;

          // check months
          if (this.value.length() > 4 && ovalue.length() > 4) {
            int month = Integer.valueOf(this.value.substring(5, 7));
            int omonth = Integer.valueOf(ovalue.substring(5, 7));
            if (month < omonth)
              return true;
            else if (omonth < month)
              return false;
          }

          // check days - some timexes don't have days e.g. "1998-10"
          if (this.value.length() > 7 && ovalue.length() > 7) {
            int day = Integer.valueOf(this.value.substring(8, 10));
            int oday = Integer.valueOf(ovalue.substring(8, 10));
            if (day < oday)
              return true;
            else if (oday < day)
              return false;
          }
          // Called when the date is bad, non-numeric ... "1998-WXX"
        } catch (NumberFormatException ex) {
        }
      }
    }
    return false;
  }

  /**
   * @desc Looks in the "value" attribute of the timexes, and compares the
   *       ordering if they are both DATEs or TIMEs. The value is a date string
   *       "YYYY-MM-DD".
   * @param other
   *          The date/time with which to compare this object
   * @return True if this timex occurs before the given timex
   */
  public boolean includes(Timex other) {
    // don't bother if we're not a date or time
    if ((this.type == Type.DATE || this.type == Type.TIME
        || this.type == Type.DURATION)
        && (other.getType() == Type.DATE || other.getType() == Type.TIME
            || other.getType() == Type.DURATION)) {
      String ovalue = other.getValue();

      // make sure there are values to compare
      if (this.value != null && this.value.length() > 0
          && this.value.matches("\\d.*") && ovalue != null
          && ovalue.length() > 0 && ovalue.matches("\\d.*")) {

        // If the values are exactly the same, then they aren't INCLUDES.
        if (this.value.equalsIgnoreCase(ovalue))
          return false;

        // If a timex in the document is "now", then it INCLUDES the document's
        // creation time.
        if (this.value.equalsIgnoreCase("PRESENT_REF")
            && other.getDocumentFunction() == DocumentFunction.CREATION_TIME)
          return true;

        try {
          // System.out.println("Checking " + value + " and " + ovalue);
          // check years
          int year = Integer.valueOf(this.value.substring(0, 4));
          int oyear = Integer.valueOf(ovalue.substring(0, 4));
          if (year != oyear)
            return false;
          else { // same year

            // Then this time is a year, and the other is a month in the year.
            // True!
            if (this.value.length() == 4 && ovalue.length() > 4)
              return true;

            // Both have a month, so see if it is the same month.
            else if (this.value.length() > 4 && ovalue.length() > 4) {
              int month = Integer.valueOf(this.value.substring(5, 7));
              int omonth = Integer.valueOf(ovalue.substring(5, 7));
              if (month != omonth)
                return false;
              else if (this.value.length() == 7 && ovalue.length() > 7)
                return true;
            }
          }

          // Called when the date is bad, non-numeric ... "1998-WXX"
        } catch (NumberFormatException ex) {
        }
      }
    }
    return false;
  }

  /**
   * This prints only the opening TIMEX3 tag, and does not close it nor print
   * its inner text contents.
   * 
   * @return A string representation of the <TIMEX3 ...> opening tag.
   */
  public String toXMLString() {
    StringBuffer buf = new StringBuffer();
    buf.append("<TIMEX3 tid=\"");
    buf.append(this.getTid());
    buf.append('\"');
    buf.append(" type=\"");
    buf.append(this.getType());
    buf.append('\"');
    buf.append(" value=\"");
    buf.append(this.getValue());
    buf.append('\"');
    buf.append(" temporalFunction=\"");
    buf.append(this.getTemporalFunction());
    buf.append('\"');
    buf.append(" functionInDocument=\"");
    if (this.getDocumentFunction() == null)
      buf.append("NONE");
    else
      buf.append(this.getDocumentFunction());
    buf.append('\"');
    buf.append('>');
    // buf.append(text());
    // buf.append("</TIMEX3>");
    return buf.toString();
  }

  public org.jdom.Element toElement(Namespace ns) {
    org.jdom.Element el = new org.jdom.Element(Timex.TIMEX_ELEM, ns);
    el.setAttribute(Timex.TID_ATT, tid);
    if (text != null)
      el.setAttribute(Timex.TEXT_ATT, text);
    el.setAttribute(Timex.OFFSET_ATT, String.valueOf(this.tokenOffset));
    el.setAttribute(Timex.LENGTH_ATT, String.valueOf(this.tokenLength));
    if (this.anchorTid != null)
      el.setAttribute(Timex.ANCHORTID_ATT, this.anchorTid);
    el.setAttribute(Timex.TYPE_ATT, String.valueOf(type));
    if (this.mod != null)
      el.setAttribute(Timex.MOD_ATT, String.valueOf(this.mod.toString()));
    if (this.value != null)
      el.setAttribute(Timex.VALUE_ATT, this.value);
    if (this.documentFunction != null)
      el.setAttribute(Timex.DOCFUNC_ATT, String.valueOf(this.documentFunction));
    el.setAttribute(Timex.TEMPFUNC_ATT, String.valueOf(this.temporalFunction));
    return el;
  }

  public String toString() {
    return this.tid + " " + this.sid + " " + this.type + " " + this.value + " '"
        + this.text + "' " + this.preposition;
  }

  public static String dateFromValue(String value) {
    String creationTimeRegex = "(\\d{4}-\\d{2}-\\d{2}).*";
    Pattern creationTimePattern = Pattern.compile(creationTimeRegex);
    Matcher matcher = creationTimePattern.matcher(value);
    if (matcher.matches()) {
      String dayValue = matcher.group(1);
      return dayValue;
    } else {
      return null;
    }
  }

  /**
   * @returns Timex with truncated DCT (to the day granularity)
   * @param DCTTimex
   */
  public static Timex dctDayTimex(Timex dctTimex) {
    // First confirm that dctTimex is DCT
    if (!dctTimex.isDCT())
      return null;

    // Truncate the dctTimex's value to the day granularity if possible;
    // otherwise, do nothing
    Timex dcdTimex = new Timex(dctTimex);
    String creationTimeRegex = "(\\d{4}-\\d{2}-\\d{2}).*";
    Pattern creationTimePattern = Pattern.compile(creationTimeRegex);
    Matcher matcher = creationTimePattern.matcher(dctTimex.getValue());
    if (matcher.matches()) {
      String dcdValue = matcher.group(1);
      dcdTimex.setValue(dcdValue);
    }

    // Confirm that dcdTimex's value is of the appropriate form (no need if
    // match acheived above),
    // and then return it;
    // if it doesn't then return null
    String creationDayRegex = "\\d{4}-\\d{2}-\\d{2}";
    Pattern creationDayPattern = Pattern.compile(creationTimeRegex);
    Matcher matcherDay = creationTimePattern.matcher(dctTimex.getValue());
    if (matcherDay.matches()) {
      return dcdTimex;
    } else {
      return null;
    }
  }
}
