/*
 * DateTimeRange class.
 *
 * Copyright (C) 2007 SATOH Takayuki All Rights Reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
package ts.util;

import java.util.List;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.TimeZone;
import java.io.Serializable;

/**
 * 日時の範囲を表すクラス。
 * <br>
 * フィールド変数に二つの日時を持ち、その差を年単位や日単位で算出したり、
 * 指定された日時が範囲内にあるかどうかを判定するためのメソッドを用意している。
 *
 * @author  佐藤隆之. 
 * @version $Revision: 1.2 $, $Date: 2011-08-01 15:29:30 $
 */
public class DateTimeRange extends Range<DateTime>
{
  /** シリアル・バージョン番号。 */
  static final long serialVersionUID = 6074073624839528360L;

  /** 開始日時のミリ秒。 */
  private long startMillis_ ;

  /** 終了日時。 */
  private long endMillis_ ;

  /** カレンダ。 */
  private Calendar calendar_ ; 

  /**
   * 二つの日時オブジェクトを引数にとるコンストラクタ。
   * <br>
   * 引数の日時オブジェクトには、大小を関係なく指定することができる。
   *
   * @param  dttm1 日時1。
   * @param  dttm2 日時2。
   * @throws IllegalArgumentException 二つの日時オブジェクトのカレンダーが
   *           異なる場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  public DateTimeRange(DateTime dttm1, DateTime dttm2)
  {
    super(dttm1, dttm2);
  }

  /**
   * 二つの日時オブジェクトを設定する。
   * <br>
   * 引数の日時オブジェクトには、大小を関係なく指定することができる。
   *
   * @param  dttm1 日時1。
   * @param  dttm2 日時2。
   * @throws IllegalArgumentException 二つの日時オブジェクトのカレンダーが
   *           異なる場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ)。
   */
  @Override
  public final void setRange(DateTime dttm1, DateTime dttm2)
  {
    assert (dttm1 != null) : "@param:dttm1 is null.";
    assert (dttm2 != null) : "@param:dttm2 is null.";

    Calendar cal1 = dttm1.getCalendar();
    cal1.setTimeInMillis(0L);

    Calendar cal2 = dttm2.getCalendar();
    cal2.setTimeInMillis(0L);

    if (! cal1.equals(cal2)) {
      throw new IllegalArgumentException(
        "calendar of two date-time are different.");
    }

    set(dttm1.getDateTimeMillis(), dttm2.getDateTimeMillis(), cal2);
  }

  /**
   * 日時範囲の開始ミリ秒と終了ミリ秒、カレンダを設定する。
   * <br>
   * 開始ミリ秒と終了ミリ秒の大小関係が逆の場合は入れ替えて設定する。
   *
   * @param  millis1 開始ミリ秒。
   * @param  millis2 終了ミリ秒。
   * @param  calendar カレンダオブジェクト。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  protected void set(long millis1, long millis2, Calendar calendar)
  {
    assert (calendar != null) : "@param:calendar is null.";

    if (millis1 < millis2) {
      startMillis_ = millis1;
      endMillis_ = millis2;
    }
    else {
      startMillis_ = millis2;
      endMillis_ = millis1;
    }
    
    calendar_ = calendar;
  }
  
  /**
   * 開始日時を取得する。
   * <br>
   * 二つの日時のうちの小さい方を返す。
   *
   * @return 開始日時。
   */
  @Override
  public DateTime getStart()
  {
    DateTime dttm = new DateTime(calendar_);
    dttm.setDateTime(startMillis_);
    return dttm;
  }

  /**
   * 終了日時を取得する。
   * <br>
   * 二つの日時のうちの大きい方を返す。
   *
   * @return 終了日時。
   */
  @Override
  public DateTime getEnd()
  {
    DateTime dttm = new DateTime(calendar_);
    dttm.setDateTime(endMillis_);
    return dttm;
  }

  /**
   * 二つの日時の差を年単位で求める。
   * <br>
   * 二つの日時の大きい方が、小さい方の翌年の日時よりも前の場合に0と数える。
   * 例えば、2005-04-10〜2006-04-09の場合は0、2005-04-10〜2006-04-11の場合は1を
   * 返す。
   *
   * @return このオブジェクトが保持する二つの日時の年単位の差。
   */
  public int intervalInYear()
  {
    long  millisS = startMillis_ ;
    long  millisE = endMillis_ ;

    calendar_.setTimeInMillis(millisE);
    int y = calendar_.get(Calendar.YEAR);
    calendar_.setTimeInMillis(millisS);
    y -= calendar_.get(Calendar.YEAR);

    calendar_.add(Calendar.YEAR, y);
    long millis = calendar_.getTimeInMillis();
    return (millis > millisE) ? (y-1) : y;
  }

  /**
   * 二つの日時の差を月単位で求める。
   * <br>
   * 二つの日時の大きい方が、小さい方の翌月の日時よりも前の場合に0と数える。
   * 例えば、2006-04-10〜2006-05-09の場合は0、2006-04-10〜2006-05-11の場合は1を
   * 返す。
   *
   * @return このオブジェクトが保持する二つの日時の月単位の差。
   */
  public int intervalInMonth()
  {
    long millisS = startMillis_ ;
    long millisE = endMillis_ ;

    int m;
    if (calendar_ instanceof GregorianCalendar) {
      calendar_.setTimeInMillis(millisE);
      int y = calendar_.get(Calendar.YEAR);
      m = calendar_.get(Calendar.MONTH);
      calendar_.setTimeInMillis(millisS);
      y -= calendar_.get(Calendar.YEAR);
      m -= calendar_.get(Calendar.MONTH);
      m += y * 12;
    }
    else {
      calendar_.setTimeInMillis(millisE);
      int ye = calendar_.get(Calendar.YEAR);
      m = calendar_.get(Calendar.MONTH);
      calendar_.setTimeInMillis(millisS);
      int ys = calendar_.get(Calendar.YEAR);
      m -= calendar_.get(Calendar.MONTH);
      for (int y=ys; y<ye; y++) {
        calendar_.clear();
        calendar_.set(y+1, 0, 1, 0, 0, 0);
        calendar_.add(Calendar.SECOND, -1);
        m += calendar_.get(Calendar.MONTH) + 1;
      }
      calendar_.setTimeInMillis(millisS);
    }

    calendar_.add(Calendar.MONTH, m);
    long millis = calendar_.getTimeInMillis();
    return (millis > millisE) ? (m-1) : m;
  } 

  /**
   * 二つの日時の差を日単位で求める。
   * <br>
   * 二つの日時の大きい方が、小さい方の翌日の日時よりも前の場合に0と数える。
   * 例えば、2006-04-10 11:22:33〜2006-04-11 11:22:32の場合は0、
   * 2006-04-10 11:22:33〜2006-04-11 11:22:34の場合は1を返す。
   *
   * @return このオブジェクトが保持する二つの日時の日単位の差。
   */
  public int intervalInDay()
  {
    long millisS = startMillis_ ;
    long millisE = endMillis_ ;

    return (int)((millisE - millisS) / (long)(24 * 60 * 60 * 1000));
  }

  /**
   * 二つの日時の差を時単位で求める。
   * <br>
   * 二つの日時の大きい方が、小さい方の次の時の日時よりも前の場合に0と数える。
   * 例えば、2006-04-10 11:22:33〜2006-04-10 12:22:32の場合は0、
   * 2006-04-10 11:22:33〜2006-04-10 12:22:34の場合は1を返す。
   *
   * @return このオブジェクトが保持する二つの日時の時単位の差。
   */
  public long intervalInHour()
  {
    long millisS = startMillis_ ;
    long millisE = endMillis_ ;

    return (millisE - millisS) / (long)(60 * 60 * 1000);
  }

  /**
   * 二つの日時の差を分単位で求める。
   * <br>
   * 二つの日時の大きい方が、小さい方の次の分の日時よりも前の場合に0と数える。
   * 例えば、2006-04-10 11:22:33〜2006-04-10 11:23:32の場合は0、
   * 2006-04-10 11:22:33〜2006-04-10 11:22:34の場合は1を返す。
   *
   * @return このオブジェクトが保持する二つの日時の分単位の差。
   */
  public long intervalInMinute()
  {
    long millisS = startMillis_ ;
    long millisE = endMillis_ ;

    return (millisE - millisS) / (long)(60 * 1000);
  }

  /**
   * 二つの日時の差を秒単位で求める。
   * <br>
   * 二つの日時の大きい方が、小さい方の次の秒の日時よりも前の場合に0と数える。
   * 例えば、2006-04-10 11:22:33.111〜2006-04-10 11:22:34.110の場合は0、
   * 2006-04-10 11:22:33.111〜2006-04-10 11:22:34.112の場合は1を返す。
   *
   * @return このオブジェクトが保持する二つの日時の分単位の差。
   */
  public long intervalInSecond()
  {
    long millisS = startMillis_ ;
    long millisE = endMillis_ ;

    return (millisE - millisS) / (long)(1000);
  }

  /**
   * 日時の範囲に含まれる年の数を求める。
   * <br>
   * 例えば、2005-04-10〜2007-04-09の場合は2005,2006,2007年が含まれるので3を
   * 返す。
   *
   * @return このオブジェクトが示す日時の範囲に含まれる年の数。
   */
  public int countYears()
  {
    long millisS = startMillis_ ;
    long millisE = endMillis_ ;

    calendar_.setTimeInMillis(millisE);
    int y = calendar_.get(Calendar.YEAR);
    calendar_.setTimeInMillis(millisS);
    y -= calendar_.get(Calendar.YEAR);

    return (y+1);
  }

  /**
   * 日時の範囲に含まれる月の数を求める。
   * <br>
   * 例えば、2006-04-10〜2006-06-09の場合は4,5,6月が含まれるので3を返す。
   *
   * @return このオブジェクトが示す日時の範囲に含まれる月の数。
   */
  public int countMonths()
  {
    long millisS = startMillis_ ;
    long millisE = endMillis_ ;

    int m;
    if (calendar_ instanceof GregorianCalendar) {
      calendar_.setTimeInMillis(millisE);
      int y = calendar_.get(Calendar.YEAR);
      m = calendar_.get(Calendar.MONTH);
      calendar_.setTimeInMillis(millisS);
      y -= calendar_.get(Calendar.YEAR);
      m -= calendar_.get(Calendar.MONTH);
      m += y * 12;
    }
    else {
      calendar_.setTimeInMillis(millisE);
      int ye = calendar_.get(Calendar.YEAR);
      m = calendar_.get(Calendar.MONTH);
      calendar_.setTimeInMillis(millisS);
      int ys = calendar_.get(Calendar.YEAR);
      m -= calendar_.get(Calendar.MONTH);
      for (int y=ys; y<ye; y++) {
        calendar_.clear();
        calendar_.set(y+1, 0, 1, 0, 0, 0);
        calendar_.add(Calendar.SECOND, -1);
        m += calendar_.get(Calendar.MONTH) + 1;
      }
    }

    return (m+1);
  } 

  /**
   * 日時の範囲に含まれる日の数を求める。
   * <br>
   * 例えば、2006-04-10 11:22:33〜2006-04-12 11:22:32の場合は10,11,12日
   * が含まれるので3を返す。
   *
   * @return このオブジェクトが示す日時の範囲に含まれる日の数。
   */
  public int countDays()
  {
    long millisS = startMillis_ ;
    long millisE = endMillis_ ;

    DateTime dttmS = new DateTime(millisS);
    DateTime dttmE = new DateTime(millisE);
    dttmS.setStartTimeOfDay();
    dttmE.setStartTimeOfDay();
    dttmE.addDay(1);
    long diff = dttmE.getDateTimeMillis() - dttmS.getDateTimeMillis();
    return (int)(diff / (24L * 60L * 60L * 1000L));
  }

  /**
   * 日時の範囲に含まれる時の数を求める。
   * <br>
   * 例えば、2006-04-10 11:22:33〜2006-04-10 13:22:32の場合は11,12,13時が
   * 含まれるので3を返す。
   *
   * @return このオブジェクトが示す日時の範囲に含まれる時の数。
   */
  public long countHours()
  {
    long millisS = startMillis_ ;
    long millisE = endMillis_ ;

    calendar_.setTimeInMillis(millisS);
    int yS = calendar_.get(Calendar.YEAR);
    int mS = calendar_.get(Calendar.MONTH);
    int dS = calendar_.get(Calendar.DAY_OF_MONTH);
    int hS = calendar_.get(Calendar.HOUR_OF_DAY);
    calendar_.clear();
    calendar_.set(yS, mS, dS, hS, 0, 0);
    millisS = calendar_.getTimeInMillis();

    calendar_.setTimeInMillis(millisE);
    int yE = calendar_.get(Calendar.YEAR);
    int mE = calendar_.get(Calendar.MONTH);
    int dE = calendar_.get(Calendar.DAY_OF_MONTH);
    int hE = calendar_.get(Calendar.HOUR_OF_DAY);
    calendar_.clear();
    calendar_.set(yE, mE, dE, hE, 0, 0);
    calendar_.add(Calendar.HOUR_OF_DAY, 1);
    millisE = calendar_.getTimeInMillis();

    return (millisE - millisS) / (60L * 60L * 1000L);
  }

  /**
   * 日時の範囲に含まれる分の数を求める。
   * <br>
   * 例えば、2006-04-10 11:22:33〜2006-04-10 11:24:32の場合は22,23,24分が
   * 含まれるので3を返す。
   *       
   * @return このオブジェクトが示す日時の範囲に含まれる分の数。
   */
  public long countMinutes()
  {
    long millisS = startMillis_ ;
    long millisE = endMillis_ ;

    calendar_.setTimeInMillis(millisS);
    int yS = calendar_.get(Calendar.YEAR);
    int mS = calendar_.get(Calendar.MONTH);
    int dS = calendar_.get(Calendar.DAY_OF_MONTH);
    int hS = calendar_.get(Calendar.HOUR_OF_DAY);
    int miS = calendar_.get(Calendar.MINUTE);
    calendar_.clear();
    calendar_.set(yS, mS, dS, hS, miS, 0);
    millisS = calendar_.getTimeInMillis();

    calendar_.setTimeInMillis(millisE);
    int yE = calendar_.get(Calendar.YEAR);
    int mE = calendar_.get(Calendar.MONTH);
    int dE = calendar_.get(Calendar.DAY_OF_MONTH);
    int hE = calendar_.get(Calendar.HOUR_OF_DAY);
    int miE = calendar_.get(Calendar.MINUTE);
    calendar_.clear();
    calendar_.set(yE, mE, dE, hE, miE, 0);
    calendar_.add(Calendar.MINUTE, 1);
    millisE = calendar_.getTimeInMillis();

    return (millisE - millisS) / (60L * 1000L);
  }

  /**
   * 日時の範囲に含まれる秒の数を求める。
   * <br>
   * 例えば、2006-04-10 11:22:33.444〜2006-04-10 11:22:35.443の場合は
   * 33,34,35秒が含まれるので3を返す。
   *
   * @return このオブジェクトが示す日時の範囲に含まれる秒の数。
   */
  public long countSeconds()
  {
    long millisS = startMillis_ ;
    long millisE = endMillis_ ;

    calendar_.setTimeInMillis(millisS);
    int yS = calendar_.get(Calendar.YEAR);
    int mS = calendar_.get(Calendar.MONTH);
    int dS = calendar_.get(Calendar.DAY_OF_MONTH);
    int hS = calendar_.get(Calendar.HOUR_OF_DAY);
    int miS = calendar_.get(Calendar.MINUTE);
    int sS = calendar_.get(Calendar.SECOND);
    calendar_.clear();
    calendar_.set(yS, mS, dS, hS, miS, sS);
    millisS = calendar_.getTimeInMillis();

    calendar_.setTimeInMillis(millisE);
    int yE = calendar_.get(Calendar.YEAR);
    int mE = calendar_.get(Calendar.MONTH);
    int dE = calendar_.get(Calendar.DAY_OF_MONTH);
    int hE = calendar_.get(Calendar.HOUR_OF_DAY);
    int miE = calendar_.get(Calendar.MINUTE);
    int sE = calendar_.get(Calendar.SECOND);
    calendar_.clear();
    calendar_.set(yE, mE, dE, hE, miE, sE);
    calendar_.add(Calendar.SECOND, 1);
    millisE = calendar_.getTimeInMillis();

    return (millisE - millisS) / 1000L;
  }

  /**
   * 指定された日時がこのオブジェクトの示す日時の範囲に含まれているかどうかを
   * 判定する。
   *
   * @param  dttm 判定対象となる日時。
   * @return 引数の日時が含まれている場合は<tt>true</tt>、そうでなければ
   *           <tt>false</tt>。
   * @throws IllegalArgumentException 引数のもつカレンダが、このオブジェクト
   *           の保持するカレンダと異なる場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ)。
   */
  @Override
  public boolean encompass(DateTime dttm)
  {
    assert (dttm != null) : "@param:dttm is null.";

    calendar_.setTimeInMillis(0L);

    Calendar cal = dttm.getCalendar();
    cal.setTimeInMillis(0L);

    if (! calendar_.equals(cal)) {
      throw new IllegalArgumentException(
        "Specified calendar are different from calendar of this object.");
    }

    long ltime = dttm.getDateTimeMillis();
    return (startMillis_ <= ltime && ltime <= endMillis_);
  }

  /**
   * 指定された日時の範囲がこのオブジェクトの示す日時の範囲に含まれているか
   * どうかを判定する。
   *
   * @param  range 判定対象となる日時の範囲。
   * @return 引数の日時の範囲が含まれている場合は<tt>true</tt>、そうでなければ
   *           <tt>false</tt>。
   * @throws IllegalArgumentException 引数のもつカレンダが、このオブジェクト
   *           の保持するカレンダと異なる場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ)。
   */
  public boolean encompass(DateTimeRange range)
  {
    assert (range != null) : "@param:range is null.";

    calendar_.setTimeInMillis(0L);

    Calendar cal = range.calendar_ ;
    cal.setTimeInMillis(0L);

    if (! calendar_.equals(cal)) {
      throw new IllegalArgumentException(
        "Specified calendar are different from calendar of this object.");
    }

    long millisS = startMillis_ ;
    long millisE = endMillis_ ;

    long millisS1 = range.startMillis_;
    long millisE1 = range.endMillis_;

    return ((millisS1 >= millisS) && (millisE1 <= millisE)) ? true : false;
  }

  /**
   * 指定された日時の範囲とこのオブジェクトの示す日時の範囲の重なる範囲を
   * 取得する。
   *
   * @param  range 日時の範囲。
   * @return 引数の日時の範囲とこのオブジェクトとが重なる範囲。
   * @throws IllegalArgumentException 引数のもつカレンダが、このオブジェクト
   *           の保持するカレンダと異なる場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ)。
   */
  public DateTimeRange intersectsWith(DateTimeRange range)
  {
    assert (range != null) : "@param:range is null.";

    calendar_.setTimeInMillis(0L);

    Calendar cal = range.calendar_;
    cal.setTimeInMillis(0L);

    if (! calendar_.equals(cal)) {
      throw new IllegalArgumentException(
        "Specified calendar are different from calendar of this object.");
    }

    long millisS = Math.max(startMillis_, range.startMillis_);
    long millisE = Math.min(endMillis_, range.endMillis_);

    if (millisS > millisE) {
      return null;
    }
    return new DateTimeRange(new DateTime(millisS), new DateTime(millisE));
  }

  /**
   * 指定された日時を含むように、このオブジェクトの示す範囲を拡張した範囲を取得
   * する。
   * <Br>
   * 元から指定された日時を含んでいた場合は、このオブジェクトと同じ範囲を返す。
   *
   * @param  dttm 日時オブジェクト。
   * @return 指定された日時を含むように拡張された範囲。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  @Override
  public DateTimeRange expandsWith(DateTime dttm)
  {
    assert (dttm != null) : "@param:dttm is null.";

    long millisS = Math.min(startMillis_, dttm.getDateTimeMillis());
    long millisE = Math.max(endMillis_, dttm.getDateTimeMillis());

    return new DateTimeRange(new DateTime(millisS), new DateTime(millisE));
  }

  /**
   * 指定された範囲を含むように、このオブジェクトの示す範囲を拡張した範囲を
   * 取得する。
   * <br>
   * 元から指定された範囲を含んでいた場合は、このオブジェクトと同じ範囲を返す。
   *
   * @param  range 範囲。
   * @return 指定された範囲を含むように拡張された範囲。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  public DateTimeRange expandsWith(DateTimeRange range)
  {
    assert (range != null) : "@param:range is null.";

    long millisS = Math.min(startMillis_, range.startMillis_);
    long millisE = Math.max(endMillis_, range.endMillis_);

    return new DateTimeRange(new DateTime(millisS), new DateTime(millisE));
  }
}
