// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
/*
 *******************************************************************************
 * Copyright (C) 2008-2014, International Business Machines Corporation and    *
 * others. All Rights Reserved.                                                *
 *******************************************************************************
 */
package com.ibm.icu.impl;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.TreeSet;

import com.ibm.icu.util.TimeZone;

/**
 * JavaTimeZone inherits com.ibm.icu.util.TimeZone and wraps java.util.TimeZone.
 * We used to have JDKTimeZone which wrapped Java TimeZone and used it as primary
 * TimeZone implementation until ICU4J 3.4.1.  This class works exactly like
 * JDKTimeZone and allows ICU users who use ICU4J and JDK date/time/calendar
 * services in mix to maintain only JDK timezone rules.
 *
 * This TimeZone subclass is returned by the TimeZone factory method getTimeZone(String)
 * when the default timezone type in TimeZone class is TimeZone.TIMEZONE_JDK.
 */
public class JavaTimeZone extends TimeZone {

    private static final long serialVersionUID = 6977448185543929364L;

    private static final TreeSet<String> AVAILABLESET;

    private java.util.TimeZone javatz;
    private transient java.util.Calendar javacal;
    private static Method mObservesDaylightTime;

    static {
        AVAILABLESET = new TreeSet<>();
        String[] availableIds = java.util.TimeZone.getAvailableIDs();
        for (int i = 0; i < availableIds.length; i++) {
            AVAILABLESET.add(availableIds[i]);
        }

        try {
            mObservesDaylightTime = java.util.TimeZone.class.getMethod("observesDaylightTime", (Class[]) null);
        } catch (NoSuchMethodException e) {
            // Android API level 21..23
        } catch (SecurityException e) {
            // not visible
        }
    }

    /**
     * Constructs a JavaTimeZone with the default Java TimeZone
     */
    public JavaTimeZone() {
        this(java.util.TimeZone.getDefault(), null);
    }

    /**
     * Constructs a JavaTimeZone with the specified Java TimeZone and ID.
     * @param jtz the Java TimeZone
     * @param id the ID of the zone. if null, the zone ID is initialized
     * by the given Java TimeZone's ID.
     */
    public JavaTimeZone(java.util.TimeZone jtz, String id) {
        if (id == null) {
            id = jtz.getID();
        }
        javatz = jtz;
        setID(id);
        javacal = new java.util.GregorianCalendar(javatz);
    }

    /**
     * Creates an instance of JavaTimeZone with the given timezone ID.
     * @param id A timezone ID, either a system ID or a custom ID.
     * @return An instance of JavaTimeZone for the given ID, or null
     * when the ID cannot be understood.
     */
    public static JavaTimeZone createTimeZone(String id) {
        java.util.TimeZone jtz = null;

        if (AVAILABLESET.contains(id)) {
            jtz = java.util.TimeZone.getTimeZone(id);
        }

        if (jtz == null) {
            // Use ICU's canonical ID mapping
            boolean[] isSystemID = new boolean[1];
            String canonicalID = TimeZone.getCanonicalID(id, isSystemID);
            if (isSystemID[0] && AVAILABLESET.contains(canonicalID)) {
                jtz = java.util.TimeZone.getTimeZone(canonicalID);
            }
        }

        if (jtz == null) {
            return null;
        }

        return new JavaTimeZone(jtz, id);
    }

    /* (non-Javadoc)
     * @see com.ibm.icu.util.TimeZone#getOffset(int, int, int, int, int, int)
     */
    @Override
    public int getOffset(int era, int year, int month, int day, int dayOfWeek, int milliseconds) {
        return javatz.getOffset(era, year, month, day, dayOfWeek, milliseconds);
    }

    /* (non-Javadoc)
     * @see com.ibm.icu.util.TimeZone#getOffset(long, boolean, int[])
     */
    @Override
    public void getOffset(long date, boolean local, int[] offsets) {
        synchronized (javacal) {
            if (local) {
                int fields[] = new int[6];
                Grego.timeToFields(date, fields);
                int hour, min, sec, mil;
                int tmp = fields[5];
                mil = tmp % 1000;
                tmp /= 1000;
                sec = tmp % 60;
                tmp /= 60;
                min = tmp % 60;
                hour = tmp / 60;
                javacal.clear();
                javacal.set(fields[0], fields[1], fields[2], hour, min, sec);
                javacal.set(java.util.Calendar.MILLISECOND, mil);

                int doy1, hour1, min1, sec1, mil1;
                doy1 = javacal.get(java.util.Calendar.DAY_OF_YEAR);
                hour1 = javacal.get(java.util.Calendar.HOUR_OF_DAY);
                min1 = javacal.get(java.util.Calendar.MINUTE);
                sec1 = javacal.get(java.util.Calendar.SECOND);
                mil1 = javacal.get(java.util.Calendar.MILLISECOND);

                if (fields[4] != doy1 || hour != hour1 || min != min1 || sec != sec1 || mil != mil1) {
                    // Calendar field(s) were changed due to the adjustment for non-existing time
                    // Note: This code does not support non-existing local time at year boundary properly.
                    // But, it should work fine for real timezones.
                    int dayDelta = Math.abs(doy1 - fields[4]) > 1 ? 1 : doy1 - fields[4];
                    int delta = ((((dayDelta * 24) + hour1 - hour) * 60 + min1 - min) * 60 + sec1 - sec) * 1000 + mil1 - mil;

                    // In this case, we use the offsets before the transition
                   javacal.setTimeInMillis(javacal.getTimeInMillis() - delta - 1);
                }
            } else {
                javacal.setTimeInMillis(date);
            }
            offsets[0] = javacal.get(java.util.Calendar.ZONE_OFFSET);
            offsets[1] = javacal.get(java.util.Calendar.DST_OFFSET);
        }
    }

    /* (non-Javadoc)
     * @see com.ibm.icu.util.TimeZone#getRawOffset()
     */
    @Override
    public int getRawOffset() {
        return javatz.getRawOffset();
    }

    /* (non-Javadoc)
     * @see com.ibm.icu.util.TimeZone#inDaylightTime(java.util.Date)
     */
    @Override
    public boolean inDaylightTime(Date date) {
        return javatz.inDaylightTime(date);
    }

    /* (non-Javadoc)
     * @see com.ibm.icu.util.TimeZone#setRawOffset(int)
     */
    @Override
    public void setRawOffset(int offsetMillis) {
        if (isFrozen()) {
            throw new UnsupportedOperationException("Attempt to modify a frozen JavaTimeZone instance.");
        }
        javatz.setRawOffset(offsetMillis);
    }

    /* (non-Javadoc)
     * @see com.ibm.icu.util.TimeZone#useDaylightTime()
     */
    @Override
    public boolean useDaylightTime() {
        return javatz.useDaylightTime();
    }

    /* (non-Javadoc)
     * @see com.ibm.icu.util.TimeZone#observesDaylightTime()
     */
    @Override
    public boolean observesDaylightTime() {
        if (mObservesDaylightTime != null) {
            // Java 7+, Android API level 24+
            // https://developer.android.com/reference/java/util/TimeZone
            try {
                return (Boolean)mObservesDaylightTime.invoke(javatz, (Object[]) null);
            } catch (IllegalAccessException e) {
            } catch (IllegalArgumentException e) {
            } catch (InvocationTargetException e) {
            }
        }
        return super.observesDaylightTime();
    }

    /* (non-Javadoc)
     * @see com.ibm.icu.util.TimeZone#getDSTSavings()
     */
    @Override
    public int getDSTSavings() {
        return javatz.getDSTSavings();
    }

    public java.util.TimeZone unwrap() {
        return javatz;
    }

    /* (non-Javadoc)
     * @see com.ibm.icu.util.TimeZone#clone()
     */
    @Override
    public Object clone() {
        if (isFrozen()) {
            return this;
        }
        return cloneAsThawed();
    }

    /* (non-Javadoc)
     * @see com.ibm.icu.util.TimeZone#hashCode()
     */
    @Override
    public int hashCode() {
        return super.hashCode() + javatz.hashCode();
    }

    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        javacal = new java.util.GregorianCalendar(javatz);
    }

    // Freezable stuffs
    private transient volatile boolean isFrozen = false;

    /* (non-Javadoc)
     * @see com.ibm.icu.util.TimeZone#isFrozen()
     */
    @Override
    public boolean isFrozen() {
        return isFrozen;
    }

    /* (non-Javadoc)
     * @see com.ibm.icu.util.TimeZone#freeze()
     */
    @Override
    public TimeZone freeze() {
        isFrozen = true;
        return this;
    }

    /* (non-Javadoc)
     * @see com.ibm.icu.util.TimeZone#cloneAsThawed()
     */
    @Override
    public TimeZone cloneAsThawed() {
        JavaTimeZone tz = (JavaTimeZone)super.cloneAsThawed();
        tz.javatz = (java.util.TimeZone)javatz.clone();
        tz.javacal = new java.util.GregorianCalendar(javatz);  // easier than synchronized javacal.clone()
        tz.isFrozen = false;
        return tz;
    }

}
