package org.unicode.cldr.util;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.regex.Matcher;

import org.unicode.cldr.draft.FileUtilities;
import org.unicode.cldr.tool.Option;
import org.unicode.cldr.tool.Option.Options;
import org.unicode.cldr.tool.TablePrinter;
import org.unicode.cldr.util.SupplementalDataInfo.DateRange;
import org.unicode.cldr.util.SupplementalDataInfo.MetaZoneRange;
import org.unicode.cldr.util.TimezoneFormatter.Format;

import com.ibm.icu.impl.Row.R5;
import com.ibm.icu.text.MessageFormat;
import com.ibm.icu.text.SimpleDateFormat;
import com.ibm.icu.util.TimeZone;
import com.ibm.icu.util.ULocale;

public class VerifyZones {
    private static final CLDRConfig CLDR_CONFIG = CLDRConfig.getInstance();

    private static final String DIR = CLDRPaths.CHART_DIRECTORY + "verify/zones/";

    private static final boolean DEBUG = false;

    final static Options myOptions = new Options();

    enum MyOptions {
        organization(".*", "CLDR", "organization"),
        filter(".*", ".*", "locale filter (regex)"),
        timezoneFilter(".*", null, "timezone filter (regex)"), ;
        // boilerplate
        final Option option;

        MyOptions(String argumentPattern, String defaultArgument, String helpText) {
            option = myOptions.add(this, argumentPattern, defaultArgument, helpText);
        }
    }

    static long date = new Date(new Date().getYear(), 0, 15, 0, 0, 0).getTime();
    static long date6 = date + 182L * 24 * 60 * 60 * 1000;

    static class MetazoneRow extends R5<Long, String, String, Integer, String> {
        public MetazoneRow(Integer order, Integer rawOffset, String container, int orderInMetazone, String metazone, String zone) {
            super(((long) order << 32) + rawOffset, container, metazone, orderInMetazone, zone);
        }

        public String getContainer() {
            return get1();
        }

        public String getMetazone() {
            return get2();
        }

        public String getZone() {
            return get4();
        }
    }

    public static class ZoneFormats {
        private String gmtFormat;
        private String hourFormat;
        private String[] hourFormatPlusMinus;
        private ICUServiceBuilder icuServiceBuilder = new ICUServiceBuilder();
        private CLDRFile cldrFile;

        public enum Length {
            LONG, SHORT;
            public String toString() {
                return name().toLowerCase(Locale.ENGLISH);
            }
        }

        public enum Type {
            generic, standard, daylight, genericOrStandard
        }

        public ZoneFormats set(CLDRFile cldrFile) {
            this.cldrFile = cldrFile;

            gmtFormat = cldrFile.getWinningValue("//ldml/dates/timeZoneNames/gmtFormat");
            hourFormat = cldrFile.getWinningValue("//ldml/dates/timeZoneNames/hourFormat");
            hourFormatPlusMinus = hourFormat.split(";");
            icuServiceBuilder.setCldrFile(cldrFile);
            return this;
        }

        public String formatGMT(TimeZone currentZone) {
            int tzOffset = currentZone.getRawOffset();
            SimpleDateFormat dateFormat = icuServiceBuilder.getDateFormat("gregorian",
                hourFormatPlusMinus[tzOffset >= 0 ? 0 : 1]);
            String hoursMinutes = dateFormat.format(tzOffset >= 0 ? tzOffset : -tzOffset);
            return MessageFormat.format(gmtFormat, hoursMinutes);
        }

        public String getExemplarCity(String timezoneString) {
            String exemplarCity = cldrFile.getWinningValue("//ldml/dates/timeZoneNames/zone[@type=\"" + timezoneString
                + "\"]/exemplarCity");
            if (exemplarCity == null) {
                exemplarCity = timezoneString.substring(timezoneString.lastIndexOf('/') + 1).replace('_', ' ');
            }
            return exemplarCity;
        }

        public String getMetazoneName(String metazone, Length length, Type typeIn) {
            Type type = typeIn == Type.genericOrStandard ? Type.generic : typeIn;
            String name = cldrFile.getWinningValue("//ldml/dates/timeZoneNames/metazone[@type=\""
                + metazone + "\"]/" + length + "/" + type);

            return name != null ? name : typeIn != Type.genericOrStandard ? "n/a" : getMetazoneName(metazone, length,
                Type.standard);
        }
    }

    private final static SupplementalDataInfo sdi = SupplementalDataInfo.getInstance();
    private final static Map<String, Map<String, String>> metazoneToRegionToZone = sdi.getMetazoneToRegionToZone();
    private final static Set<MetazoneRow> rows = new TreeSet<MetazoneRow>();
    private final static Set<String> goldenZones = new HashSet<String>();
    private final static Map<String, Integer> countryToOrder = new HashMap<String, Integer>();

    private final static List<Format> FORMAT_LIST = Arrays.asList(Format.VVVV, Format.vvvv, Format.v, Format.zzzz,
        Format.z, Format.zzzz, Format.z);
    static {

        // find out which canonical zones are not in a metazone
        Map<String, String> nameToCountry = new TreeMap<String, String>();
        String[] zones = TimeZone.getAvailableIDs();
        Set<String> zoneSet = new LinkedHashSet<String>();
        Set<String> noncanonical = new LinkedHashSet<String>();
        for (String zone : zones) {
            String countryCode = TimeZone.getRegion(zone);
            String englishTerritory = ULocale.getDisplayCountry("und-" + countryCode, ULocale.ENGLISH);
            nameToCountry.put(englishTerritory, countryCode);
            String canon = TimeZone.getCanonicalID(zone);
            if (canon.equals(zone)) {
                zoneSet.add(canon);
            } else {
                noncanonical.add(zone);
            }
        }

        // get mapping of country names to ints
        int i = 0;
        for (Entry<String, String> entry : nameToCountry.entrySet()) {
            countryToOrder.put(entry.getValue(), i++);
        }

        //System.out.println("Canonical zones:\t" + zoneSet.size() + "\t" + zoneSet);
        //System.out.println("Non-canonical zones:\t" + noncanonical.size() + "\t" + noncanonical);

        Set<String> metazones = sdi.getAllMetazones();
        if (DEBUG && !metazones.equals(metazoneToRegionToZone.keySet())) {
            System.out.println("Mismatch between metazones");
            showVennSets(metazones, metazoneToRegionToZone.keySet());
        }

        Set<String> zonesInMetazones = new LinkedHashSet<String>();
        for (String metazone : metazones) {
            //String container = PathHeader.getMetazonePageTerritory(metazone);
            Map<String, String> regionToZone = metazoneToRegionToZone.get(metazone);
            String zone = regionToZone.get("001");
            goldenZones.add(zone);
            zonesInMetazones.add(zone);
            //            TimeZone currentZone = TimeZone.getTimeZone(tz_string);
            //            int order = Containment.getOrder(container);
            //            int offsetOrder = currentZone.getRawOffset();
            //            MetazoneRow row = new MetazoneRow(order, offsetOrder, container, 0, metazone, tz_string);
            //            rows.add(row);
            addRow(metazone, zone, 0);
        }
        //System.out.println("Zones. A = canonical zones, B = zones in metazonesToRegionToZone");
        //showVennSets(zoneSet, zonesInMetazones);
        vennSets(zoneSet, zonesInMetazones);
        Set<String> found = new LinkedHashSet<String>();
        for (String zone : zoneSet) {
            Set<MetaZoneRange> metaZoneRanges = sdi.getMetaZoneRanges(zone);
            if (metaZoneRanges == null) {
                continue;
            }
            for (MetaZoneRange metaZoneRange : metaZoneRanges) {
                if (metaZoneRange.dateRange.getTo() == DateRange.END_OF_TIME) {
                    found.add(zone);
                    addRow(metaZoneRange.metazone, zone, 1);
                    break;
                }
            }
        }
        //        zoneSet.removeAll(found);
        //        for (String zone : zoneSet) {
        //            found.add(zone);
        //            //            TimeZone currentZone = TimeZone.getTimeZone(tz_string);
        //            //            int offsetOrder = currentZone.getRawOffset();
        //            //            MetazoneRow row = new MetazoneRow(Integer.MAX_VALUE, offsetOrder, "001", 1, "None", tz_string);
        //            //            rows.add(row);
        //            addRow("None", zone, 1);
        //        }
        if (DEBUG) System.out.println("\nSorted");
        for (MetazoneRow row : rows) {
            if (row.getMetazone().equals("Europe_Central")) {
                if (DEBUG) System.out.println(row);
            }
        }
    }

    private static void addRow(String metaZone, String tz_string, int orderInMetazone) {
        TimeZone currentZone = TimeZone.getTimeZone(tz_string);
        String container = PathHeader.getMetazonePageTerritory(metaZone);
        if (container == null) {
            return; // skip
        }
        int order = Containment.getOrder(container);
        int offsetOrder = currentZone.getRawOffset();
        orderInMetazone = (orderInMetazone << 16)
            | (hasDaylight(currentZone) ? 0 : 1)
            | countryToOrder.get(TimeZone.getRegion(tz_string));
        MetazoneRow row = new MetazoneRow(order, offsetOrder, container,
            orderInMetazone, metaZone, tz_string);
        if (metaZone.equals("Europe_Central")) {
            if (DEBUG) System.out.println(row);
        }
        rows.add(row);
    }

    private static void showVennSets(Set<String> zoneSet, Set<String> zonesInMetazones) {
        Set<String> common = new LinkedHashSet<String>();
        Set<String> firstMinusSecond = new LinkedHashSet<String>();
        Set<String> secondMinusFirst = new LinkedHashSet<String>();
        vennSets(zoneSet, zonesInMetazones, common, firstMinusSecond, secondMinusFirst);
        if (!common.isEmpty()) System.out.println("A & B:\t" + common.size() + "\t" + common);
        if (!firstMinusSecond.isEmpty()) System.out.println("A - B:\t" + firstMinusSecond.size() + "\t" + firstMinusSecond);
        if (!secondMinusFirst.isEmpty()) System.out.println("B - A:\t" + secondMinusFirst.size() + "\t" + secondMinusFirst);
    }

    private static <T> void vennSets(Set<T> first, Set<T> second,
        Set<T> common, Set<T> firstMinusSecond, Set<T> secondMinusFirst) {
        common.clear();
        common.addAll(first);
        common.retainAll(second);
        firstMinusSecond.clear();
        firstMinusSecond.addAll(first);
        firstMinusSecond.removeAll(common);
        secondMinusFirst.clear();
        secondMinusFirst.addAll(second);
        secondMinusFirst.removeAll(common);
    }

    @SuppressWarnings("unused")
    private static <T> void vennSets(Set<T> first, Set<T> second, Set<T> common) {
        common.clear();
        common.addAll(first);
        common.retainAll(second);
        first.removeAll(common);
        second.removeAll(common);
    }

    private static <T> void vennSets(Set<T> first, Set<T> second) {
        first.removeAll(second);
        second.removeAll(first);
    }

    /**
     * Produce a set of static tables from the vxml data. Only a stopgap until the above is integrated into ST.
     *
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        myOptions.parse(MyOptions.organization, args, true);

        String organization = MyOptions.organization.option.getValue();
        String filter = MyOptions.filter.option.getValue();
        String timezoneFilterString = MyOptions.timezoneFilter.option.getValue();
        Matcher timezoneFilter = timezoneFilterString == null ? null : PatternCache.get(timezoneFilterString)
            .matcher("");

        Factory factory2 = Factory.make(CLDRPaths.MAIN_DIRECTORY, filter);
        CLDRFile englishCldrFile = factory2.make("en", true);
        DateTimeFormats.writeCss(DIR);
        final CLDRFile english = CLDR_CONFIG.getEnglish();

        Map<String, String> indexMap = new TreeMap<>(CLDR_CONFIG.getCollator());

        for (String localeID : factory2.getAvailableLanguages()) {
            Level level = StandardCodes.make().getLocaleCoverageLevel(organization, localeID);
            if (Level.MODERN.compareTo(level) > 0) {
                continue;
            }
            CLDRFile cldrFile = factory2.make(localeID, true);
            PrintWriter out = FileUtilities.openUTF8Writer(DIR, localeID + ".html");
            String title = "Verify Time Zones: " + englishCldrFile.getName(localeID);
            out.println("<!doctype HTML PUBLIC '-//W3C//DTD HTML 4.0 Transitional//EN'><html><head>\n" +
                "<meta http-equiv='Content-Type' content='text/html; charset=utf-8'>\n" +
                "<title>" + title + "</title>\n" +
                "<link rel='stylesheet' type='text/css' href='index.css'>\n" +
                "</head><body><h1>" + title + "</h1>\n"
                + "<p><a href='index.html'>Index</a></p>\n"
                );

            showZones(timezoneFilter, englishCldrFile, cldrFile, out);

            out.println("</body></html>");
            out.close();

            indexMap.put(english.getName(localeID), localeID + ".html");
        }
        try (PrintWriter index = DateTimeFormats.openIndex(DIR, "Time Zones")) {
            DateTimeFormats.writeIndexMap(indexMap, index);
        }

        // Look at DateTimeFormats.java

        if (true) return;

        // Set<String> defaultContentLocales = sdi.getDefaultContentLocales();
        // NumberFormat enf = NumberFormat.getIntegerInstance(ULocale.ENGLISH);
        // enf.setGroupingUsed(false);
        // Set<String> debugCreationErrors = new LinkedHashSet<String>();
        // Set<String> errors = new LinkedHashSet<String>();
        //
        // for (String locale : factory2.getAvailableLanguages()) {
        // if (defaultContentLocales.contains(locale)) {
        // continue;
        // }
        // Level level = StandardCodes.make().getLocaleCoverageLevel(organization, locale);
        // if (Level.MODERN.compareTo(level) > 0) {
        // continue;
        // }
        //
        // // one path for group-3, one for group-4
        // int factor = USES_GROUPS_OF_4.contains(locale) ? 10000 : 1000;
        //
        // ULocale locale2 = new ULocale(locale);
        // NumberFormat nf = NumberFormat.getIntegerInstance(locale2);
        // nf.setMaximumFractionDigits(0);
        // CLDRFile cldrFile = factory2.make(locale, true, DraftStatus.contributed);
        // PluralInfo pluralInfo = sdi.getPlurals(locale);
        // Set<Double> samples = new TreeSet<Double>();
        // for (Entry<Count, List<Double>> entry : pluralInfo.getCountToExamplesMap().entrySet()) {
        // samples.add(entry.getValue().get(0));
        // }
        // String[] debugOriginals = null;
        // CompactDecimalFormat cdf = BuildIcuCompactDecimalFormat.build(cldrFile, debugCreationErrors, debugOriginals,
        // Style.SHORT, locale2);
        // captureErrors(debugCreationErrors, errors, locale, "short");
        // CompactDecimalFormat cdfs = BuildIcuCompactDecimalFormat.build(cldrFile, debugCreationErrors, debugOriginals,
        // Style.LONG, locale2);
        // captureErrors(debugCreationErrors, errors, locale, "long");
        //
        // Set<Double> samples2 = new TreeSet<Double>();
        // for (int i = 10; i < factor; i *= 10) {
        // for (Double sample : samples) {
        // samples2.add(sample*i);
        // }
        // }
        // samples.addAll(samples2);
        // samples.add(1.5d);
        // System.out.println("———\t" + englishCldrFile.getName(locale) + "\t———");
        //
        // String column12 = (locale + "\t" + englishCldrFile.getName(locale));
        // System.out.print(column12 +
        // "\tNumeric\tCompact-Short\tCompact-Long\tFixed Numeric\tFixed Compact-Short\tFixed Compact-Long\n");
        //
        // try {
        // // we print the __ so that it can be imported into a spreadsheet without problems.
        // for (long i = factor; i <= 100000000000000L; i *= factor) {
        // for (Double sample : samples) {
        // double source = i * sample;
        // if (false && source == 22000000 && locale.equals("cs")) {
        // System.out.println("**");
        // }
        // System.out.print(locale + "\t__" + enf.format(source));
        // System.out.print("\t__" + nf.format(source));
        // String formatted = cdf.format(source);
        // System.out.print("\t__" + formatted);
        // formatted = cdfs.format(source);
        // System.out.println("\t__" + formatted);
        // }
        // System.out.println();
        // }
        // } catch (Exception e) {
        // e.printStackTrace();
        // }
        // }
        // for (String s : errors) {
        // System.out.println(s);
        // }
    }

    public static void showZones(Matcher timezoneFilter,
        CLDRFile englishCldrFile, CLDRFile nativeCdrFile,
        Appendable out) throws IOException {
        TablePrinter tablePrinter = new TablePrinter() // .setCaption("Timezone Formats")
        .setTableAttributes("class='dtf-table'")
        .addColumn("Metazone").setHeaderCell(true).setSpanRows(true)
        .setHeaderAttributes("class='dtf-th'").setCellAttributes("class='dtf-s'")
        .addColumn("Region: TZID").setHeaderCell(true).setSpanRows(true)
        .setHeaderAttributes("class='dtf-th'").setCellAttributes("class='dtf-s'")
        //.setCellPattern(CldrUtility.getDoubleLinkMsg())
        // HACK because anchors don't work any more
        // .addColumn("Region: City").setHeaderCell(true).setSpanRows(true)
        // .addColumn("Region/City").setSpanRows(true)
        ;
        //         .addColumn("Code", "class='source'", CldrUtility.getDoubleLinkMsg(), "class='source'", true).setSpanRows(true)

        boolean daylight = false;
        for (Format s : FORMAT_LIST) {
            tablePrinter.addColumn(s.toString()
                + "<br>" + s.type.toString(daylight)
                + "<br>" + s.location
                + "<br>" + s.length).setSpanRows(true).setHeaderAttributes("class='dtf-th'")
                .setCellAttributes("class='dtf-s'");
            if (s == Format.z) {
                daylight = true; // reset for final 2 items
            }
        }
        tablePrinter.addColumn("View").setHeaderCell(true).setHeaderAttributes("class='dtf-th'").setCellAttributes("class='dtf-s'");
        ZoneFormats englishZoneFormats = new ZoneFormats().set(englishCldrFile);
        addZones(englishZoneFormats, nativeCdrFile, timezoneFilter, tablePrinter);

        out.append(tablePrinter.toString() + "\n");
    }

    private static void addZones(ZoneFormats englishZoneFormats, CLDRFile cldrFile, Matcher timezoneFilter,
        TablePrinter output) throws IOException {
        CLDRFile englishCldrFile = englishZoneFormats.cldrFile;
        //ZoneFormats nativeZoneFormats = new ZoneFormats().set(cldrFile);
        TimezoneFormatter tzformatter = new TimezoneFormatter(cldrFile);

        for (MetazoneRow row : rows) {
            String grouping = row.getContainer();
            String metazone = row.getMetazone();
            String tzid = row.getZone();
            TimeZone currentZone = TimeZone.getTimeZone(tzid);
            TimeZone tz = currentZone;

            String englishGrouping = englishCldrFile.getName(CLDRFile.TERRITORY_NAME, grouping);

            String metazoneInfo = englishGrouping
                + "<br>" + englishZoneFormats.formatGMT(currentZone)
                + "<br>" + "MZ: " + metazone;

            boolean isGolden = goldenZones.contains(tzid);
            String countryCode2 = TimeZone.getRegion(tzid);
            if (countryCode2.equals("001")) {
                continue;
            }
            String englishTerritory = englishCldrFile.getName(CLDRFile.TERRITORY_NAME, countryCode2);
            output.addRow()
            .addCell(metazoneInfo)
            .addCell(englishTerritory + ": " + tzid.replace("/", "/\u200B"));
            long date2 = getStandardDate(tz);
            for (Format pattern : FORMAT_LIST) {
                String formattedZone = tzformatter.getFormattedZone(tzid, pattern.toString(), date2);
                if (isGolden) {
                    formattedZone = "<b>" + formattedZone + "</b>";
                }
                output.addCell(formattedZone);
                if (pattern == Format.z) {
                    if (!hasDaylight(tz)) {
                        output.addCell("<i>n/a</i>");
                        output.addCell("<i>n/a</i>");
                        break;
                    }
                    date2 = date2 == date ? date6 : date; // reverse for final 2 items
                }
            }
            String view = PathHeader.getLinkedView(surveyUrl, cldrFile, METAZONE_PREFIX + metazone + METAZONE_SUFFIX);
            if (view == null) {
                view = PathHeader.getLinkedView(surveyUrl, cldrFile, METAZONE_PREFIX + metazone + METAZONE_SUFFIX2);
            }

            output.addCell(view == null
                ? ""
                    : view);
            output.finishRow();
        }
    }

    private static String surveyUrl = CLDR_CONFIG.getProperty("CLDR_SURVEY_URL",
        "http://st.unicode.org/cldr-apps/survey");

    static private String METAZONE_PREFIX = "//ldml/dates/timeZoneNames/metazone[@type=\"";
    static private String METAZONE_SUFFIX = "\"]/long/generic";
    static private String METAZONE_SUFFIX2 = "\"]/long/standard";

    private static boolean hasDaylight(TimeZone tz) {
        int dateOffset = tz.getOffset(date);
        return dateOffset != tz.getRawOffset() || dateOffset != tz.getOffset(date6);
    }

    private static long getStandardDate(TimeZone tz) {
        return tz.getOffset(date) == tz.getRawOffset() ? date : date6;
    }

    private static long getDaylightDate(TimeZone tz) {
        return tz.getOffset(date) == tz.getRawOffset() ? date6 : date;
    }

    private static void captureErrors(Set<String> debugCreationErrors, Set<String> errors, String locale, String length) {
        if (debugCreationErrors.size() != 0) {
            for (String s : debugCreationErrors) {
                errors.add(locale + "\t" + length + "\t" + s);
            }
            debugCreationErrors.clear();
        }
    }
}
