/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You 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.j2objc.util;

import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

/*-[
#import "IOSClass.h"
#import "java/lang/IllegalArgumentException.h"
#import "java/util/GregorianCalendar.h"
#import "java/util/GregorianCalendar.h"
]-*/

/**
 * An NSTimeZone-backed concrete TimeZone implementation that provides daylight saving time (DST)
 * and historical time zone offsets from the native iOS/OS X time zone database.
 *
 * @author Lukhnos Liu
 */
public final class NativeTimeZone extends TimeZone {

  // Constants for quick Gregorian calendar computations; these are only used by {@link
  // #getOffset(int, int, int, int, int, int)} and are adapted from libcore's ZoneInfo. The
  // MILLISECONDS_PER_400_YEARS constant reflects the fact that there are only 97 leap years for
  // every 400 years. See the linked method for a brief summary of the leap year rule.
  private static final long MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
  private static final long MILLISECONDS_PER_400_YEARS =
      MILLISECONDS_PER_DAY * (400 * 365 + 100 - 3);
  private static final long UNIX_OFFSET = 62167219200000L;
  private static final int[] NORMAL = new int[] {
    0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334,
  };
  private static final int[] LEAP = new int[] {
    0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335,
  };

  private final Object nativeTimeZone;
  private final int rawOffset;
  private final int dstSavings;
  private final boolean useDaylightTime;

  static {
    // Observe the native NSSystemTimeZoneDidChangeNotification so that we can flush TimeZone's
    // cached default time zone upon system time zone change.
    setUpTimeZoneDidChangeNotificationHandler();
  }

  public static native String[] getAvailableNativeTimeZoneNames() /*-[
    NSArray *timeZones = [NSTimeZone knownTimeZoneNames];
    return [IOSObjectArray arrayWithNSArray:timeZones type:NSString_class_()];
  ]-*/;


  public static native NativeTimeZone get(String name) /*-[
    return ComGoogleJ2objcUtilNativeTimeZone_fromNativeTimeZoneWithId_(
        [NSTimeZone timeZoneWithName:name]);
  ]-*/;

  public static native NativeTimeZone getDefaultNativeTimeZone() /*-[
    return ComGoogleJ2objcUtilNativeTimeZone_fromNativeTimeZoneWithId_(
        [NSTimeZone defaultTimeZone]);
  ]-*/;

  private static native NativeTimeZone fromNativeTimeZone(Object nativeTimeZone) /*-[
    NSTimeZone *tz = (NSTimeZone *)nativeTimeZone;
    if (!tz) {
      return nil;
    }
    NSDate *now = [NSDate date];
    NSInteger offset = [tz secondsFromGMTForDate:now];
    NSTimeInterval dstOffset = [tz daylightSavingTimeOffsetForDate:now];

    // The DST offset is relative to the current offset, and hence the math here.
    jint rawOffset = (jint)(offset * 1000) - (jint)(dstOffset * 1000.0);

    NSDate *nextTransition = [tz nextDaylightSavingTimeTransitionAfterDate:now];
    jint dstSavings;
    jboolean useDaylightTime;
    if (nextTransition) {
      NSTimeInterval nextDstOffset = [tz daylightSavingTimeOffsetForDate:nextTransition];

      // This is a simplified assumption. Technically, there's nothing in the TZ rules
      // that says you can't have a +1 transition tomorrow, and a +2 the day after. This
      // is why in more modern time libraries, there is no longer the notion of a fixed
      // DST offset.
      NSTimeInterval fixedDstOffset = (dstOffset != 0) ? dstOffset : nextDstOffset;

      // And the offset is always positive regardless the hemisphere the TZ is in.
      dstSavings = fabs(fixedDstOffset) * 1000;
      useDaylightTime = true;
    } else {
      dstSavings = 0;
      useDaylightTime = false;
    }

    return
      create_ComGoogleJ2objcUtilNativeTimeZone_initWithId_withNSString_withInt_withInt_withBoolean_(
        nativeTimeZone, tz.name, rawOffset, dstSavings, useDaylightTime);
  ]-*/;

  private static native void setUpTimeZoneDidChangeNotificationHandler() /*-[
    [[NSNotificationCenter defaultCenter] addObserverForName: NSSystemTimeZoneDidChangeNotification
                                                      object:nil
                                                       queue:nil
                                                  usingBlock:^(NSNotification *note) {
        ComGoogleJ2objcUtilNativeTimeZone_handleTimeZoneChangeWithId_(note);
    }];
  ]-*/;

  private static void handleTimeZoneChange(Object notification) {
    TimeZone.setDefault(null);
  }

  /**
   * Create an NSTimeZone-backed TimeZone instance.
   *
   * @param nativeTimeZone the NSTimeZone instance.
   * @param name the native time zone's name.
   * @param rawOffset the pre-calculated raw offset (in millis) from UTC. When TimeZone was
   *                  designed, the assumption was that the rawOffset would be a constant at
   *                  all times. We pre-compute this offset using the instant when the
   *                  instance is created.
   * @param useDaylightTime whether this time zone observes DST at the moment this instance
   *                        is created.
   */
  private NativeTimeZone(Object nativeTimeZone, String name, int rawOffset, int dstSavings,
                 boolean useDaylightTime) {
    if (name.startsWith("GMT-")) {
      int offsetMillis = rawOffset;
      if (useDaylightTime) {
        offsetMillis += dstSavings;
      }
      setID(createGmtOffsetString(true, true, offsetMillis));
    } else {
      setID(name);
    }
    this.nativeTimeZone = nativeTimeZone;
    this.rawOffset = rawOffset;
    this.dstSavings = dstSavings;
    this.useDaylightTime = useDaylightTime;
  }

  @Override
  public boolean equals(Object obj) {
    if (!(obj instanceof NativeTimeZone)) {
      return false;
    }

    return nativeTimeZone.equals(((NativeTimeZone) obj).nativeTimeZone);
  }

  @Override
  public native int hashCode() /*-[
    return [nativeTimeZone_ hash];
  ]-*/;

  @Override
  public boolean hasSameRules(TimeZone other) {
    if (other instanceof NativeTimeZone) {
      return compareNativeTimeZoneRules(((NativeTimeZone) other).nativeTimeZone);
    }
    return super.hasSameRules(other);
  }

  @Override
  public native int getOffset(long time) /*-[
    double interval = (double)time / 1000.0;
    NSDate *date = [NSDate dateWithTimeIntervalSince1970:interval];
    return (jint)[(NSTimeZone *)nativeTimeZone_ secondsFromGMTForDate:date] * 1000;
  ]-*/;

  /**
   * Used by java.util.GregorianCalendar.
   */
  public native int getOffsetsByUtcTime(long utcTimeInMillis, int[] offsets) /*-[
    NSDate *date = [NSDate dateWithTimeIntervalSince1970:(double)utcTimeInMillis / 1000.0];
    NSTimeZone *tz = (NSTimeZone *)nativeTimeZone_;
    jint totalOffset = (jint)[tz secondsFromGMTForDate:date] * 1000;
    jint dstOffset = (jint)[tz daylightSavingTimeOffsetForDate:date] * 1000;
    jint rawOffset = totalOffset - dstOffset;
    *IOSIntArray_GetRef(offsets, 0) = rawOffset;
    *IOSIntArray_GetRef(offsets, 1) = dstOffset;
    return totalOffset;
  ]-*/;

  /**
   * This implementation is adapted from libcore's ZoneInfo.
   *
   * The method always assumes Gregorian calendar, and uses a simple formula to first derive the
   * instant of the local datetime arguments and then call {@link #getOffset(long)} to get the
   * actual offset. The local datetime used here is always in the non-DST time zone, i.e. the time
   * zone with the "raw" offset, as evidenced by actual JDK implementation and the code below. This
   * means it's possible to call getOffset with a practically non-existent date time, such as 2:30
   * AM, March 13, 2016, which does not exist in US Pacific Time -- it falls in the DST gap of that
   * day.
   *
   * When we compute the milliseconds for the year component, we need to take leap years into
   * consideration. According to http://aa.usno.navy.mil/faq/docs/calendars.php: "The Gregorian leap
   * year rule is: Every year that is exactly divisible by four is a leap year, except for years
   * that are exactly divisible by 100, but these centurial years are leap years if they are exactly
   * divisible by 400. For example, the years 1700, 1800, and 1900 are not leap years, but the year
   * 2000 is." Hence the rules and constants used here.
   *
   * Since this method only supports Gregorian calendar, the return value of any date before October
   * 4, 1582 is not reliable. In addition, the era and dayOfWeek arguments are not used in this
   * method.
   */
  @Override
  public int getOffset(int era, int year, int month, int day, int dayOfWeek, int millis) {
    long calc = (year / 400) * MILLISECONDS_PER_400_YEARS;
    year %= 400;

    calc += year * (365 * MILLISECONDS_PER_DAY);
    calc += ((year + 3) / 4) * MILLISECONDS_PER_DAY;

    if (year > 0) {
        calc -= ((year - 1) / 100) * MILLISECONDS_PER_DAY;
    }

    boolean isLeap = (year == 0 || (year % 4 == 0 && year % 100 != 0));
    int[] mlen = isLeap ? LEAP : NORMAL;

    calc += mlen[month] * MILLISECONDS_PER_DAY;
    calc += (day - 1) * MILLISECONDS_PER_DAY;
    calc += millis;

    calc -= rawOffset;
    calc -= UNIX_OFFSET;

    return getOffset(calc);
  }

  @Override
  public int getRawOffset() {
    return rawOffset;
  }

  @Override
  public void setRawOffset(int offsetMillis) {
    throw new UnsupportedOperationException("Cannot set raw offset on a native TimeZone");
  }

  @Override
  public int getDSTSavings() {
    return dstSavings;
  }

  @Override
  public boolean useDaylightTime() {
    return useDaylightTime;
  }

  @Override
  public boolean inDaylightTime(Date date) {
    return getOffset(date.getTime()) != rawOffset;
  }

  @Override
  public native String getDisplayName(boolean daylight, int style, Locale locale) /*-[
    if (style != JavaUtilTimeZone_SHORT && style != JavaUtilTimeZone_LONG) {
      @throw AUTORELEASE([[JavaLangIllegalArgumentException alloc] init]);
    }

    NSTimeZoneNameStyle zoneStyle;

    // "daylight" is defined in <time.h>, hence the renaming.
    if (daylight_ && useDaylightTime_) {
      zoneStyle = (style == JavaUtilTimeZone_SHORT) ?
        NSTimeZoneNameStyleShortDaylightSaving : NSTimeZoneNameStyleDaylightSaving;
    } else {
      zoneStyle = (style == JavaUtilTimeZone_SHORT) ?
        NSTimeZoneNameStyleShortStandard : NSTimeZoneNameStyleStandard;
    }

    // Find native locale.
    NSLocale *nativeLocale;
    if (locale) {
      NSMutableDictionary *components = [NSMutableDictionary dictionary];
      [components setObject:[locale getLanguage] forKey:NSLocaleLanguageCode];
      [components setObject:[locale getCountry] forKey:NSLocaleCountryCode];
      [components setObject:[locale getVariant] forKey:NSLocaleVariantCode];
      NSString *localeId = [NSLocale localeIdentifierFromComponents:components];
      nativeLocale = AUTORELEASE([[NSLocale alloc] initWithLocaleIdentifier:localeId]);
    } else {
      nativeLocale = [NSLocale currentLocale];
    }
    return [(NSTimeZone *) nativeTimeZone_ localizedName:zoneStyle locale:nativeLocale];
  ]-*/;

  private native boolean compareNativeTimeZoneRules(Object otherNativeTimeZone) /*-[
    // [NSTimeZone isEqualToTimeZone:] also compares names, which we don't want. Since we
    // only deal with native time zones that can be obtained with known names, we'll just
    // compare the underlying data.
    NSTimeZone *other = (NSTimeZone *)otherNativeTimeZone;
    return [((NSTimeZone *)self->nativeTimeZone_).data isEqualToData:other.data];
  ]-*/;
}
