package org.unicode.cldr.unittest;

import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import org.unicode.cldr.util.CldrUtility;
import org.unicode.cldr.util.Pair;
import org.unicode.cldr.util.SupplementalDataInfo;

import com.ibm.icu.impl.OlsonTimeZone;
import com.ibm.icu.impl.Relation;
import com.ibm.icu.text.DecimalFormat;
import com.ibm.icu.text.SimpleDateFormat;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.TimeZone;
import com.ibm.icu.util.TimeZoneRule;
import com.ibm.icu.util.TimeZoneTransition;
import com.ibm.icu.util.ULocale;

public class TestMetazoneTransitions {

    private static final int printDaylightTransitions = 6;

    private static final int SECOND = 1000;
    private static final int MINUTE = 60 * SECOND;
    private static final int HOUR = 60 * MINUTE;

    static final long startDate;

    static final long endDate;

    static final SimpleDateFormat neutralFormat = new SimpleDateFormat(
        "yyyy-MM-dd HH:mm:ss", ULocale.ENGLISH);
    static final DecimalFormat threeDigits = new DecimalFormat("000");
    static final DecimalFormat twoDigits = new DecimalFormat("00");

    public static final Set<Integer> allOffsets = new TreeSet<Integer>();

    static {
        TimeZone GMT = TimeZone.getTimeZone("Etc/GMT");
        neutralFormat.setTimeZone(GMT);
        Calendar cal = Calendar.getInstance(GMT, ULocale.US);
        int year = cal.get(Calendar.YEAR);
        cal.clear(); // need to clear fractional seconds
        cal.set(1970, 0, 1, 0, 0, 0);
        startDate = cal.getTimeInMillis();
        cal.set(year + 5, 0, 1, 0, 0, 0);
        endDate = cal.getTimeInMillis();
        if (startDate != 0) {
            throw new IllegalArgumentException();
        }
    }

    public static void main(String[] args) throws Exception {
        java.util.TimeZone zone2 = java.util.TimeZone.getTimeZone("GMT+830");
        System.out.println(zone2.getID());
        zone2 = java.util.TimeZone.getTimeZone("GMT+08");
        System.out.println(zone2.getID());
        zone2 = java.util.TimeZone.getTimeZone("Etc/GMT-8");
        System.out.println(zone2.getID());

        java.util.TimeZone.setDefault(java.util.TimeZone
            .getTimeZone("America/Los_Angeles"));
        DateFormat javaFormat = DateFormat.getDateTimeInstance(DateFormat.FULL,
            DateFormat.MEDIUM, Locale.US);
        long start = new Date(107, 0, 1, 0, 30, 0).getTime();
        start -= start % 1000; // clean up millis
        long end = new Date(108, 0, 1, 0, 30, 0).getTime();
        for (long date = start; date < end; date += 15 * MINUTE) {
            String formatted = javaFormat.format(date);
            Date roundTrip = javaFormat.parse(formatted);
            if (roundTrip.getTime() != date) {
                System.out.println("Java roundtrip failed for: " + formatted
                    + "\tSource: " + new Date(date) + "\tTarget: "
                    + roundTrip);
            }
        }
        new TestMetazoneTransitions().run();
    }

    private static class ZoneTransition implements Comparable<ZoneTransition> {
        long date;

        int offset;

        @Override
        public boolean equals(Object that) {
            ZoneTransition other = (ZoneTransition) that;
            return date == other.date && offset == other.offset;
        }

        @Override
        public int hashCode() {
            return (int) (date ^ (date >>> 32) ^ offset);
        }

        public ZoneTransition(long date, int offset) {
            this.date = date;
            this.offset = offset;
        }

        /**
         * Return the one with the smaller offset, or if equal then the smallest
         * time
         *
         * @param o
         * @return
         */
        @Override
        public int compareTo(ZoneTransition o) {
            int delta = offset - o.offset;
            if (delta != 0)
                return delta;
            long delta2 = date - o.date;
            return delta2 == 0 ? 0 : delta2 < 0 ? -1 : 1;
        }

        @Override
        public String toString() {
            return neutralFormat.format(date) + ": " + ((double) offset) / HOUR
                + "hrs";
        }
    }

    enum DaylightChoice {
        NO_DAYLIGHT, ONLY_DAYLIGHT
    };

    private static class ZoneTransitions implements Comparable<ZoneTransitions> {
        List<ZoneTransition> chronologicalList = new ArrayList<ZoneTransition>();

        @Override
        public boolean equals(Object that) {
            ZoneTransitions other = (ZoneTransitions) that;
            return chronologicalList.equals(other.chronologicalList);
        }

        @Override
        public int hashCode() {
            return chronologicalList.hashCode();
        }

        public ZoneTransitions(String tzid, DaylightChoice allowDaylight) {
            TimeZone zone = TimeZone.getTimeZone(tzid);
            for (long date = startDate; date < endDate; date = getTransitionAfter(
                zone, date)) {
                addIfDifferent(zone, date, allowDaylight);
            }
        }

        private void addIfDifferent(TimeZone zone, long date,
            DaylightChoice allowDaylight) {
            int offset = zone.getOffset(date);
            allOffsets.add(offset);
            int delta = getDSTSavings(zone, date);
            switch (allowDaylight) {
            case ONLY_DAYLIGHT:
                offset = delta;
                break;
            case NO_DAYLIGHT:
                offset -= delta;
                break;
            }
            int size = chronologicalList.size();
            if (size > 0) {
                ZoneTransition last = chronologicalList.get(size - 1);
                if (last.offset == offset) {
                    return;
                }
            }
            chronologicalList.add(new ZoneTransition(date, offset));
        }

        @Override
        public int compareTo(ZoneTransitions other) {
            int minSize = Math.min(chronologicalList.size(),
                other.chronologicalList.size());
            for (int i = 0; i < minSize; ++i) {
                ZoneTransition a = chronologicalList.get(i);
                ZoneTransition b = other.chronologicalList.get(i);
                int order = a.compareTo(b);
                if (order != 0)
                    return order;
            }
            return chronologicalList.size() - other.chronologicalList.size();
        }

        public String toString(String separator, int abbreviateToSize) {
            if (abbreviateToSize > 0
                && chronologicalList.size() > abbreviateToSize) {
                int limit = abbreviateToSize / 2;
                return CldrUtility.join(slice(chronologicalList, 0, limit),
                    separator)
                    + separator
                    + "..."
                    + separator
                    + CldrUtility.join(
                        slice(chronologicalList,
                            chronologicalList.size() - limit,
                            chronologicalList.size()),
                        separator);
            }
            return CldrUtility.join(chronologicalList, separator);
        }

        @Override
        public String toString() {
            return toString("; ", -1);
        }

        public int size() {
            // TODO Auto-generated method stub
            return chronologicalList.size();
        }

        public Pair<ZoneTransitions, ZoneTransitions> getDifferenceFrom(
            ZoneTransitions other) {
            int minSize = Math.min(chronologicalList.size(),
                other.chronologicalList.size());
            for (int i = 0; i < minSize; ++i) {
                ZoneTransition a = chronologicalList.get(i);
                ZoneTransition b = other.chronologicalList.get(i);
                int order = a.compareTo(b);
                if (order != 0)
                    return new Pair(a, b);
            }
            if (chronologicalList.size() > other.chronologicalList.size()) {
                return new Pair(chronologicalList.get(minSize), null);
            } else if (chronologicalList.size() < other.chronologicalList
                .size()) {
                return new Pair(null, other.chronologicalList.get(minSize));
            } else {
                return new Pair(null, null);
            }
        }

        public ZoneTransition get(int i) {
            return chronologicalList.get(i);
        }
    }

    final static SupplementalDataInfo supplementalData = SupplementalDataInfo
        .getInstance("C:/cvsdata/unicode/cldr/common/supplemental/");

    private void run() {
        // String[] zones = TimeZone.getAvailableIDs();
        Relation<ZoneTransitions, String> partition = Relation.of(
            new TreeMap<ZoneTransitions, Set<String>>(), TreeSet.class);
        Relation<ZoneTransitions, String> daylightPartition = Relation.of(
            new TreeMap<ZoneTransitions, Set<String>>(), TreeSet.class);
        Map<String, String> toDaylight = new TreeMap<String, String>();
        Map<ZoneTransitions, String> daylightNames = new TreeMap<ZoneTransitions, String>();

        // get the main data
        for (String zone : supplementalData.getCanonicalZones()) {
            ZoneTransitions transitions = new ZoneTransitions(zone,
                DaylightChoice.NO_DAYLIGHT);
            partition.put(transitions, zone);
            transitions = new ZoneTransitions(zone,
                DaylightChoice.ONLY_DAYLIGHT);
            if (transitions.size() > 1) {
                daylightPartition.put(transitions, zone);
            }
        }
        // now assign names
        int count = 0;
        for (ZoneTransitions transitions : daylightPartition.keySet()) {
            final String dname = "D" + threeDigits.format(++count);
            daylightNames.put(transitions, dname);
            for (String zone : daylightPartition.getAll(transitions)) {
                toDaylight.put(zone, dname);
            }
        }
        // get the "primary" zone for each metazone
        Map<String, String> zoneToMeta = new TreeMap<String, String>();
        Map<String, Map<String, String>> metazoneToRegionToZone = supplementalData
            .getMetazoneToRegionToZone();
        for (String meta : metazoneToRegionToZone.keySet()) {
            Map<String, String> regionToZone = metazoneToRegionToZone.get(meta);
            String keyZone = regionToZone.get("001");
            zoneToMeta.put(keyZone, meta);
        }

        System.out.println();
        System.out
            .println("=====================================================");
        System.out.println("*** Non-Daylight Partition");
        System.out
            .println("=====================================================");
        System.out.println();

        count = 0;
        Set<String> noMeta = new TreeSet<String>();
        Set<String> multiMeta = new TreeSet<String>();
        Set<String> stableZones = new TreeSet<String>();
        for (ZoneTransitions transitions : partition.keySet()) {

            System.out.println();
            final String nonDaylightPartitionName = "M"
                + threeDigits.format(++count);
            System.out.println("Non-Daylight Partition "
                + nonDaylightPartitionName);
            int metaCount = 0;
            Set<String> metas = new TreeSet<String>();
            for (String zone : partition.getAll(transitions)) {
                String daylightName = toDaylight.get(zone);
                String meta = zoneToMeta.get(zone);
                if (meta != null) {
                    ++metaCount;
                    metas.add(meta);
                }
                System.out.println("\t" + zone
                    + (daylightName == null ? "" : "\t" + daylightName)
                    + (meta == null ? "" : "\t\tMETA:" + meta));
            }
            if (metaCount == 0) {
                noMeta.add(nonDaylightPartitionName + "{"
                    + CldrUtility.join(partition.getAll(transitions), ", ")
                    + "}");
            } else if (metaCount > 1) {
                multiMeta.add(nonDaylightPartitionName + "{"
                    + CldrUtility.join(metas, ", ") + "}");
            }
            if (transitions.size() == 1) {
                final int offset = transitions.get(0).offset;
                allOffsets.remove(offset);
                stableZones.add(nonDaylightPartitionName + ", " + offset
                    / (double) HOUR + "hrs " + "{"
                    + CldrUtility.join(partition.getAll(transitions), ", ")
                    + "}");
            }
            System.out.println("\t\t"
                + transitions.toString(CldrUtility.LINE_SEPARATOR + "\t\t",
                    -1));
        }
        System.out.println();
        System.out
            .println("*** Non-Daylight Partitions with no canonical meta");
        System.out.println("\t"
            + CldrUtility.join(noMeta, CldrUtility.LINE_SEPARATOR + "\t"));
        System.out.println();
        System.out
            .println("*** Non-Daylight Partitions with more than one canonical meta");
        System.out.println("\t"
            + CldrUtility
                .join(multiMeta, CldrUtility.LINE_SEPARATOR + "\t"));
        System.out.println();
        System.out.println("*** Stable Non-Daylight Partitions");
        System.out.println("\t"
            + CldrUtility.join(stableZones, CldrUtility.LINE_SEPARATOR
                + "\t"));
        System.out.println();
        System.out.println("*** Offsets with no stable partition");
        for (int offset : allOffsets) {
            System.out.println("\t" + offset / (double) HOUR + "hrs");
        }
        System.out.println();

        System.out.println();
        System.out
            .println("=====================================================");
        System.out.println("*** Daylight Partition");
        System.out
            .println("=====================================================");
        System.out.println();

        ZoneTransitions lastTransitions = null;
        String lastName = null;
        for (ZoneTransitions transitions : daylightPartition.keySet()) {
            System.out.println();
            String daylightName = daylightNames.get(transitions);
            System.out.println("Daylight Partition\t" + daylightName);
            for (String zone : daylightPartition.getAll(transitions)) {
                System.out.println("\t" + zone);
            }
            System.out.println("\t\t"
                + transitions.toString(CldrUtility.LINE_SEPARATOR + "\t\t",
                    printDaylightTransitions));
            if (lastTransitions != null) {
                Pair<ZoneTransitions, ZoneTransitions> diff = transitions
                    .getDifferenceFrom(lastTransitions);
                System.out.println("\t\tTransition Difference from " + lastName
                    + ":\t" + diff);
            }
            lastTransitions = transitions;
            lastName = daylightName;
        }

    }

    public static <T> List<T> slice(List<T> list, int start, int limit) {
        ArrayList<T> temp = new ArrayList<T>();
        for (int i = start; i < limit; ++i) {
            temp.add(list.get(i));
        }
        return temp;
    }

    /* Methods that ought to be on TimeZone */
    /**
     * Return the next point in time after date when the zone has a different
     * offset than what it has on date. If there are no later transitions,
     * returns Long.MAX_VALUE.
     *
     * @param zone
     *            input zone -- should be method of TimeZone
     * @param date
     *            input date, in standard millis since 1970-01-01 00:00:00 GMT
     */
    public static long getTransitionAfter(TimeZone zone, long date) {
        TimeZoneTransition transition = ((OlsonTimeZone) zone)
            .getNextTransition(date, false);
        if (transition == null) {
            return Long.MAX_VALUE;
        }
        date = transition.getTime();
        return date;
    }

    /**
     * Return true if the zone is in daylight savings on the date.
     *
     * @param zone
     *            input zone -- should be method of TimeZone
     * @param date
     *            input date, in standard millis since 1970-01-01 00:00:00 GMT
     */
    public static boolean inDaylightTime(TimeZone zone, long date) {
        return ((OlsonTimeZone) zone).inDaylightTime(new Date(date));
    }

    /**
     * Return the daylight savings offset on the given date.
     *
     * @param zone
     *            input zone -- should be method of TimeZone
     * @param date
     *            input date, in standard millis since 1970-01-01 00:00:00 GMT
     */
    public static int getDSTSavings(TimeZone zone, long date) {
        if (!inDaylightTime(zone, date)) {
            return 0;
        }
        TimeZoneTransition transition = ((OlsonTimeZone) zone)
            .getPreviousTransition(date + 1, true);
        TimeZoneRule to = transition.getTo();
        int delta = to.getDSTSavings();
        // if (delta != HOUR) {
        // System.out.println("Delta " + delta/(double)HOUR + " for " +
        // zone.getID());
        // }
        return delta;
    }
}