// Copyright 2012 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.api.ads.admanager.lib.utils;

import java.lang.reflect.InvocationTargetException;
import java.util.Calendar;
import java.util.Locale;
import java.util.TimeZone;
import org.apache.commons.beanutils.PropertyUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.Instant;
import org.joda.time.format.ISODateTimeFormat;

/**
 * A utility class for handling {@code DateTime} objects.
 *
 * @param <T> the {@code DateTime} type
 * @param <D> the {@code Date} type
 */
public class DateTimesHelper<T, D> {

  private static final String DATE_PATTERN = "%04d-%02d-%02d";

  private Class<T> dateTimeClass;
  private Class<D> dateClass;

  /**
   * Constructor.
   *
   * @param dateTimeClass the date time class
   * @param dateClass the date class
   */
  public DateTimesHelper(Class<T> dateTimeClass, Class<D> dateClass) {
    this.dateTimeClass = dateTimeClass;
    this.dateClass = dateClass;
  }

  /** Converts a {@code Calendar} object to an API date time preserving the time zone. */
  public T toDateTime(Calendar calendar) {
    return toDateTime(new DateTime(calendar));
  }

  /** Converts an {@code Instant} object to an API date time in the time zone supplied. */
  public T toDateTime(Instant instant, String timeZoneId) {
    return toDateTime(
        instant.toDateTime(DateTimeZone.forTimeZone(TimeZone.getTimeZone(timeZoneId))));
  }

  /** Converts a {@code DateTime} object to an API date time preserving the time zone. */
  public T toDateTime(DateTime dateTime) {
    try {
      D dateObj = dateClass.newInstance();
      PropertyUtils.setProperty(dateObj, "year", dateTime.getYear());
      PropertyUtils.setProperty(dateObj, "month", dateTime.getMonthOfYear());
      PropertyUtils.setProperty(dateObj, "day", dateTime.getDayOfMonth());

      T dateTimeObj = dateTimeClass.newInstance();
      PropertyUtils.setProperty(dateTimeObj, "date", dateObj);
      PropertyUtils.setProperty(dateTimeObj, "hour", dateTime.getHourOfDay());
      PropertyUtils.setProperty(dateTimeObj, "minute", dateTime.getMinuteOfHour());
      PropertyUtils.setProperty(dateTimeObj, "second", dateTime.getSecondOfMinute());
      PropertyUtils.setProperty(dateTimeObj, "timeZoneId", dateTime.getZone().toTimeZone().getID());

      return dateTimeObj;
    } catch (InstantiationException e) {
      throw new IllegalStateException("Could not instantiate class.", e);
    } catch (IllegalAccessException e) {
      throw new IllegalStateException("Could not instantiate class.", e);
    } catch (InvocationTargetException e) {
      throw new IllegalStateException("Could not set field.", e);
    } catch (NoSuchMethodException e) {
      throw new IllegalStateException("Could not set field.", e);
    }
  }

  /**
   * Converts a string in the form of {@code yyyy-MM-dd'T'HH:mm:ss} to an API date time in the time
   * zone supplied.
   */
  public T toDateTime(String dateTime, String timeZoneId) {
    return toDateTime(
        ISODateTimeFormat.dateHourMinuteSecond()
            .parseDateTime(dateTime)
            .withZoneRetainFields(DateTimeZone.forTimeZone(TimeZone.getTimeZone(timeZoneId))));
  }

  /**
   * Converts a string in the form of {@code yyyy-MM-dd'T'HH:mm:ss±HH:mm} to an API date time in the
   * time zone supplied.
   */
  public T toDateTimeWithTimeZone(String dateTime) {
    return toDateTime(
        ISODateTimeFormat.dateTimeParser().withOffsetParsed().parseDateTime(dateTime));
  }

  /**
   * Returns string representation of this date.
   *
   * @param date the date to stringify
   * @return a string representation of the {@code Date} in {@code yyyy-MM-dd}
   */
  public String dateToString(D date) {
    try {
      return String.format(
          DATE_PATTERN,
          (Integer) PropertyUtils.getProperty(date, "year"),
          (Integer) PropertyUtils.getProperty(date, "month"),
          (Integer) PropertyUtils.getProperty(date, "day"));
    } catch (IllegalAccessException e) {
      throw new IllegalStateException("Could not access class.", e);
    } catch (InvocationTargetException e) {
      throw new IllegalStateException("Could not get field.", e);
    } catch (NoSuchMethodException e) {
      throw new IllegalStateException("Could not get field.", e);
    }
  }

  /** Converts an API date time to a {@code DateTime} preserving the time zone. */
  public DateTime toDateTime(T dateTime) {
    try {
      @SuppressWarnings("unchecked") // Expected class.
      D dateObj = (D) PropertyUtils.getProperty(dateTime, "date");
      String timeZoneId = (String) PropertyUtils.getProperty(dateTime, "timeZoneId");
      return new DateTime(
          (Integer) PropertyUtils.getProperty(dateObj, "year"),
          (Integer) PropertyUtils.getProperty(dateObj, "month"),
          (Integer) PropertyUtils.getProperty(dateObj, "day"),
          (Integer) PropertyUtils.getProperty(dateTime, "hour"),
          (Integer) PropertyUtils.getProperty(dateTime, "minute"),
          (Integer) PropertyUtils.getProperty(dateTime, "second"),
          0,
          DateTimeZone.forTimeZone(TimeZone.getTimeZone(timeZoneId)));
    } catch (IllegalAccessException e) {
      throw new IllegalStateException("Could not access class.", e);
    } catch (InvocationTargetException e) {
      throw new IllegalStateException("Could not get field.", e);
    } catch (NoSuchMethodException e) {
      throw new IllegalStateException("Could not get field.", e);
    }
  }

  /** Gets a calendar for a {@code DateTime} using the default locale, i.e. Locale.getDefault(). */
  public Calendar toCalendar(T dateTime) {
    return toDateTime(dateTime).toCalendar(Locale.getDefault());
  }

  /** Gets a calendar for a {@code DateTime} in the supplied locale. */
  public Calendar toCalendar(T dateTime, Locale locale) {
    return toDateTime(dateTime).toCalendar(locale);
  }

  /**
   * Returns string representation of this date time. The string representation does not include the
   * time zone since using date times for filtering does not use the time zone. If you need to
   * convert the date time into another time zone before filtering on it, please use {@link
   * #toStringForTimeZone(Object, String)} instead.
   *
   * @param dateTime the date time to stringify
   * @return a string representation of the {@code DateTime} in {@code yyyy-MM-dd'T'HH:mm:ss}
   */
  public String toString(T dateTime) {
    return toDateTime(dateTime).toString(ISODateTimeFormat.dateHourMinuteSecond());
  }

  /**
   * Returns string representation of this date time with time zone. If you need to convert the date
   * time into another time zone before filtering on it, please use {@link
   * #toStringForTimeZone(Object, String)} instead.
   *
   * @param dateTime the date time to stringify
   * @return a string representation of the {@code DateTime} in {@code yyyy-MM-dd'T'HH:mm:ss±HH:mm},
   *     i.e. {@code 2013-09-013T12:02:03+08:00}
   */
  public String toStringWithTimeZone(T dateTime) {
    return toDateTime(dateTime).toString(ISODateTimeFormat.dateTimeNoMillis());
  }

  /**
   * Returns string representation of this date time with a different time zone, preserving the
   * millisecond instant.
   *
   * <p>This method is useful for finding the local time in another time zone, especially for
   * filtering.
   *
   * <p>For example, if this date time holds 12:30 in Europe/London, the result from this method
   * with Europe/Paris would be 13:30. You may also want to use this with your network's time zone,
   * i.e.
   *
   * <pre><code> String timeZoneId = networkService.getCurrentNetwork().getTimeZone();
   * String statementPart =
   *     "startDateTime > "
   *         + DateTimes.toString(apiDateTime, timeZoneId);
   * //...
   * statementBuilder.where(statementPart);
   * </code></pre>
   *
   * This method is in the same style of {@link DateTime#withZone(org.joda.time.DateTimeZone)}.
   *
   * @param dateTime the date time to stringify into a new time zone
   * @param newZoneID the time zone ID of the new zone
   * @return a string representation of the {@code DateTime} in {@code yyyy-MM-dd'T'HH:mm:ss}
   */
  public String toStringForTimeZone(T dateTime, String newZoneID) {
    return toDateTime(dateTime)
        .withZone(DateTimeZone.forTimeZone(TimeZone.getTimeZone(newZoneID)))
        .toString(ISODateTimeFormat.dateHourMinuteSecond());
  }
}
