package OpenRate.utils;

import OpenRate.CommonConfig;
import OpenRate.resource.ConversionCache;
import OpenRate.resource.ResourceContext;
import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;

/**
 * This class offers conversion and formatting methods (primarily for dates), so
 * that we have a simple single set conversion handling methods.
 *
 * @author ian
 */
public class ConversionUtils
{
  // This is the format used for converting dates on input
  private String InputDateFormat = CommonConfig.OR_DEFAULT_DATE_FORMAT;

  // This is the format used for converting dates on input
  private String OutputDateFormat = CommonConfig.OR_DEFAULT_DATE_FORMAT;

  // format used for input
  private SimpleDateFormat sdfIn = null;

  // format used for output
  private SimpleDateFormat sdfOut = null;

  // set that we are using integer (UTC) format
  private boolean integerFormat = false;

  // set that we are using long format (UTC + ms)
  private boolean longFormat = false;

  // set that we are using long format (UTC + ms)
  private boolean stringFormat = true;

  // used for general date manipulation
  private final Calendar cal;

  // Used to cache access to the conversion objects
  private static ConversionCache tmpConvCache = null;

  // Singleton instance
  private static ConversionUtils convUtilsObj;

 /**
  * constructor - initialise the internal formatting object
  */
  public ConversionUtils()
  {
    // This is the date format we are using for the output
    sdfOut = new SimpleDateFormat(OutputDateFormat);
    sdfOut.setLenient(false);

    // This is the date format we are using for the input
    sdfIn = new SimpleDateFormat(InputDateFormat);
    sdfIn.setLenient(false);

    // used for date manipulation
    cal = new GregorianCalendar();
  }

 /**
  * Get the access to the singleton conversion cache
  *
  * @return Conversion Cache Reference
  */
  public static ConversionCache getConversionCache()
  {
    if (tmpConvCache == null)
    {
      // Get access to the conversion cache
      ResourceContext ctx    = new ResourceContext();

      // try the new Logging model.
      tmpConvCache = (ConversionCache) ctx.get(ConversionCache.RESOURCE_KEY);
    }

    return tmpConvCache;
  }

 /**
  * Get the access to the singleton conversion utils object
  *
  * @return Conversion Cache Reference
  */
  public static ConversionUtils getConversionUtilsObject()
  {
    if(convUtilsObj == null)
    {
      convUtilsObj = new ConversionUtils();
    }

    return convUtilsObj;
  }

//------------------------------------------------------------------------------
//---------------------------- Date Utilities ----------------------------------
//------------------------------------------------------------------------------

 /**
  * Convert an amorphic string to a UTC date representation for input
  *
  * @param amorphicDate the amorphic date string to read and convert
  * @return The short UTC date format
  * @throws ParseException
  */
  public long convertInputDateToUTC(String amorphicDate) throws ParseException
  {
    long tmpUTCDate = 0;

    if (integerFormat)
    {
      return Integer.parseInt(amorphicDate);
    }
    else if (longFormat)
    {
      return Long.parseLong(amorphicDate) / 1000;
    }
    else if (stringFormat)
    {
      if (amorphicDate == null)
      {
        return 0;
      }
      else
      {
        tmpUTCDate = sdfIn.parse(amorphicDate).getTime() / 1000;
      }
    }

    return tmpUTCDate;
  }

 /**
  * This converts the UTC date into day of week
  *
  * @param UTCDateValue The long UTC date the convert
  * @return The integer day of the week
  */
  public int getDayOfWeek(long UTCDateValue)
  {
    cal.setTimeInMillis(UTCDateValue*1000);
    return cal.get(Calendar.DAY_OF_WEEK);
  }

 /**
  * Utility function to get the minute of the day from a long UTC value
  *
  * @param UTCDateValue The long UTC date the convert
  * @return The minute of the day
  */
  public int getMinuteOfDay(long UTCDateValue)
  {
    cal.setTimeInMillis(UTCDateValue*1000);
    return cal.get(Calendar.HOUR_OF_DAY) * 60 + cal.get(Calendar.MINUTE);
  }

 /**
  * Sets the standard overall date format for input to a new value for this
  * converter. The allowed values here are:
  * "Integer" - take a UTC date (without milliseconds),
  * "Long" - take a UTC date with milliseconds
  * Other - any other value that can be treated as a date string using the
  * SimpleDateFormat formatting strings.
  *
  * @param newFormat The new format to adopt
  * @return True if the change was successful otherwise false
  */
  public boolean setInputDateFormat(String newFormat)
  {
    if (newFormat.equalsIgnoreCase("integer"))
    {
      // set UTC format
      integerFormat = true;
      longFormat = false;
      stringFormat = false;
    }
    else if (newFormat.equalsIgnoreCase("long"))
    {
      // set UTC long format with milliseconds
      integerFormat = false;
      longFormat = true;
      stringFormat = false;
    }
    else
    {
      sdfIn = new SimpleDateFormat(newFormat);

      // set a string format
      integerFormat = false;
      longFormat = false;
      stringFormat = true;
    }

    // set the input date format
    InputDateFormat = newFormat;

    return true;
  }

 /**
  * Gets the standard overall date format for input
  *
  * @return The current date format string
  */
  public String getInputDateFormat()
  {
    return InputDateFormat;
  }

 /**
  * Sets the standard overall date format for input to a new value for this
  * converter. The allowed values here are:
  * "Integer" - take a UTC date (without milliseconds),
  * "Long" - take a UTC date with milliseconds
  * Other - any other value that can be treated as a date string using the
  * SimpleDateFormat formatting strings.
  *
  * @param newFormat The new format to adopt
  * @return True if the change was successful otherwise false
  */
  public boolean setOutputDateFormat(String newFormat)
  {
    if (newFormat.equalsIgnoreCase("integer"))
    {
      // set UTC format
      integerFormat = true;
      longFormat = false;
      stringFormat = false;
    }
    else if (newFormat.equalsIgnoreCase("long"))
    {
      // set UTC long format with milliseconds
      integerFormat = false;
      longFormat = true;
      stringFormat = false;
    }
    else
    {
      sdfOut = new SimpleDateFormat(newFormat);

      // set a string format
      integerFormat = false;
      longFormat = false;
      stringFormat = true;
    }

    // set the input date format
    OutputDateFormat = newFormat;

    return true;
  }

 /**
  * Gets the standard overall date format for output
  *
  * @return The current date format string
  */
  public String getOutputDateFormat()
  {
    return OutputDateFormat;
  }

 /**
  * This formats a date given as a long into a standard string format
  *
  * @param dateToFormat The long date
  * @return The formatted date string
  */
  public String formatLongDate(long dateToFormat)
  {
    return sdfOut.format(new Date(dateToFormat*1000));
  }

 /**
  * This formats a date given as a Date into a standard string format
  *
  * @param dateToFormat The date value to format
  * @return The formatted date string
  */
  public String formatLongDate(Date dateToFormat)
  {
    if (dateToFormat == null)
    {
      return "Null Date";
    }
    else
    {
      return sdfOut.format(dateToFormat);
    }
  }

 /**
  * Converts a string in the standard date format to a date
  *
  * @param DateToFormat The date in the string format
  * @return The converted dagte
  * @throws java.text.ParseException
  */
  public Date getDatefromLongFormat(String DateToFormat) throws ParseException
  {
    Date tmpDate = sdfIn.parse(DateToFormat);

    return tmpDate;
  }

  /**
   * This function converts a date to a GMT date
   *
   * @param date The date to convert
   * @return The date at GMT
   */
  public Date getGmtDate( Date date )
  {
     TimeZone tz = TimeZone.getDefault();
     Date ret = new Date( date.getTime() - tz.getRawOffset() );

     // if we are now in DST, back off by the delta.  Note that we are
     // checking the GMT date, this is the KEY.
     if ( tz.inDaylightTime( ret ))
     {
        Date dstDate = new Date( ret.getTime() - tz.getDSTSavings() );

        // check to make sure we have not crossed back into standard time
        // this happens when we are on the cusp of DST (7pm the day before
        // the change for PDT)
        if ( tz.inDaylightTime( dstDate ))
        {
           ret = dstDate;
        }
     }

     return ret;
  }

  /**
   * This function sees if a date is in Daylight Savings Time (DST)
   *
   * @param date The date the check
   * @return True if the date is in DST otherwise false
   */
  public boolean getDateInDST( Date date )
  {
     TimeZone tz = TimeZone.getDefault();

     boolean RetValue = tz.inDaylightTime(date);

     return RetValue;
  }

 /**
  * Gets the rounded start date of the month the given event date is in
  *
  * @param EventStartDate The date of the event
  * @return The UTC month start date
  */
  public long getUTCMonthStart(Date EventStartDate)
  {
    Date roundedDate = getMonthStart(EventStartDate);
    cal.setTime(roundedDate);
    long validityMonthStart = cal.getTimeInMillis() / 1000;

    return validityMonthStart;
  }

 /**
  * Gets the rounded end date of the month the given event date is in
  *
  * @param EventStartDate The date of the event
  * @return The UTC month end date
  */
  public long getUTCMonthEnd(Date EventStartDate)
  {
    Date roundedDate = getMonthEnd(EventStartDate);
    cal.setTime(roundedDate);
    long validityMonthEnd = cal.getTimeInMillis() / 1000;

    return validityMonthEnd;
  }

 /**
  * Gets the rounded start date of the month the given event date is in
  *
  * @param EventStartDate The date of the event
  * @return The UTC month start date
  */
  public Date getMonthStart(Date EventStartDate)
  {
    // Get the montly counter validity periods for this CDR
    cal.setTime(EventStartDate);
    cal.set(Calendar.HOUR_OF_DAY,0);
    cal.set(Calendar.MINUTE,0);
    cal.set(Calendar.SECOND,0);
    cal.set(Calendar.MILLISECOND,0);
    cal.set(Calendar.SECOND,0);
    cal.set(Calendar.DATE,1);

    return cal.getTime();
  }

 /**
  * Gets the rounded end date of the month the given event date is in
  *
  * @param EventStartDate The date of the event
  * @return The UTC month end date
  */
  public Date getMonthEnd(Date EventStartDate)
  {
    // Get the montly counter validity periods for this CDR
    cal.setTime(EventStartDate);
    cal.set(Calendar.HOUR_OF_DAY,0);
    cal.set(Calendar.MINUTE,0);
    cal.set(Calendar.SECOND,0);
    cal.set(Calendar.MILLISECOND,0);
    cal.set(Calendar.DATE,1);
    cal.add(Calendar.MONTH,1);
    cal.add(Calendar.SECOND,-1);

    return cal.getTime();
  }

	/**
	 * Gets the rounded start date of the year the given event date is in
	 *
	 * @param EventStartDate
	 *            The date of the event
	 * @return The UTC year start date
	 */
	public long getUTCYearStart(Date EventStartDate) {
		Date roundedDate = getYearStart(EventStartDate);
		cal.setTime(roundedDate);
		long validityYearStart = cal.getTimeInMillis() / 1000;

		return validityYearStart;
	}

	/**
	 * Gets the rounded end date of the year the given event date is in
	 *
	 * @param EventStartDate
	 *            The date of the event
	 * @return The UTC year end date
	 */
	public long getUTCYearEnd(Date EventStartDate) {
		Date roundedDate = getYearEnd(EventStartDate);
		cal.setTime(roundedDate);
		long validityYearEnd = cal.getTimeInMillis() / 1000;

		return validityYearEnd;
	}

	/**
	 * Gets the rounded start date of the year the given event date is in
	 *
	 * @param EventStartDate
	 *            The date of the event
	 * @return The year start date
	 */
	public Date getYearStart(Date EventStartDate) {
		// Get the yearly counter validity periods for this CDR
		cal.setTime(EventStartDate);
		cal.set(Calendar.DAY_OF_YEAR, 1);
		cal.set(Calendar.HOUR_OF_DAY, 0);
		cal.set(Calendar.MINUTE, 0);
		cal.set(Calendar.SECOND, 0);
		cal.set(Calendar.MILLISECOND, 0);

		return cal.getTime();
	}

	/**
	 * Gets the rounded end date of the year the given event date is in
	 *
	 * @param EventStartDate
	 *            The date of the event
	 * @return The year end date
	 */
	public Date getYearEnd(Date EventStartDate) {
		// Get the yearly counter validity periods for this CDR
		cal.setTime(EventStartDate);
		cal.set(Calendar.DAY_OF_YEAR, 366); // 366 for leap year
		cal.set(Calendar.HOUR_OF_DAY, 0);
		cal.set(Calendar.MINUTE, 0);
		cal.set(Calendar.SECOND, 0);
		cal.set(Calendar.MILLISECOND, 0);
		cal.add(Calendar.SECOND, -1);

		return cal.getTime();
	}
  
 /**
  * Gets the rounded start date of the month the given event date is in
  *
  * @param EventStartDate The date of the event
  * @return The UTC month start date
  */
  public long getUTCDayStart(Date EventStartDate)
  {
    Date roundedDate = getDayStart(EventStartDate);
    cal.setTime(roundedDate);
    long validityDayStart = cal.getTimeInMillis() / 1000;

    return validityDayStart;
  }

 /**
  * Gets the rounded start date of the month the given event date is in
  *
  * @param EventStartDate The date of the event
  * @param offset The number of days to offset by
  * @return The UTC month start date
  */
  public long getUTCDayStart(Date EventStartDate, int offset)
  {
    Date roundedDate = getDayEnd(EventStartDate,offset);
    cal.setTime(roundedDate);
    long validityDayStart = cal.getTimeInMillis() / 1000;

    return validityDayStart;
  }

 /**
  * Gets the UTC date of the given event date
  *
  * @param EventStartDate The date of the event
  * @return The UTC representation of the date
  */
  public long getUTCDate(Date EventStartDate)
  {
    // Get the montly counter validity periods for this CDR
    cal.setTime(EventStartDate);
    long validityDayStart = cal.getTimeInMillis() / 1000;

    return validityDayStart;
  }

 /**
  * Gets the Java date of the given event date from the UTC Event Date
  *
  * @param EventStartDate The UTC date of the event
  * @return The start date
  */
  public Date getDateFromUTC(long EventStartDate)
  {
    // Get the montly counter validity periods for this CDR
    cal.setTimeInMillis(EventStartDate*1000);

    return cal.getTime();
  }

 /**
  * Gets the rounded end date of the month the given event date is in
  *
  * @param EventStartDate The date of the event
  * @return The UTC month end date
  */
  public long getUTCDayEnd(Date EventStartDate)
  {
    Date roundedDate = getDayEnd(EventStartDate);
    cal.setTime(roundedDate);

    long validityDayEnd = cal.getTimeInMillis() / 1000;

    return validityDayEnd;
  }

 /**
  * Gets the rounded end date of the month the given event date is in
  *
  * @param EventStartDate The date of the event
  * @param offset The number of days in the future (past) to get the end date for
  * @return The UTC month end date
  */
  public long getUTCDayEnd(Date EventStartDate, int offset)
  {
    Date roundedDate = getDayEnd(EventStartDate,offset);
    cal.setTime(roundedDate);

    long validityDayEnd = cal.getTimeInMillis() / 1000;

    return validityDayEnd;
  }

 /**
  * Gets the rounded start date of the month the given event date is in
  *
  * @param EventStartDate The date of the event
  * @param offset The number of days to offset by
  * @return The UTC month start date
  */
  public Date getDayStart(Date EventStartDate , int offset)
  {
    // Get the montly counter validity periods for this CDR
    cal.setTime(EventStartDate);
    cal.set(Calendar.HOUR_OF_DAY,0);
    cal.set(Calendar.MINUTE,0);
    cal.set(Calendar.SECOND,0);
    cal.set(Calendar.MILLISECOND,0);
    cal.add(Calendar.DAY_OF_MONTH,offset);

    return cal.getTime();
  }

 /**
  * Gets the rounded end date of the month the given event date is in
  *
  * @param EventStartDate The date of the event
  * @param offset The number of days in the future (past) to get the end date for
  * @return The UTC month end date
  */
  public Date getDayEnd(Date EventStartDate, int offset)
  {
    // Get the montly counter validity periods for this CDR
    cal.setTime(EventStartDate);
    cal.set(Calendar.HOUR_OF_DAY,0);
    cal.set(Calendar.MINUTE,0);
    cal.set(Calendar.SECOND,0);
    cal.set(Calendar.MILLISECOND,0);
    cal.add(Calendar.DATE,1);
    cal.add(Calendar.SECOND,-1);
    cal.add(Calendar.DAY_OF_MONTH,offset);

    return cal.getTime();
  }

 /**
  * Gets the rounded start date of the month the given event date is in
  *
  * @param EventStartDate The date of the event
  * @return The UTC month start date
  */
  public Date getDayStart(Date EventStartDate )
  {
    // Get the montly counter validity periods for this CDR
    cal.setTime(EventStartDate);
    cal.set(Calendar.HOUR_OF_DAY,0);
    cal.set(Calendar.MINUTE,0);
    cal.set(Calendar.SECOND,0);
    cal.set(Calendar.MILLISECOND,0);

    return cal.getTime();
  }

 /**
  * Gets the rounded end date of the month the given event date is in
  *
  * @param EventStartDate The date of the event
  * @return The UTC month end date
  */
  public Date getDayEnd(Date EventStartDate)
  {
    // Get the montly counter validity periods for this CDR
    cal.setTime(EventStartDate);
    cal.set(Calendar.HOUR_OF_DAY,0);
    cal.set(Calendar.MINUTE,0);
    cal.set(Calendar.SECOND,0);
    cal.set(Calendar.MILLISECOND,0);
    cal.add(Calendar.DATE,1);
    cal.add(Calendar.SECOND,-1);

    return cal.getTime();
  }

 /**
  * Return the current timestamp
  *
  * @return the current UTC date in millseconds
  */
  public long getCurrentUTCms()
  {
    return Calendar.getInstance().getTimeInMillis();
  }

 /**
  * Return the current timestamp
  *
  * @return the current UTC date in seconds
  */
  public long getCurrentUTC()
  {
    return getCurrentUTCms() / 1000;
  }

 /**
   * Add seconds to a date to get a new date
   *
   * @param inputDate The date to adjust
   * @param duration The number of seconds offset
   * @return The adjusted date
   */
  public Date addDateSeconds(Date inputDate, int duration)
  {
    cal.setTime(inputDate);
    cal.add(Calendar.SECOND, duration);

    return cal.getTime();
  }

 /**
   * Add seconds to a date to get a new date
   *
   * @param inputDate The date to adjust
   * @param duration The number of seconds offset
   * @return The adjusted date
   */
  public Date addDateSeconds(Date inputDate, long duration)
  {
    cal.setTime(inputDate);
    cal.add(Calendar.SECOND, (int) duration);

    return cal.getTime();
  }

 /**
   * Add seconds to a date to get a new date
   *
   * @param inputDate The date to adjust
   * @param duration The number of seconds offset
   * @return The adjusted date
   */
  public Date addDateSeconds(Date inputDate, double duration)
  {
    cal.setTime(inputDate);
    cal.add(Calendar.SECOND, (int) duration);

    return cal.getTime();
  }

//------------------------------------------------------------------------------
//----------------------- Floating Point Utilities -----------------------------
//------------------------------------------------------------------------------

 /**
  * Perform standard rounding on a double value. This rounding rounds UP if
  * the last digit after the last decimal place to round is >=5
  *
  * @param valueToRound The unrounded value
  * @param decimalPlaces The number of decimal places to round to
  * @return The rounded value
  */
  public double getRoundedValue(double valueToRound, int decimalPlaces)
  {
    double helper;
    int    exponent;

    // Because of rounding errors in the floating point, we round to
    // one more decimal place than required
    exponent = (int) Math.pow(10, decimalPlaces);
    helper = Math.round((Math.round(valueToRound*10*exponent) + 5) / 10);

    return helper/exponent;
  }

 /**
  * Perform standard rounding on a double value. This rounding rounds UP to the
  * next largest value (away from zero)
  *
  * @param valueToRound The unrounded value
  * @param decimalPlaces The number of decimal places to round to
  * @return The rounded value
  */
  public double getRoundedValueRoundUp(double valueToRound, int decimalPlaces)
  {
    // Convert input value
    BigDecimal tmpValue = new BigDecimal(valueToRound);
    return tmpValue.setScale(decimalPlaces,BigDecimal.ROUND_UP).doubleValue();
  }

 /**
  * Perform standard rounding on a double value. This rounding rounds DOWN to the
  * next smallest value (towards zero)
  *
  * @param valueToRound The unrounded value
  * @param decimalPlaces The number of decimal places to round to
  * @return The rounded value
  */
  public double getRoundedValueRoundDown(double valueToRound, int decimalPlaces)
  {
    // Convert input value
    BigDecimal tmpValue = new BigDecimal(valueToRound);
    return tmpValue.setScale(decimalPlaces,BigDecimal.ROUND_DOWN).doubleValue();
  }

 /**
  * Perform standard rounding on a double value. This rounding rounds HALF EVEN.
  * Half Even rounding is good for financial applications where the rounding
  * e.g. 2.12345 to 2.1235 would cause a bias over time.
  *
  * @param valueToRound The unrounded value
  * @param decimalPlaces The number of decimal places to round to
  * @return The rounded value
  */
  public double getRoundedValueRoundHalfEven(double valueToRound, int decimalPlaces)
  {
    // Convert input value
    BigDecimal tmpValue = new BigDecimal(valueToRound);
    return tmpValue.setScale(decimalPlaces,BigDecimal.ROUND_HALF_EVEN).doubleValue();
  }

  /**
   * For handling different time zone conversions.
   * 
   * @param newTimeZone The tome zone to set
   */
  public void setTimeZone(TimeZone newTimeZone) {
    cal.setTimeZone(newTimeZone);
  }
  
}