/*
******************************************************************************
* Copyright (C) 2007-2011, International Business Machines Corporation and   *
* others. All Rights Reserved.                                               *
******************************************************************************
*/

package com.ibm.icu.impl.duration;

import java.util.TimeZone;

import com.ibm.icu.impl.duration.impl.DataRecord;
import com.ibm.icu.impl.duration.impl.PeriodFormatterData;
import com.ibm.icu.impl.duration.impl.PeriodFormatterDataService;

/**
 * Default implementation of PeriodBuilderFactory.  This creates builders that
 * use approximate durations.
 */
class BasicPeriodBuilderFactory implements PeriodBuilderFactory {
  private PeriodFormatterDataService ds;
  private Settings settings;
  
  private static final short allBits = 0xff;
  
  BasicPeriodBuilderFactory(PeriodFormatterDataService ds) {
    this.ds = ds;
    this.settings = new Settings();
  }

  static long approximateDurationOf(TimeUnit unit) {
    return TimeUnit.approxDurations[unit.ordinal];
  }

  class Settings {
    boolean inUse;
    short uset = allBits;
    TimeUnit maxUnit = TimeUnit.YEAR;
    TimeUnit minUnit = TimeUnit.MILLISECOND;
    int maxLimit;
    int minLimit;
    boolean allowZero = true;
    boolean weeksAloneOnly;
    boolean allowMillis = true;

    Settings setUnits(int uset) {
      if (this.uset == uset) {
        return this;
      }
      Settings result = inUse ? copy() : this;
        
      result.uset = (short)uset;

      if ((uset & allBits) == allBits) {
        result.uset = allBits;
        result.maxUnit = TimeUnit.YEAR;
        result.minUnit = TimeUnit.MILLISECOND;
      } else {
        int lastUnit = -1;
        for (int i = 0; i < TimeUnit.units.length; ++i) {
          if (0 != (uset & (1 << i))) {
            if (lastUnit == -1) {
              result.maxUnit = TimeUnit.units[i];
            }
            lastUnit = i;
          }
        }
        if (lastUnit == -1) { 
            // currently empty, but this might be transient so no fail
            result.minUnit = result.maxUnit = null;
        } else {
            result.minUnit = TimeUnit.units[lastUnit];
        }
      }

      return result;
    }

    short effectiveSet() {
      if (allowMillis) {
        return uset;
      }
      return (short)(uset & ~(1 << TimeUnit.MILLISECOND.ordinal));
    }
    
    TimeUnit effectiveMinUnit() {
        if (allowMillis || minUnit != TimeUnit.MILLISECOND) {
            return minUnit;
        }
        // -1 to skip millisecond
        for (int i = TimeUnit.units.length - 1; --i >= 0;) {
            if (0 != (uset & (1 << i))) {
                return TimeUnit.units[i];
            }
        }
        return TimeUnit.SECOND; // default for pathological case
    }
    
    Settings setMaxLimit(float maxLimit) {
      int val = maxLimit <= 0 ? 0 : (int)(maxLimit*1000);
      if (maxLimit == val) {
        return this;
      }
      Settings result = inUse ? copy() : this;
      result.maxLimit = val;
      return result;
    }

    Settings setMinLimit(float minLimit) {
      int val = minLimit <= 0 ? 0 : (int)(minLimit*1000);
      if (minLimit == val) {
        return this;
      }
      Settings result = inUse ? copy() : this;
      result.minLimit = val;
      return result;
    }

    Settings setAllowZero(boolean allow) {
      if (this.allowZero == allow) {
        return this;
      }
      Settings result = inUse ? copy() : this;
      result.allowZero = allow;
      return result;
    }

    Settings setWeeksAloneOnly(boolean weeksAlone) {
      if (this.weeksAloneOnly == weeksAlone) {
        return this;
      }
      Settings result = inUse ? copy() : this;
      result.weeksAloneOnly = weeksAlone;
      return result;
    }

    Settings setAllowMilliseconds(boolean allowMillis) {
      if (this.allowMillis == allowMillis) {
        return this;
      }
      Settings result = inUse ? copy() : this;
      result.allowMillis = allowMillis;
      return result;
    }

    Settings setLocale(String localeName) {
      PeriodFormatterData data = ds.get(localeName);
      return this
        .setAllowZero(data.allowZero())
        .setWeeksAloneOnly(data.weeksAloneOnly())
        .setAllowMilliseconds(data.useMilliseconds() != DataRecord.EMilliSupport.NO);  
    }

    Settings setInUse() {
      inUse = true;
      return this;
    }

    Period createLimited(long duration, boolean inPast) {
      if (maxLimit > 0) {
          long maxUnitDuration = approximateDurationOf(maxUnit);
          if (duration * 1000 > maxLimit * maxUnitDuration) {
              return Period.moreThan(maxLimit/1000f, maxUnit).inPast(inPast);
          }
      }
      
      if (minLimit > 0) {
          TimeUnit emu = effectiveMinUnit();
          long emud = approximateDurationOf(emu);
          long eml = (emu == minUnit) ? minLimit :
              Math.max(1000, (approximateDurationOf(minUnit) * minLimit) / emud);
          if (duration * 1000 < eml * emud) {
              return Period.lessThan(eml/1000f, emu).inPast(inPast);
          }
      }
      return null;
    }

    public Settings copy() {
        Settings result = new Settings();
        result.inUse = inUse;
        result.uset = uset;
        result.maxUnit = maxUnit;
        result.minUnit = minUnit;
        result.maxLimit = maxLimit;
        result.minLimit = minLimit;
        result.allowZero = allowZero;
        result.weeksAloneOnly = weeksAloneOnly;
        result.allowMillis = allowMillis;
        return result;
    }
  }

  public PeriodBuilderFactory setAvailableUnitRange(TimeUnit minUnit,
                                                    TimeUnit maxUnit) {
    int uset = 0;
    for (int i = maxUnit.ordinal; i <= minUnit.ordinal; ++i) {
        uset |= 1 << i;
    }
    if (uset == 0) {
        throw new IllegalArgumentException("range " + minUnit + " to " + maxUnit + " is empty");
    }
    settings = settings.setUnits(uset);
    return this;
  }

  public PeriodBuilderFactory setUnitIsAvailable(TimeUnit unit, 
                                                      boolean available) {
    int uset = settings.uset;
    if (available) {
      uset |= 1 << unit.ordinal;
    } else {
      uset &= ~(1 << unit.ordinal);
    }
    settings = settings.setUnits(uset);
    return this;
  }

  public PeriodBuilderFactory setMaxLimit(float maxLimit) {
    settings = settings.setMaxLimit(maxLimit);
    return this;
  }

  public PeriodBuilderFactory setMinLimit(float minLimit) {
    settings = settings.setMinLimit(minLimit);
    return this;
  }

  public PeriodBuilderFactory setAllowZero(boolean allow) {
    settings = settings.setAllowZero(allow);
    return this;
  }

  public PeriodBuilderFactory setWeeksAloneOnly(boolean aloneOnly) {
    settings = settings.setWeeksAloneOnly(aloneOnly);
    return this;
  }

  public PeriodBuilderFactory setAllowMilliseconds(boolean allow) {
    settings = settings.setAllowMilliseconds(allow);
    return this;
  }

  public PeriodBuilderFactory setLocale(String localeName) {
    settings = settings.setLocale(localeName);
    return this;
  }
  
  public PeriodBuilderFactory setTimeZone(TimeZone timeZone) {
      // ignore this
      return this;
  }

  private Settings getSettings() {
    if (settings.effectiveSet() == 0) {
      return null;
    }
    return settings.setInUse();
  }

  /**
   * Return a builder that represents relative time in terms of the single
   * given TimeUnit
   *
   * @param unit the single TimeUnit with which to represent times
   * @return a builder
   */
  public PeriodBuilder getFixedUnitBuilder(TimeUnit unit) {
    return FixedUnitBuilder.get(unit, getSettings());
  }

  /**
   * Return a builder that represents relative time in terms of the
   * largest period less than or equal to the duration.
   *
   * @return a builder
   */
  public PeriodBuilder getSingleUnitBuilder() {
    return SingleUnitBuilder.get(getSettings());
  }

  /**
   * Return a builder that formats the largest one or two periods,
   * Starting with the largest period less than or equal to the duration.
   * It formats two periods if the first period has a count &lt; 2
   * and the next period has a count &gt;= 1.
   *
   * @return a builder
   */
  public PeriodBuilder getOneOrTwoUnitBuilder() {
    return OneOrTwoUnitBuilder.get(getSettings());
  }

  /**
   * Return a builder that formats the given number of periods,
   * starting with the largest period less than or equal to the
   * duration.
   *
   * @return a builder
   */
  public PeriodBuilder getMultiUnitBuilder(int periodCount) {
    return MultiUnitBuilder.get(periodCount, getSettings());
  }
}

abstract class PeriodBuilderImpl implements PeriodBuilder {

  protected BasicPeriodBuilderFactory.Settings settings;
 
  public Period create(long duration) {
    return createWithReferenceDate(duration, System.currentTimeMillis());
  }

  public long approximateDurationOf(TimeUnit unit) {
    return BasicPeriodBuilderFactory.approximateDurationOf(unit);
  }

  public Period createWithReferenceDate(long duration, long referenceDate) {
    boolean inPast = duration < 0;
    if (inPast) {
      duration = -duration;
    }
    Period ts = settings.createLimited(duration, inPast);
    if (ts == null) {
      ts = handleCreate(duration, referenceDate, inPast);
      if (ts == null) {
        ts = Period.lessThan(1, settings.effectiveMinUnit()).inPast(inPast);
      }
    }
    return ts;
  }

  public PeriodBuilder withTimeZone(TimeZone timeZone) {
      // ignore the time zone
      return this;
  }

  public PeriodBuilder withLocale(String localeName) {
    BasicPeriodBuilderFactory.Settings newSettings = settings.setLocale(localeName);
    if (newSettings != settings) {
      return withSettings(newSettings);
    }
    return this;
  }

  protected abstract PeriodBuilder withSettings(BasicPeriodBuilderFactory.Settings settingsToUse);

  protected abstract Period handleCreate(long duration, long referenceDate, 
                                         boolean inPast);

  protected PeriodBuilderImpl(BasicPeriodBuilderFactory.Settings settings) {
    this.settings = settings;
  }
}

class FixedUnitBuilder extends PeriodBuilderImpl {
  private TimeUnit unit;
  
  public static FixedUnitBuilder get(TimeUnit unit, BasicPeriodBuilderFactory.Settings settingsToUse) {
    if (settingsToUse != null && (settingsToUse.effectiveSet() & (1 << unit.ordinal)) != 0) {
      return new FixedUnitBuilder(unit, settingsToUse);
    }
    return null;
  }

  FixedUnitBuilder(TimeUnit unit, BasicPeriodBuilderFactory.Settings settings) {
    super(settings);
    this.unit = unit;
  }

  protected PeriodBuilder withSettings(BasicPeriodBuilderFactory.Settings settingsToUse) {
    return get(unit, settingsToUse);
  }

  protected Period handleCreate(long duration, long referenceDate, 
                                boolean inPast) {
    if (unit == null) {
      return null;
    }
    long unitDuration = approximateDurationOf(unit);
    return Period.at((float)((double)duration/unitDuration), unit)
        .inPast(inPast);
  }
}

class SingleUnitBuilder extends PeriodBuilderImpl {
  SingleUnitBuilder(BasicPeriodBuilderFactory.Settings settings) {
    super(settings);
  }

  public static SingleUnitBuilder get(BasicPeriodBuilderFactory.Settings settings) {
    if (settings == null) {
      return null;
    }
    return new SingleUnitBuilder(settings);
  }

  protected PeriodBuilder withSettings(BasicPeriodBuilderFactory.Settings settingsToUse) {
    return SingleUnitBuilder.get(settingsToUse);
  }

  protected Period handleCreate(long duration, long referenceDate, 
                                boolean inPast) {
    short uset = settings.effectiveSet();
    for (int i = 0; i < TimeUnit.units.length; ++i) {
      if (0 != (uset & (1 << i))) {
        TimeUnit unit = TimeUnit.units[i];
        long unitDuration = approximateDurationOf(unit);
        if (duration >= unitDuration) {
          return Period.at((float)((double)duration/unitDuration), unit)
              .inPast(inPast);
        }
      }
    }
    return null;
  }
}

class OneOrTwoUnitBuilder extends PeriodBuilderImpl {
  OneOrTwoUnitBuilder(BasicPeriodBuilderFactory.Settings settings) {
    super(settings);
  }

  public static OneOrTwoUnitBuilder get(BasicPeriodBuilderFactory.Settings settings) {
    if (settings == null) {
      return null;
    }
    return new OneOrTwoUnitBuilder(settings);
  }

  protected PeriodBuilder withSettings(BasicPeriodBuilderFactory.Settings settingsToUse) {
    return OneOrTwoUnitBuilder.get(settingsToUse);
  }

  protected Period handleCreate(long duration, long referenceDate, 
                                boolean inPast) {
    Period period = null;
    short uset = settings.effectiveSet();
    for (int i = 0; i < TimeUnit.units.length; ++i) {
      if (0 != (uset & (1 << i))) {
        TimeUnit unit = TimeUnit.units[i];
        long unitDuration = approximateDurationOf(unit);
        if (duration >= unitDuration || period != null) {
          double count = (double)duration/unitDuration;
          if (period == null) {
            if (count >= 2) {
              period = Period.at((float)count, unit);
              break;
            }
            period = Period.at(1, unit).inPast(inPast);
            duration -= unitDuration;
          } else {
            if (count >= 1) {
              period = period.and((float)count, unit);
            }
            break;
          }
        }
      }
    }
    return period;
  }
}

class MultiUnitBuilder extends PeriodBuilderImpl {
  private int nPeriods;

  MultiUnitBuilder(int nPeriods, BasicPeriodBuilderFactory.Settings settings) {
    super(settings);
    this.nPeriods = nPeriods;
  }

  public static MultiUnitBuilder get(int nPeriods, BasicPeriodBuilderFactory.Settings settings) {
    if (nPeriods > 0 && settings != null) {
      return new MultiUnitBuilder(nPeriods, settings);
    }
    return null;
  }

  protected PeriodBuilder withSettings(BasicPeriodBuilderFactory.Settings settingsToUse) {
    return MultiUnitBuilder.get(nPeriods, settingsToUse);
  }

  protected Period handleCreate(long duration, long referenceDate, 
                                boolean inPast) {
    Period period = null;
    int n = 0;
    short uset = settings.effectiveSet();
    for (int i = 0; i < TimeUnit.units.length; ++i) {
      if (0 != (uset & (1 << i))) {
        TimeUnit unit = TimeUnit.units[i];
        if (n == nPeriods) {
          break;
        }
        long unitDuration = approximateDurationOf(unit);
        if (duration >= unitDuration || n > 0) {
          ++n;
          double count = (double)duration / unitDuration;
          if (n < nPeriods) {
            count = Math.floor(count);
            duration -= (long)(count * unitDuration);
          }
          if (period == null) {
            period = Period.at((float)count, unit).inPast(inPast);
          } else {
            period = period.and((float)count, unit);
          }
        }
      }
    }
    return period;
  }
}

