/*
 * Redistribution and use of this software and associated documentation ("Software"), with or
 * without modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain copyright statements and notices. Redistributions
 * must also contain a copy of this document.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
 * conditions and the following disclaimer in the documentation and/or other materials provided with
 * the distribution.
 *
 * 3. The name "Exolab" must not be used to endorse or promote products derived from this Software
 * without prior written permission of Intalio, Inc. For written permission, please contact
 * info@exolab.org.
 *
 * 4. Products derived from this Software may not be called "Exolab" nor may "Exolab" appear in
 * their names without prior written permission of Intalio, Inc. Exolab is a registered trademark of
 * Intalio, Inc.
 *
 * 5. Due credit should be given to the Exolab Project (http://www.exolab.org/).
 *
 * THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESSED OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL INTALIO, INC. OR ITS
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
 * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Copyright 2000 (C) Intalio, Inc. All Rights Reserved.
 *
 * $Id$ Date Author Changes 05/13/2001 Arnaud Blandin Added the support for omitted components
 * (section 4.5 of ISO8601) 12/05/2000 Arnaud Blandin Added the support for
 * NotSupportedOperationException 11/08/2000 Arnaud Blandin Added new constructor and setValues
 * method 11/07/2000 Arnaud Blandin Added isEqual() and isGreater() methods 11/02/2000 Arnaud
 * Blandin Changed the constructor 10/26/2000 Arnaud Blandin Created
 */
package org.exolab.castor.types;

import java.util.Date;
import java.util.SimpleTimeZone;
import java.util.StringTokenizer;
import java.util.TimeZone;
import java.text.ParseException;
import java.text.SimpleDateFormat;

import org.exolab.castor.xml.ValidationException;

/**
 * Represents recurringDuration utterly a recurringDuration must contain all the fields:
 * <p>
 * (+|-)CCYY-MM-DDThh:mm:ss.sss(Z|(+|-)hh:mm)
 * <p>
 * The validation of the date fields is done in the set methods and follows
 * <a href="http://www.iso.ch/markete/8601.pdf">the ISO8601 Date and Time Format</a>
 * <p>
 * It is possible to omit higher components by using '-'.
 * <p>
 * Note: This datatype is not included in any recommendation. It was introduced in
 * http://www.w3.org/TR/1999/WD-xmlschema-2-19990924/ and was last in
 * http://www.w3.org/TR/2000/CR-xmlschema-2-20001024/ and was removed by
 * http://www.w3.org/TR/2001/PR-xmlschema-2-20010316/. It was not in the final approved
 * recommendation: http://www.w3.org/TR/2001/REC-xmlschema-2-20010502/
 *
 * @author <a href="mailto:blandin@intalio.com">Arnaud Blandin</a>
 * @version $Revision$
 * @deprecated since Castor 1.0.6 since this type is not in any recommendation.
 */
public class RecurringDuration extends RecurringDurationBase {
  /** SerialVersionUID */
  private static final long serialVersionUID = -6037158412155942249L;

  /** The date format used by the toDate() method */
  private static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS";

  /** Set to true and recompile to include debugging code in class. */
  private static final boolean DEBUG = false;


  // Private variables
  // -1 means that the field has been omitted (cf section 4.5 of ISO 8601)
  private short _century = 0;
  private short _year = 0;
  private short _month = 0;
  private short _day = 0;

  private static final short OMITED = Short.parseShort("-1");

  public RecurringDuration() {}

  /**
   * returns a recurringDuration with the facets duration and period set up
   * 
   * @param duration the TimeDuration representing the duration facet
   * @param period the TimeDuration reprensenting the period facet
   */
  public RecurringDuration(TimeDuration duration, TimeDuration period) {
    super(duration, period);
  }

  /**
   * returns a recurringDuration with the facets duration and period set up
   * 
   * @param duration the String representing the duration facet
   * @param period the String reprensenting the period facet
   */
  public RecurringDuration(String duration, String period) {
    super(duration, period);
  }


  /**
   * returns a recurringDuration with the facets duration and period set up but also the fields
   * 
   * @param duration the String representing the duration facet
   * @param period the String reprensenting the period facet
   * @param values an array of shorts which contains the values of the fields
   * @see #setValues
   */
  public RecurringDuration(String duration, String period, short[] values)
      throws UnsupportedOperationException {
    this(duration, period);
    if (values.length != 10) {
      throw new IllegalArgumentException("Wrong numbers of values");
    }
    this.setValues(values);
  }

  /**
   * set the century field
   * 
   * @param century the value to set up
   */
  public void setCentury(short century) {
    String err = "";
    if (century < -1) {
      err = "century : " + century + " must not be a negative value.";
      throw new IllegalArgumentException(err);
    }
    _century = century;
  }

  /**
   * set the Year field Note: 0000 is not allowed
   * 
   * @param year year to set up
   */
  public void setYear(short year) throws UnsupportedOperationException {
    String err = "";
    if (year < -1) {
      err = "year : " + year + " must not be a negative value.";
      throw new IllegalArgumentException(err);
    } else if ((year == -1) && (_century != -1)) {
      err = "year can not be omitted if century is not omitted.";
      throw new IllegalArgumentException(err);
    } else if ((year == 0) && (_century == 0)) {
      err = "0000 is not an allowed year";
      throw new IllegalArgumentException(err);
    }

    _year = year;
  }

  /**
   * set the Month Field
   * 
   * @param month the value to set up Note 1<month<12
   */
  public void setMonth(short month) throws UnsupportedOperationException {
    String err = "";
    if (month == -1) {
      if (_century != -1) {
        err = "month cannot be omitted if the previous component is not omitted.\n"
            + "only higher level components can be omitted.";
        throw new IllegalArgumentException(err);
      }
    } else if (month < 1) {
      err = "month : " + month + " is not a correct value." + "\n 1<month<12";
      throw new IllegalArgumentException(err);
    }

    else if (month > 12) {
      err = "month : " + month + " is not a correct value.";
      err += "\n 1<month<12";
      throw new IllegalArgumentException(err);
    }
    _month = month;
  }

  /**
   * set the Day Field
   * 
   * @param day the value to set up Note a validation is done on the day field
   */

  public void setDay(short day) throws UnsupportedOperationException {
    String err = "";
    if (day == -1) {
      if (_month != -1) {
        err = "day cannot be omitted if the previous component is not omitted.\n"
            + "only higher level components can be omitted.";
        throw new IllegalArgumentException(err);
      }
    } else if (day < 1) {
      err = "day : " + day + " is not a correct value.";
      err += "\n 1<day";
      throw new IllegalArgumentException(err);
    }
    // in february
    if (_month == 2) {
      if (isLeap()) {
        if (day > 29) {
          err = "day : " + day + " is not a correct value.";
          err += "\n day<30 (leap year and month is february)";
          throw new IllegalArgumentException(err);
        }
      } else if (day > 28) {
        err = "day : " + day + " is not a correct value.";
        err += "\n day<30 (not a leap year and month is february)";
        throw new IllegalArgumentException(err);
      } // february
    } else if ((_month == 4) || (_month == 6) || (_month == 9) || (_month == 11)) {
      if (day > 30) {
        err = "day : " + day + " is not a correct value.";
        err += "\n day<31 ";
        throw new IllegalArgumentException(err);
      }
    } else if (day > 31) {
      err = "day : " + day + " is not a correct value.";
      err += "\n day<=31 ";
      throw new IllegalArgumentException(err);
    }

    _day = day;
  }

  /**
   * return true if the year field represents a leap year A specific year is a leap year if it is
   * either evenly divisible by 400 OR evenly divisible by 4 and not evenly divisible by 100
   * 
   * @return true if the year field represents a leap year
   */
  public boolean isLeap() {
    int temp = (_century * 100 + _year);
    boolean result = (((temp % 4) == 0) && ((temp % 100) != 0));
    result = (result || ((temp % 400) == 0));
    return result;
  }

  /**
   * set all the fields by reading the values in an array
   * 
   * @param values an array of shorts with the values the array is supposed to be of length 10 and
   *        ordered like that:
   *        <ul>
   *        <li>century</li>
   *        <li>year</li>
   *        <li>month</li>
   *        <li>day</li>
   *        <li>hour</li>
   *        <li>minute</li>
   *        <li>second</li>
   *        <li>millisecond</li>
   *        <li>zoneHour</li>
   *        <li>zoneMinute</li>
   *        </ul>
   *
   * @throws UnsupportedOperationException this exception is thrown if changing the value of one
   *         field os not allowed
   * @see RecurringDurationBase#setValues
   */
  public void setValues(short[] values) throws UnsupportedOperationException {
    this.setCentury(values[0]);
    this.setYear(values[1]);
    this.setMonth(values[2]);
    this.setDay(values[3]);
    this.setHour(values[4]);
    this.setMinute(values[5]);
    this.setSecond(values[6], values[7]);
    this.setZone(values[8], values[9]);
  }


  // Get methods
  public short getCentury() {
    return (_century);
  }

  public short getYear() {
    return (_year);
  }

  public short getMonth() {
    return (_month);
  }

  public short getDay() {
    return (_day);
  }


  /**
   * returns an array of short with all the fields which describe a RecurringDuration
   * 
   * @return an array of short with all the fields which describe a RecurringDuration
   */
  public short[] getValues() {
    short[] result = null;
    result = new short[10];
    result[0] = this.getCentury();
    result[1] = this.getYear();
    result[2] = this.getMonth();
    result[3] = this.getDay();
    result[4] = this.getHour();
    result[5] = this.getMinute();
    result[6] = this.getSeconds();
    result[7] = this.getMilli();
    result[8] = this.getZoneHour();
    result[5] = this.getZoneMinute();
    return result;
  } // getValues

  /**
   * convert this recurringDuration into a local Date
   * <p>
   * Note : Be aware a the 'local' property of the date i.e <tt>toDate()</tt> will de the conversion
   * between a UTC date and your computer date format. For instance if you have set up your computer
   * time zone on the Pacific Day Time the conversion of <tt>2000-10-20T00:00:00.000</tt> into a
   * <tt>java.util.Date</tt> will return <tt>Thu Oct 19 17:00:00 PDT 2000</tt>
   * 
   * @return a local date representing this recurringDuration
   * @throws ParseException
   */
  public Date toDate() throws ParseException {
    Date date = null;
    SimpleDateFormat df = new SimpleDateFormat(DATE_FORMAT);
    SimpleTimeZone timeZone = new SimpleTimeZone(0, "UTC");

    // Set the time zone
    if (!isUTC()) {
      int offset = 0;
      offset = ((this.getZoneMinute() + this.getZoneHour() * 60) * 60 * 1000);
      offset = isZoneNegative() ? -offset : offset;
      timeZone.setRawOffset(offset);
      timeZone.setID(TimeZone.getAvailableIDs(offset)[0]);
    }
    df.setTimeZone(timeZone);
    date = df.parse(this.toPrivateString());
    return date;
  }// toDate()

  /**
   * <p>
   * Convert this recurringDuration to a string
   * <p>
   * The format is defined by W3C XML Schema draft and ISO8601 i.e
   * (+|-)CCYY-MM-DDThh:mm:ss.sss(Z|(+|-)hh:mm)
   * 
   * @return a string representing this recurringDuration
   */
  public String toString() {
    return this.toPrivateString();
  }

  /*
   * This method is needed for the toDate() method
   */
  private final String toPrivateString() {

    StringBuilder result = new StringBuilder();

    if (this.getCentury() == -1)
      result.append('-');
    else {
      if (this.getCentury() / 10 == 0)
        result.append(0);
      result.append(this.getCentury());

      if ((this.getYear() / 10) == 0)
        result.append(0);
      result.append(this.getYear());
    }
    result.append('-');
    if (this.getMonth() == -1)
      result.append('-');
    else {
      if ((this.getMonth() / 10) == 0)
        result.append(0);
      result.append(this.getMonth());
    }
    result.append('-');
    if (this.getDay() == -1)
      result.append('-');
    else {
      if ((this.getDay() / 10) == 0)
        result.append(0);
      result.append(this.getDay());
    }
    // nowhere it is said in the specs that Time can be omitted
    // choose to always keep it
    result.append('T');
    if (this.getHour() == -1)
      result.append('-');
    else {
      if ((this.getHour() / 10) == 0)
        result.append(0);
      result.append(this.getHour());
    }
    result.append(':');
    if (this.getMinute() == -1)
      result.append('-');
    else {
      if ((this.getMinute() / 10) == 0)
        result.append(0);
      result.append(this.getMinute());
    }
    result.append(':');
    if (this.getSeconds() == -1)
      result.append('-');
    else {
      if ((this.getSeconds() / 10) == 0)
        result.append(0);
      result.append(this.getSeconds());
    }
    result.append('.');
    result.append(this.getMilli());

    if (isNegative())
      result.append('-');

    // by default we choose to not concat the Z
    if (!isUTC()) {
      StringBuilder timeZone = new StringBuilder();
      if ((this.getZoneHour() / 10) == 0)
        timeZone.append(0);
      timeZone.append(this.getZoneHour());

      timeZone.append(':');
      if ((this.getZoneMinute() / 10) == 0)
        timeZone.append(0);
      timeZone.append(this.getZoneMinute());

      if (isZoneNegative())
        timeZone.insert(0, '-');
      else
        timeZone.insert(0, '+');
      result.append(timeZone);
    }

    if (isNegative())
      result.insert(0, '-');

    return result.toString();

  }// toString

  public static Object parse(String str) throws ParseException {
    return parseRecurringDuration(str);
  }

  /**
   * Parse a String and convert it into a recurringDuration.
   *
   * @param str The string to parse.
   * @return The recurringDuration represented by the string.
   * @throws ParseException A parse exception is thrown if the string to parse does not follow the
   *         rigth format (see the description of this class).
   */
  public static RecurringDuration parseRecurringDuration(String str) throws ParseException {
    // TODO optimize this method (too much strings)
    RecurringDuration result = new RecurringDuration();

    // remove if necessary the Z at the end
    if (str.endsWith("Z"))
      str = str.substring(0, str.indexOf("Z"));

    // isNegative ?
    if (str.startsWith("-") && !str.startsWith("--"))
      result.setNegative();

    // Is there a time Zone?
    String zoneStr = str.substring(str.length() - 6, str.length());
    boolean timeZone = (((zoneStr.lastIndexOf("-") != -1) || (zoneStr.lastIndexOf("+") != -1))
        && (zoneStr.lastIndexOf(":") != -1));

    if (DEBUG) {
      System.out.println("In parsing method of RecurringDuration");
      System.out.println("String to parse : " + str);
      System.out.println("Negative ? " + result.isNegative());
      String tzone = timeZone ? zoneStr : "false";
      System.out.println("Time zone :" + tzone);
    }

    if (!timeZone)
      zoneStr = null;
    else {
      int index = str.lastIndexOf("+") != -1 ? str.lastIndexOf("+") : str.lastIndexOf("-");
      str = str.substring(0, index);
    }

    // the 'T' is required
    if (str.indexOf('T') == -1) {
      throw new ParseException("The 'T' element is required", 0);
    }
    String date = str.substring(0, str.indexOf("T"));
    String time = str.substring(str.indexOf("T"));

    // proceed date
    StringTokenizer token = new StringTokenizer(date, "-");

    if (token.countTokens() > 3)
      throw new ParseException(str + ": Bad date format", 0);

    try {
      // CCYY
      boolean process = false;
      String temp;
      if (token.countTokens() == 3) {
        temp = token.nextToken();
        if (temp.length() != 4)
          throw new ParseException(str + ":Bad year format", 1);
        if (DEBUG) {
          System.out.println("Processing century: " + temp.substring(0, 2));
        }
        result.setCentury(Short.parseShort(temp.substring(0, 2)));
        if (DEBUG) {
          System.out.println("Processing year: " + temp.substring(2, 4));
        }
        result.setYear(Short.parseShort(temp.substring(2, 4)));
        process = true;
      }
      if (!process)
        result.setCentury(OMITED);
      if (token.countTokens() == 2) {
        // MM
        temp = token.nextToken();
        if (temp.length() != 2)
          throw new ParseException(str + ": Bad month format", 5);
        if (DEBUG) {
          System.out.println("Processing month: " + temp);
        }
        result.setMonth(Short.parseShort(temp));
        process = true;
      }

      if ((!process)) {
        result.setMonth(OMITED);
      }
      if (token.countTokens() == 1) {
        // DD
        temp = token.nextToken();
        if (temp.length() != 2)
          throw new ParseException(str + ":Bad day format", 8);
        if (DEBUG) {
          System.out.println("Processing day: " + temp);
        }
        result.setDay(Short.parseShort(temp));
        process = true;
      }

      if (!process) {
        result.setDay(OMITED);
      }


      // proceed Time
      token = new StringTokenizer(time, ":");
      process = false;
      if (token.countTokens() > 5)
        throw new ParseException(str + ": Bad time format", 11);

      if (token.countTokens() == 3) {
        // hh
        temp = token.nextToken();
        temp = temp.substring(temp.indexOf("T") + 1);
        if (temp.length() != 2)
          throw new ParseException(str + ": Bad hour format", 11);
        if (DEBUG) {
          System.out.println("Processing hour: " + temp);
        }
        result.setHour(Short.parseShort(temp));
        process = true;
      }

      if (!process) {
        if (result.getDay() == OMITED)
          result.setHour(OMITED);
        else
          throw new IllegalArgumentException("hour cannot be omitted");
      }
      if (token.countTokens() == 2) {
        // mm
        temp = token.nextToken();

        if (temp.length() != 2)
          throw new ParseException(str + ": Bad minute format", 14);

        if (DEBUG) {
          System.out.println("Processing minute: " + temp);
        }
        result.setMinute(Short.parseShort(temp));
        process = true;
      }
      if (!process) {
        if (result.getDay() == OMITED) {
          result.setHour(OMITED);
          result.setMinute(OMITED);
        } else
          throw new IllegalArgumentException("hour cannot be omitted");
      }
      if (token.countTokens() == 1) {
        // ss
        temp = token.nextToken();
        String milsecond = "0";
        if (temp.indexOf(".") != -1) {
          milsecond = temp.substring(temp.indexOf(".") + 1);
          temp = temp.substring(0, temp.indexOf("."));
        }

        if (temp.length() != 2)
          throw new ParseException(str + ": Bad second format", 17);
        if (DEBUG) {
          System.out.println("Processing seconds: " + temp);
        }
        result.setSecond(Short.parseShort(temp.substring(0, 2)), Short.parseShort(milsecond));

        process = true;
      }

      if (!process) {
        if (result.getDay() == OMITED) {
          result.setHour(OMITED);
          result.setMinute(OMITED);
          result.setSecond(OMITED, OMITED);
        } else
          throw new IllegalArgumentException("hour cannot be omitted");
      }

      // proceed TimeZone if any
      if (timeZone) {
        if (zoneStr.startsWith("-"))
          result.setZoneNegative();
        if (zoneStr.length() != 6)
          throw new ParseException(str + ": Bad time zone format", 20);
        result.setZone(Short.parseShort(zoneStr.substring(1, 3)),
            Short.parseShort(zoneStr.substring(4, 6)));
      } else
        result.isUTC();
      temp = null;
    } catch (UnsupportedOperationException e) {
      // we are sure that this method is used with a recurringDuration
      // (if not a ParseException is thrown) so we can never reach that point
    }
    return result;
  }// parse

  /**
   * Override the java.lang.equals method
   * 
   * @see #equal
   */
  public boolean equals(Object object) {
    if (object instanceof RecurringDuration) {
      try {
        return equal((RecurringDuration) object);
      } catch (ValidationException e) {
        e.printStackTrace();
        return false;
      }
    }
    return false;
  }

  /**
   * <p>
   * Returns true if the present instance of Recurring Duration is equal to the parameter.
   * <p>
   * The equals relation is the following :
   * <p>
   * <tt>rd1 equals rd2 </tt> iff each field of rd1 is equal to the corresponding field of rd2
   * 
   * @param reccD the recurring duration to compare with the present instance
   * @return true if the present instance is equal to the parameter false if not
   */
  public boolean equal(RecurringDuration reccD) throws ValidationException {
    boolean result = false;
    if (reccD == null)
      return false;
    if (!(this.getPeriod().equals(reccD.getPeriod()))
        || !(this.getDuration().equals(reccD.getDuration()))) {
      String err = " Recurring Duration which have different values "
          + "for the duration and period can not be compared";
      throw new ValidationException(err);
    }
    result = this.getCentury() == reccD.getCentury();
    result = result && (this.getYear() == reccD.getYear());
    result = result && (this.getMonth() == reccD.getMonth());
    result = result && (this.getDay() == reccD.getDay());
    result = result && (this.getHour() == reccD.getHour());
    result = result && (this.getMinute() == reccD.getMinute());
    result = result && (this.getSeconds() == reccD.getSeconds());
    result = result && (this.getMilli() == reccD.getMilli());
    result = result && (this.isNegative() == this.isNegative());
    if (!reccD.isUTC()) {
      result = result && (!this.isUTC());
      result = result && (this.getZoneHour() == reccD.getZoneHour());
      result = result && (this.getZoneMinute() == reccD.getZoneMinute());
    }
    return result;
  }// equals

  /**
   * <p>
   * Returns true if the present instance of RecurringDuration is greater than the parameter
   * <p>
   * Note : the order relation follows the W3C XML Schema draft i.e <tt>rd1 < rd2 </tt>iff rd2-rd1>0
   * 
   * @param reccD the recurring duration to compare with the present instance
   * @return true if the present instance is the greatest, false if not
   */
  public boolean isGreater(RecurringDuration reccD) throws ValidationException {
    boolean result = false;
    if (!(this.getPeriod().equals(reccD.getPeriod()))
        || !(this.getDuration().equals(reccD.getDuration()))) {
      String err = " Recurring Duration which have different values "
          + "for the duration and period can not be compared";
      throw new ValidationException(err);
    }
    short[] val_this = this.getValues();
    short[] val_reccD = reccD.getValues();
    int i = 0;
    while ((result != true) && (i < (val_this.length - 1))) {
      result = val_this[i] > val_reccD[i];
      if (val_this[i] < val_reccD[i])
        return false;
      i++;
    }
    return result;
  }// isGreater

} // RecurringDuration
