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

package com.ibm.icu.impl.duration;

import com.ibm.icu.impl.duration.BasicPeriodFormatterFactory.Customizations;
import com.ibm.icu.impl.duration.impl.DataRecord.ECountVariant;
import com.ibm.icu.impl.duration.impl.DataRecord.EMilliSupport;
import com.ibm.icu.impl.duration.impl.DataRecord.ESeparatorVariant;
import com.ibm.icu.impl.duration.impl.DataRecord.ETimeDirection;
import com.ibm.icu.impl.duration.impl.DataRecord.ETimeLimit;
import com.ibm.icu.impl.duration.impl.PeriodFormatterData;

/**
 * Core implementation class for PeriodFormatter.
 */
class BasicPeriodFormatter implements PeriodFormatter {
  private BasicPeriodFormatterFactory factory;
  private String localeName;
  private PeriodFormatterData data;
  private Customizations customs;

  BasicPeriodFormatter(BasicPeriodFormatterFactory factory, 
                       String localeName,
                       PeriodFormatterData data, 
                       Customizations customs) {
    this.factory = factory;
    this.localeName = localeName;
    this.data = data;
    this.customs = customs;
  }

  public String format(Period period) {
    if (!period.isSet()) {
      throw new IllegalArgumentException("period is not set");
    }
    return format(period.timeLimit, period.inFuture, period.counts);
  }

  public PeriodFormatter withLocale(String locName) {
    if (!this.localeName.equals(locName)) {
      PeriodFormatterData newData = factory.getData(locName);
      return new BasicPeriodFormatter(factory, locName, newData, 
                                      customs);
    }
    return this;
  }

  private String format(int tl, boolean inFuture, int[] counts) {
    int mask = 0;
    for (int i = 0; i < counts.length; ++i) {
      if (counts[i] > 0) {
        mask |= 1 << i;
      }
    }

    // if the data does not allow formatting of zero periods, 
    // remove these from consideration.  If the result has no
    // periods set, return null to indicate we could not format
    // the duration.
    if (!data.allowZero()) {
      for (int i = 0, m = 1; i < counts.length; ++i, m <<= 1) {
        if ((mask & m) != 0 && counts[i] == 1) {
          mask &= ~m;
        }
      }
      if (mask == 0) {
        return null;
      }
    }

    // if the data does not allow milliseconds but milliseconds are
    // set, merge them with seconds and force display of seconds to
    // decimal with 3 places.
    boolean forceD3Seconds = false;
    if (data.useMilliseconds() != EMilliSupport.YES && 
        (mask & (1 << TimeUnit.MILLISECOND.ordinal)) != 0) {
      int sx = TimeUnit.SECOND.ordinal;
      int mx = TimeUnit.MILLISECOND.ordinal;
      int sf = 1 << sx;
      int mf = 1 << mx;
      switch (data.useMilliseconds()) {      
        case EMilliSupport.WITH_SECONDS: {
          // if there are seconds, merge with seconds, otherwise leave alone
          if ((mask & sf) != 0) {
            counts[sx] += (counts[mx]-1)/1000;
            mask &= ~mf;
            forceD3Seconds = true;
          }
        } break;
        case EMilliSupport.NO: {
          // merge with seconds, reset seconds before use just in case
          if ((mask & sf) == 0) {
            mask |= sf;
            counts[sx] = 1;
          }
          counts[sx] += (counts[mx]-1)/1000;
          mask &= ~mf;
          forceD3Seconds = true;
        } break;
      }
    }

    // get the first and last units that are set.
    int first = 0;
    int last = counts.length - 1;
    while (first < counts.length && (mask & (1 << first)) == 0) ++first;
    while (last > first && (mask & (1 << last)) == 0) --last;

    // determine if there is any non-zero unit
    boolean isZero = true;
    for (int i = first; i <= last; ++i) {
      if (((mask & (1 << i)) != 0) &&  counts[i] > 1) {
        isZero = false;
        break;
      }
    }

    StringBuffer sb = new StringBuffer();

    // if we've been requested to not display a limit, or there are
    // no non-zero units, do not display the limit.
    if (!customs.displayLimit || isZero) {
      tl = ETimeLimit.NOLIMIT;
    }

    // if we've been requested to not display the direction, or there
    // are no non-zero units, do not display the direction.
    int td;
    if (!customs.displayDirection || isZero) {
      td = ETimeDirection.NODIRECTION;
    } else {
      td = inFuture ? ETimeDirection.FUTURE : ETimeDirection.PAST;
    }

    // format the initial portion of the string before the units.
    // record whether we need to use a digit prefix (because the
    // initial portion forces it)
    boolean useDigitPrefix = data.appendPrefix(tl, td, sb);

    // determine some formatting params and initial values
    boolean multiple = first != last;
    boolean wasSkipped = true; // no initial skip marker
    boolean skipped = false;
    boolean countSep = customs.separatorVariant != ESeparatorVariant.NONE;

    // loop for formatting the units
    for (int i = first, j = i; i <= last; i = j) {
      if (skipped) {
        // we didn't format the previous unit
        data.appendSkippedUnit(sb);
        skipped = false;
        wasSkipped = true;
      }

      while (++j < last && (mask & (1 << j)) == 0) {
        skipped = true; // skip
      }

      TimeUnit unit = TimeUnit.units[i];
      int count = counts[i] - 1;

      int cv = customs.countVariant;
      if (i == last) {
        if (forceD3Seconds) {
          cv = ECountVariant.DECIMAL3;
        }
        // else leave unchanged
      } else {
        cv = ECountVariant.INTEGER;
      }
      boolean isLast = i == last;
      boolean mustSkip = data.appendUnit(unit, count, cv, customs.unitVariant, 
                                         countSep, useDigitPrefix, multiple, isLast, wasSkipped, sb);
      skipped |= mustSkip;
      wasSkipped = false;

      if (customs.separatorVariant != ESeparatorVariant.NONE && j <= last) {
        boolean afterFirst = i == first;
        boolean beforeLast = j == last;
        boolean fullSep = customs.separatorVariant == ESeparatorVariant.FULL;
        useDigitPrefix = data.appendUnitSeparator(unit, fullSep, afterFirst, beforeLast, sb);
      } else {
        useDigitPrefix = false;
      }
    }
    data.appendSuffix(tl, td, sb);

    return sb.toString();
  }
}
