package org.unicode.cldr.util;

import java.io.BufferedReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
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 java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.ibm.icu.dev.util.XEquivalenceClass;

public class ZoneParser {
    static final boolean DEBUG = false;

    private String version;

    private Map<String, String> zone_to_country;

    private Map<String, Set<String>> country_to_zoneSet;

    /**
     * @return mapping from zone id to country. If a zone has no country, then XX
     *         is used.
     */
    public Map<String, String> getZoneToCounty() {
        if (zone_to_country == null)
            make_zone_to_country();
        return zone_to_country;
    }

    /**
     * @return mapping from country to zoneid. If a zone has no country, then XX
     *         is used.
     */
    public Map<String, Set<String>> getCountryToZoneSet() {
        if (country_to_zoneSet == null)
            make_zone_to_country();
        return country_to_zoneSet;
    }

    /**
     * @return map from tzids to a list: latitude, longitude, country, comment?. + =
     *         N or E
     */
    public Map<String, List<String>> getZoneData() {
        if (zoneData == null)
            makeZoneData();
        return zoneData;
    }

    public List<String> getDeprecatedZoneIDs() {
        return Arrays.asList(FIX_DEPRECATED_ZONE_DATA);
    }

    /**
    * 
    */
    private void make_zone_to_country() {
        zone_to_country = new TreeMap<String, String>(TZIDComparator);
        country_to_zoneSet = new TreeMap<String, Set<String>>();
        // Map aliasMap = getAliasMap();
        Map<String, List<String>> zoneData = getZoneData();
        for (String zone : zoneData.keySet()) {
            String country = (String) zoneData.get(zone).get(2);
            zone_to_country.put(zone, country);
            Set<String> s = country_to_zoneSet.get(country);
            if (s == null)
                country_to_zoneSet.put(country, s = new TreeSet<String>());
            s.add(zone);
        }
        /*
         * Set territories = getAvailableCodes("territory"); for (Iterator it =
         * territories.iterator(); it.hasNext();) { String code = (String)
         * it.next(); String[] zones = TimeZone.getAvailableIDs(code); for (int i =
         * 0; i < zones.length; ++i) { if (aliasMap.get(zones[i]) != null) continue;
         * zone_to_country.put(zones[i], code); } } String[] zones =
         * TimeZone.getAvailableIDs(); for (int i = 0; i < zones.length; ++i) { if
         * (aliasMap.get(zones[i]) != null) continue; if
         * (zone_to_country.get(zones[i]) == null) { zone_to_country.put(zones[i],
         * NO_COUNTRY); } } for (Iterator it = zone_to_country.keySet().iterator();
         * it.hasNext();) { String tzid = (String) it.next(); String country =
         * (String) zone_to_country.get(tzid); Set s = (Set)
         * country_to_zoneSet.get(country); if (s == null)
         * country_to_zoneSet.put(country, s = new TreeSet()); s.add(tzid); }
         */
        // protect
        zone_to_country = Collections.unmodifiableMap(zone_to_country);
        country_to_zoneSet = CldrUtility.protectCollection(country_to_zoneSet);
    }

    /**
     * 
     * 
     * private Map bogusZones = null;
     * 
     * private Map getAliasMap() { if (bogusZones == null) { try { bogusZones =
     * new TreeMap(); BufferedReader in =
     * Utility.getUTF8Data"TimeZoneAliases.txt"); while (true) { String line =
     * in.readLine(); if (line == null) break; line = line.trim(); int pos =
     * line.indexOf('#'); if (pos >= 0) { skippedAliases.add(line); line =
     * line.substring(0,pos).trim(); } if (line.length() == 0) continue; List
     * pieces = Utility.splitList(line,';', true); bogusZones.put(pieces.get(0),
     * pieces.get(1)); } in.close(); } catch (IOException e) { throw new
     * IllegalArgumentException("Can't find timezone aliases"); } } return
     * bogusZones; }
     */

    Map<String, List<String>> zoneData;

    Set<String> skippedAliases = new TreeSet<String>();

    /*
     * # This file contains a table with the following columns: # 1. ISO 3166
     * 2-character country code. See the file `iso3166.tab'. # 2. Latitude and
     * longitude of the zone's principal location # in ISO 6709
     * sign-degrees-minutes-seconds format, # either +-DDMM+-DDDMM or
     * +-DDMMSS+-DDDMMSS, # first latitude (+ is north), then longitude (+ is
     * east). # 3. Zone name used in value of TZ environment variable. # 4.
     * Comments; present if and only if the country has multiple rows. # # Columns
     * are separated by a single tab.
     */
    static int parseYear(String year, int defaultValue) {
        if ("only".startsWith(year))
            return defaultValue;
        if ("minimum".startsWith(year))
            return Integer.MIN_VALUE;
        if ("maximum".startsWith(year))
            return Integer.MAX_VALUE;
        return Integer.parseInt(year);
    }

    public static class Time {
        public int seconds;
        public byte type;
        static final byte WALL = 0, STANDARD = 1, UNIVERSAL = 2;

        Time(String in) {
            if (in.equals("-")) return; // zero/WALL is the default
            char suffix = in.charAt(in.length() - 1);
            switch (suffix) {
            case 'w':
                in = in.substring(0, in.length() - 1);
                break;
            case 's':
                in = in.substring(0, in.length() - 1);
                type = STANDARD;
                break;
            case 'u':
            case 'g':
            case 'z':
                in = in.substring(0, in.length() - 1);
                type = UNIVERSAL;
                break;
            }
            seconds = parseSeconds(in, false);
        }

        public static int parseSeconds(String in, boolean allowNegative) {
            boolean negative = false;
            if (in.startsWith("-")) {
                assert (allowNegative);
                negative = true;
                in = in.substring(1);
            }
            String[] pieces = in.split(":");
            int multiplier = 3600;
            int result = 0;
            for (int i = 0; i < pieces.length; ++i) {
                result += multiplier * Integer.parseInt(pieces[i]);
                multiplier /= 60;
                assert (multiplier >= 0);
            }
            if (negative) result = -result;
            return result;
        }

        public String toString() {
            return BoilerplateUtilities.toStringHelper(this);
        }
    }

    static final String[] months = { "january", "february", "march", "april", "may", "june", "july", "august",
        "september", "october", "november", "december" };
    static final String[] weekdays = { "sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday" };

    static int findStartsWith(String value, String[] array, boolean exact) {
        value = value.toLowerCase(Locale.ENGLISH);
        for (int i = 0; i < array.length; ++i) {
            if (array[i].startsWith(value)) return i;
        }
        throw new IllegalArgumentException("Can't find " + value + " in " + Arrays.asList(months));
    }

    static Pattern dayPattern = Pattern.compile("([0-9]+)|(last)([a-z]+)|([a-z]+)([<=>]+)([0-9]+)");
    static final String[] relations = { "<=", ">=" };

    public static class Day implements Comparable<Object> {
        public int number;
        public byte relation;
        public int weekDay;
        static final byte NONE = 0, LEQ = 2, GEQ = 4;

        Day(String value) {
            value = value.toLowerCase();
            Matcher matcher = dayPattern.matcher(value);
            if (!matcher.matches()) {
                throw new IllegalArgumentException();
            }
            if (matcher.group(1) != null) {
                number = Integer.parseInt(matcher.group(1));
                return;
            }
            if (matcher.group(2) != null) {
                weekDay = findStartsWith(matcher.group(3), weekdays, false);
                number = 31;
                relation = LEQ;
                return;
            }
            if (matcher.group(4) != null) {
                weekDay = findStartsWith(matcher.group(4), weekdays, false);
                relation = (byte) findStartsWith(matcher.group(5), relations, false);
                number = Integer.parseInt(matcher.group(6));
                return;
            }
            throw new IllegalArgumentException();
        }

        public String toString() {
            return BoilerplateUtilities.toStringHelper(this);
        }

        public int compareTo(Object other) {
            return toString().compareTo(other.toString());
        }
    }

    /**
     * 
     A rule line has the form
     * 
     * Rule NAME FROM TO TYPE IN ON AT SAVE LETTER/S
     * 
     * For example:
     * 
     * Rule US 1967 1973 - Apr lastSun 2:00 1:00 D
     * 
     * The fields that make up a rule line are:
     * 
     * NAME Gives the (arbitrary) name of the set of rules this
     * rule is part of.
     * 
     * FROM Gives the first year in which the rule applies. Any
     * integer year can be supplied; the Gregorian calendar
     * is assumed. The word minimum (or an abbreviation)
     * means the minimum year representable as an integer.
     * The word maximum (or an abbreviation) means the
     * maximum year representable as an integer. Rules can
     * describe times that are not representable as time
     * values, with the unrepresentable times ignored; this
     * allows rules to be portable among hosts with
     * differing time value types.
     * 
     * TO Gives the final year in which the rule applies. In
     * addition to minimum and maximum (as above), the word
     * only (or an abbreviation) may be used to repeat the
     * value of the FROM field.
     * 
     * TYPE Gives the type of year in which the rule applies.
     * If TYPE is - then the rule applies in all years
     * between FROM and TO inclusive. If TYPE is something
     * else, then zic executes the command
     * yearistype year type
     * to check the type of a year: an exit status of zero
     * is taken to mean that the year is of the given type;
     * an exit status of one is taken to mean that the year
     * is not of the given type.
     * 
     * IN Names the month in which the rule takes effect.
     * Month names may be abbreviated.
     * 
     * ON Gives the day on which the rule takes effect.
     * Recognized forms include:
     * 
     * 5 the fifth of the month
     * lastSun the last Sunday in the month
     * lastMon the last Monday in the month
     * Sun>=8 first Sunday on or after the eighth
     * Sun<=25 last Sunday on or before the 25th
     * 
     * Names of days of the week may be abbreviated or
     * spelled out in full. Note that there must be no
     * spaces within the ON field.
     * 
     * AT Gives the time of day at which the rule takes
     * effect. Recognized forms include:
     * 
     * 2 time in hours
     * 2:00 time in hours and minutes
     * 15:00 24-hour format time (for times after noon)
     * 1:28:14 time in hours, minutes, and seconds
     * - equivalent to 0
     * 
     * where hour 0 is midnight at the start of the day,
     * and hour 24 is midnight at the end of the day. Any
     * of these forms may be followed by the letter w if
     * the given time is local "wall clock" time, s if the
     * given time is local "standard" time, or u (or g or
     * z) if the given time is universal time; in the
     * absence of an indicator, wall clock time is assumed.
     *** cannot be negative
     * 
     * SAVE Gives the amount of time to be added to local
     * standard time when the rule is in effect. This
     * field has the same format as the AT field (although,
     * of course, the w and s suffixes are not used).
     *** can be positive or negative
     * 
     * LETTER/S
     * Gives the "variable part" (for example, the "S" or
     * "D" in "EST" or "EDT") of time zone abbreviations to
     * be used when this rule is in effect. If this field
     * is -, the variable part is null.
     * 
     * 
     * 
     */

    public static class RuleLine {
        public static Set<String> types = new TreeSet<String>();
        public static Set<Day> days = new TreeSet<Day>();
        static Set<Integer> saves = new TreeSet<Integer>();

        RuleLine(List<String> l) {
            fromYear = parseYear(l.get(0), 0);
            toYear = parseYear(l.get(1), fromYear);
            type = l.get(2);
            if (type.equals("-")) type = null;
            month = 1 + findStartsWith((String) l.get(3), months, false);
            day = new Day(l.get(4));
            time = new Time(l.get(5));
            save = Time.parseSeconds(l.get(6), true);
            letter = l.get(7);
            if (letter.equals("-")) letter = null;
            if (type != null) types.add(type);
            days.add(day);
        }

        public String toString() {
            return BoilerplateUtilities.toStringHelper(this);
        }

        public int fromYear;

        public int toYear;

        public String type;

        public int month;

        public Day day;

        public Time time;

        public int save;

        public String letter;

        public static final int FIELD_COUNT = 8; // excluding Rule, Name
    }

    /**
     * A zone line has the form
     * 
     * Zone NAME GMTOFF RULES/SAVE FORMAT [UNTIL]
     * 
     * For example:
     * 
     * Zone Australia/Adelaide 9:30 Aus CST 1971 Oct 31 2:00
     * 
     * The fields that make up a zone line are:
     * 
     * NAME The name of the time zone. This is the name used in
     * creating the time conversion information file for the
     * zone.
     * 
     * GMTOFF
     * The amount of time to add to UTC to get standard time
     * in this zone. This field has the same format as the
     * AT and SAVE fields of rule lines; begin the field with
     * a minus sign if time must be subtracted from UTC.
     * 
     * RULES/SAVE
     * The name of the rule(s) that apply in the time zone
     * or, alternately, an amount of time to add to local
     * standard time. If this field is - then standard time
     * always applies in the time zone.
     * 
     * FORMAT
     * The format for time zone abbreviations in this time
     * zone. The pair of characters %s is used to show where
     * the "variable part" of the time zone abbreviation
     * goes. Alternately, a slash (/) separates standard and
     * daylight abbreviations.
     * 
     * UNTIL The time at which the UTC offset or the rule(s) change
     * for a location. It is specified as a year, a month, a
     * day, and a time of day. If this is specified, the
     * time zone information is generated from the given UTC
     * offset and rule change until the time specified. The
     * month, day, and time of day have the same format as
     * the IN, ON, and AT columns of a rule; trailing columns
     * can be omitted, and default to the earliest possible
     * value for the missing columns.
     * 
     * The next line must be a "continuation" line; this has
     * the same form as a zone line except that the string
     * "Zone" and the name are omitted, as the continuation
     * line will place information starting at the time
     * specified as the UNTIL field in the previous line in
     * the file used by the previous line. Continuation
     * lines may contain an UNTIL field, just as zone lines
     * do, indicating that the next line is a further
     * continuation.
     */
    public static class ZoneLine {
        public static Set<Day> untilDays = new TreeSet<Day>();
        public static Set<String> rulesSaves = new TreeSet<String>();

        ZoneLine(List<String> l) {
            gmtOff = Time.parseSeconds(l.get(0), true);
            rulesSave = (String) l.get(1);
            if (rulesSave.equals("-"))
                rulesSave = "0";
            else if (rulesSave.charAt(0) < 'A') rulesSave = "" + Time.parseSeconds(rulesSave, false);

            format = (String) l.get(2);
            switch (l.size()) {
            case 7:
                untilTime = new Time(l.get(6)); // fall through
            case 6:
                untilDay = new Day(l.get(5)); // fall through
                untilDays.add(untilDay);
            case 5:
                untilMonth = 1 + findStartsWith((String) l.get(4), months, false); // fall through
            case 4:
                untilYear = parseYear(l.get(3), Integer.MAX_VALUE); // fall through
            case 3:
                break; // ok
            default:
                throw new IllegalArgumentException("Wrong field count: " + l);
            }
            rulesSaves.add(rulesSave);
        }

        public String toString() {
            return BoilerplateUtilities.toStringHelper(this);
        }

        public int gmtOff;

        public String rulesSave;

        public String format;

        public int untilYear = Integer.MAX_VALUE; // indicating continuation

        public int untilMonth;

        public Day untilDay;

        public Time untilTime;

        public String comment;

        public static final int FIELD_COUNT = 3; // excluding Zone, Name

        public static final int FIELD_COUNT_UNTIL = 7; // excluding Zone, Name
    }

    Map<String, List<RuleLine>> ruleID_rules = new TreeMap<String, List<RuleLine>>();

    Map<String, List<ZoneLine>> zone_rules = new TreeMap<String, List<ZoneLine>>();

    Map<String, String> linkold_new = new TreeMap<String, String>();

    Map<String, Set<String>> linkNew_oldSet = new TreeMap<String, Set<String>>();

    public class Transition {
        public long date;
        public long offset;
        public String abbreviation;
    }

    public class TransitionList {

        void addTransitions(ZoneLine lastZoneLine, ZoneLine zoneLine, int startYear, int endYear) {
            // add everything between the zonelines
            if (lastZoneLine == null) {
                return;
            }
            startYear = Math.max(startYear, lastZoneLine.untilYear);
            endYear = Math.min(endYear, zoneLine.untilYear);
            int gmtOffset = lastZoneLine.gmtOff;
            for (int year = startYear; year <= endYear; ++year) {
                resolveTime(gmtOffset, lastZoneLine.untilYear, lastZoneLine.untilMonth,
                    lastZoneLine.untilDay, lastZoneLine.untilTime);
            }
        }

        private long resolveTime(int gmtOffset, int untilYear, int untilMonth, Day untilDay, Time untilTime) {
            return 0;
        }
    }

    public TransitionList getTransitions(String zoneID, int startYear, int endYear) {
        TransitionList results = new TransitionList();
        List<ZoneLine> rules = zone_rules.get(zoneID);
        ZoneLine lastZoneLine = null;
        for (ZoneLine zoneLine : rules) {
            results.addTransitions(lastZoneLine, zoneLine, startYear, endYear);
            lastZoneLine = zoneLine;
        }
        return results;
    }

    public Comparator<String> getTZIDComparator() {
        return TZIDComparator;
    }

    private static List<String> errorData = Arrays.asList(new String[] {
        new Double(Double.MIN_VALUE).toString(), new Double(Double.MIN_VALUE).toString(), "" });

    private Comparator<String> TZIDComparator = new Comparator<String>() {
        Map<String, List<String>> data = getZoneData();

        public int compare(String s1, String s2) {
            List<String> data1 = data.get(s1);
            if (data1 == null)
                data1 = errorData;
            List<String> data2 = data.get(s2);
            if (data2 == null)
                data2 = errorData;

            int result;
            // country
            String country1 = (String) data1.get(2);
            String country2 = (String) data2.get(2);

            if ((result = country1.compareTo(country2)) != 0)
                return result;
            // longitude
            Double d1 = Double.valueOf(data1.get(1));
            Double d2 = Double.valueOf(data2.get(1));
            if ((result = d1.compareTo(d2)) != 0)
                return result;
            // latitude
            d1 = Double.valueOf(data1.get(0));
            d2 = Double.valueOf(data2.get(0));
            if ((result = d1.compareTo(d2)) != 0)
                return result;
            // name
            return s1.compareTo(s2);
        }
    };

    public static MapComparator<String> regionalCompare = new MapComparator<String>();
    static {
        regionalCompare.add("America");
        regionalCompare.add("Atlantic");
        regionalCompare.add("Europe");
        regionalCompare.add("Africa");
        regionalCompare.add("Asia");
        regionalCompare.add("Indian");
        regionalCompare.add("Australia");
        regionalCompare.add("Pacific");
        regionalCompare.add("Arctic");
        regionalCompare.add("Antarctica");
        regionalCompare.add("Etc");
    }

    private static String[] TZFiles = { "africa", "antarctica", "asia",
        "australasia", "backward", "etcetera", "europe", "northamerica",
        "pacificnew", "southamerica", "systemv" };

    private static Map<String, String> FIX_UNSTABLE_TZIDS;

    //private static Set<String> SKIP_LINKS = new HashSet<String>(Arrays.asList(new String[] {
    //    "Navajo", "America/Shiprock" }));

    private static Set<String> PREFERRED_BASES = new HashSet<String>(Arrays.asList(new String[] { "Europe/London" }));

    private static String[][] ADD_ZONE_ALIASES_DATA = { { "Etc/UTC", "Etc/GMT" },
        { "Etc/UCT", "Etc/GMT" },
        // { "Navajo", "America/Shiprock" },
        // extras added in 2006g
        { "SystemV/AST4ADT", "America/Halifax" },
        { "SystemV/EST5EDT", "America/New_York" },
        { "EST5EDT", "America/New_York" },
        { "SystemV/CST6CDT", "America/Chicago" },
        { "CST6CDT", "America/Chicago" },
        { "SystemV/MST7MDT", "America/Denver" },
        { "MST7MDT", "America/Denver" },
        { "SystemV/PST8PDT", "America/Los_Angeles" },
        { "PST8PDT", "America/Los_Angeles" },
        { "SystemV/YST9YDT", "America/Anchorage" },

        { "SystemV/AST4", "Etc/GMT+4" },
        { "SystemV/EST5", "Etc/GMT+5" },
        { "EST", "Etc/GMT+5" },
        { "SystemV/CST6", "Etc/GMT+6" },
        { "SystemV/MST7", "Etc/GMT+7" },
        { "MST", "Etc/GMT+7" },
        { "SystemV/PST8", "Etc/GMT+8" },
        { "SystemV/YST9", "Etc/GMT+9" },
        { "SystemV/HST10", "Etc/GMT+10" },
        { "HST", "Etc/GMT+10" },
    };

    /*
     * ## Zone SystemV/AST4 -4:00 - AST
     * ## Zone SystemV/EST5 -5:00 - EST
     * ## Zone SystemV/CST6 -6:00 - CST
     * ## Zone SystemV/MST7 -7:00 - MST
     * ## Zone SystemV/PST8 -8:00 - PST
     * ## Zone SystemV/YST9 -9:00 - YST
     * ## Zone SystemV/HST10 -10:00 - HST
     */

    static String[] FIX_DEPRECATED_ZONE_DATA = {
        "Africa/Timbuktu",
        "America/Argentina/ComodRivadavia",
        "Europe/Belfast",
        "Pacific/Yap",
        "Antarctica/South_Pole",
        "America/Shiprock",
        "America/Montreal",
        "Asia/Chongqing",
        "Asia/Harbin",
        "Asia/Kashgar"
    };
    static {
        // The format is <new name>, <old name>
        String[][] FIX_UNSTABLE_TZID_DATA = new String[][] {
            { "America/Atikokan", "America/Coral_Harbour" },
            { "America/Argentina/Buenos_Aires", "America/Buenos_Aires" },
            { "America/Argentina/Catamarca", "America/Catamarca" },
            { "America/Argentina/Cordoba", "America/Cordoba" },
            { "America/Argentina/Jujuy", "America/Jujuy" },
            { "America/Argentina/Mendoza", "America/Mendoza" },
            { "America/Kentucky/Louisville", "America/Louisville" },
            { "America/Indiana/Indianapolis", "America/Indianapolis" },
            { "Africa/Asmara", "Africa/Asmera" },
            { "Atlantic/Faroe", "Atlantic/Faeroe" },
            { "Asia/Kolkata", "Asia/Calcutta" },
            { "Asia/Ho_Chi_Minh", "Asia/Saigon" },
            { "Asia/Kathmandu", "Asia/Katmandu" },
            { "Pacific/Pohnpei", "Pacific/Ponape" },
            { "Pacific/Chuuk", "Pacific/Truk" }
        };
        FIX_UNSTABLE_TZIDS = CldrUtility.asMap(FIX_UNSTABLE_TZID_DATA);
    }

    /**
    * 
    */
    private void makeZoneData() {
        try {
            // get version
            BufferedReader versionIn = CldrUtility.getUTF8Data("tzdb-version.txt");
            version = versionIn.readLine();
            if (!version.matches("[0-9]{4}[a-z]")) {
                throw new IllegalArgumentException(String.format("Bad Version number: %s, should be of the form 2007x",
                    version));
            }
            versionIn.close();

            // String deg = "([+-][0-9]+)";//
            String deg = "([+-])([0-9][0-9][0-9]?)([0-9][0-9])([0-9][0-9])?";//
            Matcher m = Pattern.compile(deg + deg).matcher("");
            zoneData = new TreeMap<String, List<String>>();
            BufferedReader in = CldrUtility.getUTF8Data("zone.tab");
            while (true) {
                String line = in.readLine();
                if (line == null)
                    break;
                line = line.trim();
                int pos = line.indexOf('#');
                if (pos >= 0) {
                    skippedAliases.add(line);
                    line = line.substring(0, pos).trim();
                }
                if (line.length() == 0)
                    continue;
                List<String> pieces = CldrUtility.splitList(line, '\t', true);
                String country = pieces.get(0);
                String latLong = pieces.get(1);
                String tzid = pieces.get(2);
                String ntzid = FIX_UNSTABLE_TZIDS.get(tzid);
                if (ntzid != null)
                    tzid = ntzid;
                String comment = pieces.size() < 4 ? null : (String) pieces.get(3);
                pieces.clear();
                if (!m.reset(latLong).matches())
                    throw new IllegalArgumentException("Bad zone.tab, lat/long format: "
                        + line);

                pieces.add(getDegrees(m, true).toString());
                pieces.add(getDegrees(m, false).toString());
                pieces.add(country);
                if (comment != null)
                    pieces.add(comment);
                if (zoneData.containsKey(tzid))
                    throw new IllegalArgumentException("Bad zone.tab, duplicate entry: "
                        + line);
                zoneData.put(tzid, pieces);
            }
            in.close();
            // add Etcs
            for (int i = -14; i <= 12; ++i) {
                List<String> pieces = new ArrayList<String>();
                int latitude = 0;
                int longitude = i * 15;
                if (longitude <= -180) {
                    longitude += 360;
                }
                pieces.add(new Double(latitude).toString()); // lat
                // remember that the sign of the TZIDs is wrong
                pieces.add(new Double(-longitude).toString()); // long
                pieces.add(StandardCodes.NO_COUNTRY); // country

                zoneData.put("Etc/GMT" + (i == 0 ? "" : i < 0 ? "" + i : "+" + i),
                    pieces);
            }
            // add Unknown
            List<String> pieces = new ArrayList<String>();
            pieces.add(new Double(0).toString()); // lat
            pieces.add(new Double(0).toString()); // long
            pieces.add(StandardCodes.NO_COUNTRY); // country
            zoneData.put("Etc/Unknown", pieces);

            zoneData = CldrUtility.protectCollection(zoneData); // protect for later

            // now get links
            Pattern whitespace = Pattern.compile("\\s+");
            XEquivalenceClass<String, String> linkedItems = new XEquivalenceClass<String, String>("None");
            for (int i = 0; i < TZFiles.length; ++i) {
                in = CldrUtility.getUTF8Data(TZFiles[i]);
                String zoneID = null;
                while (true) {
                    String line = in.readLine();
                    if (line == null)
                        break;
                    String originalLine = line;
                    int commentPos = line.indexOf("#");
                    String comment = null;
                    if (commentPos >= 0) {
                        comment = line.substring(commentPos + 1).trim();
                        line = line.substring(0, commentPos);
                    }
                    line = line.trim();
                    if (line.length() == 0)
                        continue;
                    String[] items = whitespace.split(line);
                    if (zoneID != null || items[0].equals("Zone")) {
                        List<String> l = new ArrayList<String>();
                        l.addAll(Arrays.asList(items));

                        // Zone Africa/Algiers 0:12:12 - LMT 1891 Mar 15 0:01
                        // 0:09:21 - PMT 1911 Mar 11 # Paris Mean Time
                        if (zoneID == null) {
                            l.remove(0); // "Zone"
                            zoneID = (String) l.get(0);
                            String ntzid = (String) FIX_UNSTABLE_TZIDS.get(zoneID);
                            if (ntzid != null)
                                zoneID = ntzid;
                            l.remove(0);
                        }
                        List<ZoneLine> zoneRules = zone_rules.get(zoneID);
                        if (zoneRules == null) {
                            zoneRules = new ArrayList<ZoneLine>();
                            zone_rules.put(zoneID, zoneRules);
                        }

                        if (l.size() < ZoneLine.FIELD_COUNT
                            || l.size() > ZoneLine.FIELD_COUNT_UNTIL) {
                            System.out.println("***Zone incorrect field count:");
                            System.out.println(l);
                            System.out.println(originalLine);
                        }

                        ZoneLine zoneLine = new ZoneLine(l);
                        zoneLine.comment = comment;
                        zoneRules.add(zoneLine);
                        if (l.size() == ZoneLine.FIELD_COUNT) {
                            zoneID = null; // no continuation line
                        }
                    } else if (items[0].equals("Rule")) {
                        // # Rule NAME FROM TO TYPE IN ON AT SAVE LETTER/S
                        // Rule Algeria 1916 only - Jun 14 23:00s 1:00 S

                        String ruleID = items[1];
                        List<RuleLine> ruleList = ruleID_rules.get(ruleID);
                        if (ruleList == null) {
                            ruleList = new ArrayList<RuleLine>();
                            ruleID_rules.put(ruleID, ruleList);
                        }
                        List<String> l = new ArrayList<String>();
                        l.addAll(Arrays.asList(items));
                        l.remove(0);
                        l.remove(0);
                        if (l.size() != RuleLine.FIELD_COUNT) {
                            System.out.println("***Rule incorrect field count:");
                            System.out.println(l);
                        }
                        if (comment != null)
                            l.add(comment);
                        RuleLine ruleLine = new RuleLine(l);
                        ruleList.add(ruleLine);

                    } else if (items[0].equals("Link")) {
                        String old = items[2];
                        String newOne = items[1];
                        //if (!SKIP_LINKS.contains(old) && !SKIP_LINKS.contains(newOne)) {
                        // System.out.println("Original " + old + "\t=>\t" + newOne);
                        linkedItems.add(old, newOne);
                        //}
                        /*
                         * String conflict = (String) linkold_new.get(old); if (conflict !=
                         * null) { System.out.println("Conflict with old: " + old + " => " +
                         * conflict + ", " + newOne); } System.out.println(old + "\t=>\t" +
                         * newOne); linkold_new.put(old, newOne);
                         */
                    } else {
                        if (DEBUG)
                            System.out.println("Unknown zone line: " + line);
                    }
                }
                in.close();
            }
            // add in stuff that should be links
            for (int i = 0; i < ADD_ZONE_ALIASES_DATA.length; ++i) {
                linkedItems.add(ADD_ZONE_ALIASES_DATA[i][0],
                    ADD_ZONE_ALIASES_DATA[i][1]);
            }
            // linkedItems.add("Etc/UTC", "Etc/GMT");
            // linkedItems.add("Etc/UCT", "Etc/GMT");
            // linkedItems.add("Navajo", "America/Shiprock");

            Set<String> isCanonical = zoneData.keySet();

            // walk through the sets, and
            // if any set contains two canonical items, split it.
            // if any contains one, make it the primary
            // if any contains zero, problem!
            for (Set<String> equivalents : linkedItems.getEquivalenceSets()) {
                Set<String> canonicals = new TreeSet<String>(equivalents);
                canonicals.retainAll(isCanonical);
                if (canonicals.size() == 0)
                    throw new IllegalArgumentException("No canonicals in: " + equivalents);
                if (canonicals.size() > 1) {
                    if (DEBUG) {
                        System.out.println("Too many canonicals in: " + equivalents);
                        System.out
                            .println("\t*Don't* put these into the same equivalence class: "
                                + canonicals);
                    }
                    Set<String> remainder = new TreeSet<String>(equivalents);
                    remainder.removeAll(isCanonical);
                    if (remainder.size() != 0) {
                        if (DEBUG) {
                            System.out
                                .println("\tThe following should be equivalent to others: "
                                    + remainder);
                        }
                    }
                }
                {
                    String newOne;
                    // get the item that we want to hang all the aliases off of.
                    // normally this is the first (alphabetically) one, but
                    // it may be overridden with PREFERRED_BASES
                    Set<String> preferredItems = new HashSet<String>(PREFERRED_BASES);
                    preferredItems.retainAll(canonicals);
                    if (preferredItems.size() > 0) {
                        newOne = preferredItems.iterator().next();
                    } else {
                        newOne = canonicals.iterator().next();
                    }
                    for (String oldOne : equivalents) {
                        if (canonicals.contains(oldOne))
                            continue;
                        // System.out.println("Mapping " + oldOne + "\t=>\t" + newOne);
                        linkold_new.put(oldOne, newOne);
                    }
                }
            }

            /*
             * // fix the links from old to new, to remove chains for (Iterator it =
             * linkold_new.keySet().iterator(); it.hasNext();) { Object oldItem =
             * it.next(); Object newItem = linkold_new.get(oldItem); while (true) {
             * Object linkItem = linkold_new.get(newItem); if (linkItem == null)
             * break; if (true) System.out.println("Connecting link chain: " + oldItem +
             * "\t=> " + newItem + "\t=> " + linkItem); newItem = linkItem;
             * linkold_new.put(oldItem, newItem); } }
             * // reverse the links *from* canonical names for (Iterator it =
             * linkold_new.keySet().iterator(); it.hasNext();) { Object oldItem =
             * it.next(); if (!isCanonical.contains(oldItem)) continue; Object newItem =
             * linkold_new.get(oldItem); }
             * 
             * // fix unstable TZIDs Set itemsToRemove = new HashSet(); Map
             * itemsToAdd = new HashMap(); for (Iterator it =
             * linkold_new.keySet().iterator(); it.hasNext();) { Object oldItem =
             * it.next(); Object newItem = linkold_new.get(oldItem); Object modOldItem =
             * RESTORE_UNSTABLE_TZIDS.get(oldItem); Object modNewItem =
             * FIX_UNSTABLE_TZIDS.get(newItem); if (modOldItem == null && modNewItem ==
             * null) continue; if (modOldItem == null) { // just fix old entry
             * itemsToAdd.put(oldItem, modNewItem); continue; } // otherwise have to
             * nuke and redo itemsToRemove.add(oldItem); if (modNewItem == null)
             * modNewItem = newItem; itemsToAdd.put(modOldItem, modNewItem); } // now
             * make fixes (we couldn't earlier because we were iterating
             * Utility.removeAll(linkold_new, itemsToRemove);
             * linkold_new.putAll(itemsToAdd);
             * // now remove all links that are from canonical zones
             * Utility.removeAll(linkold_new, zoneData.keySet());
             */

            // generate list of new to old
            for (Iterator<String> it = linkold_new.keySet().iterator(); it.hasNext();) {
                String oldZone = it.next();
                String newZone = linkold_new.get(oldZone);
                Set<String> s = linkNew_oldSet.get(newZone);
                if (s == null)
                    linkNew_oldSet.put(newZone, s = new HashSet<String>());
                s.add(oldZone);
            }

            // PROTECT EVERYTHING
            linkNew_oldSet = CldrUtility.protectCollection(linkNew_oldSet);
            linkold_new = CldrUtility.protectCollection(linkold_new);
            ruleID_rules = CldrUtility.protectCollection(ruleID_rules);
            zone_rules = CldrUtility.protectCollection(zone_rules);
            // TODO protect zone info later
        } catch (IOException e) {
            throw (IllegalArgumentException) new IllegalArgumentException(
                "Can't find timezone aliases: " + e.toString()).initCause(e);
        }
    }

    /**
     * @param m
     */
    private Double getDegrees(Matcher m, boolean lat) {
        int startIndex = lat ? 1 : 5;
        double amount = Integer.parseInt(m.group(startIndex + 1))
            + Integer.parseInt(m.group(startIndex + 2)) / 60.0;
        if (m.group(startIndex + 3) != null)
            amount += Integer.parseInt(m.group(startIndex + 3)) / 3600.0;
        if (m.group(startIndex).equals("-"))
            amount = -amount;
        return new Double(amount);
    }

    /**
     * @return Returns the linkold_new.
     */
    public Map<String, String> getZoneLinkold_new() {
        getZoneData();
        return linkold_new;
    }

    /**
     * @return Returns the linkold_new.
     */
    public Map<String, Set<String>> getZoneLinkNew_OldSet() {
        getZoneData();
        return linkNew_oldSet;
    }

    /**
     * @return Returns the ruleID_rules.
     */
    public Map<String, List<RuleLine>> getZoneRuleID_rules() {
        getZoneData();
        return ruleID_rules;
    }

    /**
     * @return Returns the zone_rules.
     */
    public Map<String, List<ZoneLine>> getZone_rules() {
        getZoneData();
        return zone_rules;
    }

    public String getVersion() {
        return version;
    }

}