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

import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.MissingResourceException;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import com.ibm.icu.impl.TextTrieMap.ResultHandler;
import com.ibm.icu.text.TimeZoneNames;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.UResourceBundle;

/**
 * Yet another TimeZoneNames implementation based on the tz database.
 * This implementation contains only tz abbreviations (short standard
 * and daylight names) for each metazone.
 *
 * The data file $ICU4C_ROOT/source/data/zone/tzdbNames.txt contains
 * the metazone - abbreviations mapping data (manually edited).
 *
 * Note: The abbreviations in the tz database are not necessarily
 * unique. For example, parsing abbreviation "IST" is ambiguous
 * (can be parsed as India Standard Time or Israel Standard Time).
 * The data file (tzdbNames.txt) contains regional mapping, and
 * the locale in the constructor is used as a hint for resolving
 * these ambiguous names.
 */
public class TZDBTimeZoneNames extends TimeZoneNames {
    private static final long serialVersionUID = 1L;

    private static final ConcurrentHashMap<String, TZDBNames> TZDB_NAMES_MAP =
            new ConcurrentHashMap<String, TZDBNames>();

    private static volatile TextTrieMap<TZDBNameInfo> TZDB_NAMES_TRIE = null;

    private static final ICUResourceBundle ZONESTRINGS;
    static {
        UResourceBundle bundle = ICUResourceBundle
                .getBundleInstance(ICUData.ICU_ZONE_BASE_NAME, "tzdbNames");
        ZONESTRINGS = (ICUResourceBundle)bundle.get("zoneStrings");
    }

    private ULocale _locale;
    private transient volatile String _region;

    public TZDBTimeZoneNames(ULocale loc) {
        _locale = loc;
    }

    /* (non-Javadoc)
     * @see com.ibm.icu.text.TimeZoneNames#getAvailableMetaZoneIDs()
     */
    @Override
    public Set<String> getAvailableMetaZoneIDs() {
        return TimeZoneNamesImpl._getAvailableMetaZoneIDs();
    }

    /* (non-Javadoc)
     * @see com.ibm.icu.text.TimeZoneNames#getAvailableMetaZoneIDs(java.lang.String)
     */
    @Override
    public Set<String> getAvailableMetaZoneIDs(String tzID) {
        return TimeZoneNamesImpl._getAvailableMetaZoneIDs(tzID);
    }

    /* (non-Javadoc)
     * @see com.ibm.icu.text.TimeZoneNames#getMetaZoneID(java.lang.String, long)
     */
    @Override
    public String getMetaZoneID(String tzID, long date) {
        return TimeZoneNamesImpl._getMetaZoneID(tzID, date);
    }

    /* (non-Javadoc)
     * @see com.ibm.icu.text.TimeZoneNames#getReferenceZoneID(java.lang.String, java.lang.String)
     */
    @Override
    public String getReferenceZoneID(String mzID, String region) {
        return TimeZoneNamesImpl._getReferenceZoneID(mzID, region);
    }

    /* (non-Javadoc)
     * @see com.ibm.icu.text.TimeZoneNames#getMetaZoneDisplayName(java.lang.String,
     *      com.ibm.icu.text.TimeZoneNames.NameType)
     */
    @Override
    public String getMetaZoneDisplayName(String mzID, NameType type) {
        if (mzID == null || mzID.length() == 0 ||
                (type != NameType.SHORT_STANDARD && type != NameType.SHORT_DAYLIGHT)) {
            return null;
        }
        return getMetaZoneNames(mzID).getName(type);
    }

    /* (non-Javadoc)
     * @see com.ibm.icu.text.TimeZoneNames#getTimeZoneDisplayName(java.lang.String,
     *      com.ibm.icu.text.TimeZoneNames.NameType)
     */
    @Override
    public String getTimeZoneDisplayName(String tzID, NameType type) {
        // No abbreviations associated a zone directly for now.
        return null;
    }

//    /* (non-Javadoc)
//     * @see com.ibm.icu.text.TimeZoneNames#getExemplarLocationName(java.lang.String)
//     */
//    public String getExemplarLocationName(String tzID) {
//        return super.getExemplarLocationName(tzID);
//    }

    /* (non-Javadoc)
     * @see com.ibm.icu.text.TimeZoneNames#find(java.lang.CharSequence, int, java.util.EnumSet)
     */
    @Override
    public Collection<MatchInfo> find(CharSequence text, int start, EnumSet<NameType> nameTypes) {
        if (text == null || text.length() == 0 || start < 0 || start >= text.length()) {
            throw new IllegalArgumentException("bad input text or range");
        }

        prepareFind();
        TZDBNameSearchHandler handler = new TZDBNameSearchHandler(nameTypes, getTargetRegion());
        TZDB_NAMES_TRIE.find(text, start, handler);
        return handler.getMatches();
    }

    private static class TZDBNames {
        public static final TZDBNames EMPTY_TZDBNAMES = new TZDBNames(null, null);

        private String[] _names;
        private String[] _parseRegions;
        private static final String[] KEYS = {"ss", "sd"};

        private TZDBNames(String[] names, String[] parseRegions) {
            _names = names;
            _parseRegions = parseRegions;
        }

        static TZDBNames getInstance(ICUResourceBundle zoneStrings, String key) {
            if (zoneStrings == null || key == null || key.length() == 0) {
                return EMPTY_TZDBNAMES;
            }

            ICUResourceBundle table = null;
            try {
                table = (ICUResourceBundle)zoneStrings.get(key);
            } catch (MissingResourceException e) {
                return EMPTY_TZDBNAMES;
            }

            boolean isEmpty = true;
            String[] names = new String[KEYS.length];
            for (int i = 0; i < names.length; i++) {
                try {
                    names[i] = table.getString(KEYS[i]);
                    isEmpty = false;
                } catch (MissingResourceException e) {
                    names[i] = null;
                }
            }

            if (isEmpty) {
                return EMPTY_TZDBNAMES;
            }

            String[] parseRegions = null;
            try {
                ICUResourceBundle regionsRes = (ICUResourceBundle)table.get("parseRegions");
                if (regionsRes.getType() == UResourceBundle.STRING) {
                    parseRegions = new String[1];
                    parseRegions[0] = regionsRes.getString();
                } else if (regionsRes.getType() == UResourceBundle.ARRAY) {
                    parseRegions = regionsRes.getStringArray();
                }
            } catch (MissingResourceException e) {
                // fall through
            }

            return new TZDBNames(names, parseRegions);
        }

        String getName(NameType type) {
            if (_names == null) {
                return null;
            }
            String name = null;
            switch (type) {
            case SHORT_STANDARD:
                name = _names[0];
                break;
            case SHORT_DAYLIGHT:
                name = _names[1];
                break;
            default:
                // No names for all other types handled by
                // this class.
                break;
            }

            return name;
        }

        String[] getParseRegions() {
            return _parseRegions;
        }
    }

    private static class TZDBNameInfo {
        final String mzID;
        final NameType type;
        final boolean ambiguousType;
        final String[] parseRegions;

        TZDBNameInfo(String mzID, NameType type, boolean ambiguousType, String[] parseRegions) {
            this.mzID = mzID;
            this.type = type;
            this.ambiguousType = ambiguousType;
            this.parseRegions = parseRegions;
        }
    }

    private static class TZDBNameSearchHandler implements ResultHandler<TZDBNameInfo> {
        private EnumSet<NameType> _nameTypes;
        private Collection<MatchInfo> _matches;
        private String _region;

        TZDBNameSearchHandler(EnumSet<NameType> nameTypes, String region) {
            _nameTypes = nameTypes;
            assert region != null;
            _region = region;
        }

        /* (non-Javadoc)
         * @see com.ibm.icu.impl.TextTrieMap.ResultHandler#handlePrefixMatch(int,
         *      java.util.Iterator)
         */
        @Override
        public boolean handlePrefixMatch(int matchLength, Iterator<TZDBNameInfo> values) {
            TZDBNameInfo match = null;
            TZDBNameInfo defaultRegionMatch = null;

            while (values.hasNext()) {
                TZDBNameInfo ninfo = values.next();

                if (_nameTypes != null && !_nameTypes.contains(ninfo.type)) {
                    continue;
                }

                // Some tz database abbreviations are ambiguous. For example,
                // CST means either Central Standard Time or China Standard Time.
                // Unlike CLDR time zone display names, this implementation
                // does not use unique names. And TimeZoneFormat does not expect
                // multiple results returned for the same time zone type.
                // For this reason, this implementation resolve one among same
                // zone type with a same name at this level.
                if (ninfo.parseRegions == null) {
                    // parseRegions == null means this is the default metazone
                    // mapping for the abbreviation.
                    if (defaultRegionMatch == null) {
                        match = defaultRegionMatch = ninfo;
                    }
                } else {
                    boolean matchRegion = false;
                    // non-default metazone mapping for an abbreviation
                    // comes with applicable regions. For example, the default
                    // metazone mapping for "CST" is America_Central,
                    // but if region is one of CN/MO/TW, "CST" is parsed
                    // as metazone China (China Standard Time).
                    for (String region : ninfo.parseRegions) {
                        if (_region.equals(region)) {
                            match = ninfo;
                            matchRegion = true;
                            break;
                        }
                    }
                    if (matchRegion) {
                        break;
                    }
                    if (match == null) {
                        match = ninfo;
                    }
                }
            }

            if (match != null) {
                NameType ntype = match.type;
                // Note: Workaround for duplicated standard/daylight names
                // The tz database contains a few zones sharing a
                // same name for both standard time and daylight saving
                // time. For example, Australia/Sydney observes DST,
                // but "EST" is used for both standard and daylight.
                // When both SHORT_STANDARD and SHORT_DAYLIGHT are included
                // in the find operation, we cannot tell which one was
                // actually matched.
                // TimeZoneFormat#parse returns a matched name type (standard
                // or daylight) and DateFormat implementation uses the info to
                // to adjust actual time. To avoid false type information,
                // this implementation replaces the name type with SHORT_GENERIC.
                if (match.ambiguousType
                        && (ntype == NameType.SHORT_STANDARD || ntype == NameType.SHORT_DAYLIGHT)
                        && _nameTypes.contains(NameType.SHORT_STANDARD)
                        && _nameTypes.contains(NameType.SHORT_DAYLIGHT)) {
                    ntype = NameType.SHORT_GENERIC;
                }
                MatchInfo minfo = new MatchInfo(ntype, null, match.mzID, matchLength);
                if (_matches == null) {
                    _matches = new LinkedList<MatchInfo>();
                }
                _matches.add(minfo);
            }

            return true;
        }

        /**
         * Returns the match results
         * @return the match results
         */
        public Collection<MatchInfo> getMatches() {
            if (_matches == null) {
                return Collections.emptyList();
            }
            return _matches;
        }
    }

    private static TZDBNames getMetaZoneNames(String mzID) {
        TZDBNames names = TZDB_NAMES_MAP.get(mzID);
        if (names == null) {
            names = TZDBNames.getInstance(ZONESTRINGS, "meta:" + mzID);
            mzID = mzID.intern();
            TZDBNames tmpNames = TZDB_NAMES_MAP.putIfAbsent(mzID, names);
            names = (tmpNames == null) ? names : tmpNames;
        }
        return names;
    }

    private static void prepareFind() {
        if (TZDB_NAMES_TRIE == null) {
            synchronized(TZDBTimeZoneNames.class) {
                if (TZDB_NAMES_TRIE == null) {
                    // loading all names into trie
                    TextTrieMap<TZDBNameInfo> trie = new TextTrieMap<TZDBNameInfo>(true);
                    Set<String> mzIDs = TimeZoneNamesImpl._getAvailableMetaZoneIDs();
                    for (String mzID : mzIDs) {
                        TZDBNames names = getMetaZoneNames(mzID);
                        String std = names.getName(NameType.SHORT_STANDARD);
                        String dst = names.getName(NameType.SHORT_DAYLIGHT);
                        if (std == null && dst == null) {
                            continue;
                        }
                        String[] parseRegions = names.getParseRegions();
                        mzID = mzID.intern();

                        // The tz database contains a few zones sharing a
                        // same name for both standard time and daylight saving
                        // time. For example, Australia/Sydney observes DST,
                        // but "EST" is used for both standard and daylight.
                        // we need to store the information for later processing.
                        boolean ambiguousType = (std != null && dst != null && std.equals(dst));

                        if (std != null) {
                            TZDBNameInfo stdInf = new TZDBNameInfo(mzID,
                                    NameType.SHORT_STANDARD,
                                    ambiguousType,
                                    parseRegions);
                            trie.put(std, stdInf);
                        }
                        if (dst != null) {
                            TZDBNameInfo dstInf = new TZDBNameInfo(mzID,
                                    NameType.SHORT_DAYLIGHT,
                                    ambiguousType,
                                    parseRegions);
                            trie.put(dst, dstInf);
                        }
                    }
                    TZDB_NAMES_TRIE = trie;
                }
            }
        }
    }

    private String getTargetRegion() {
        if (_region == null) {
            String region = _locale.getCountry();
            if (region.length() == 0) {
                ULocale tmp = ULocale.addLikelySubtags(_locale);
                region = tmp.getCountry();
                if (region.length() == 0) {
                    region = "001";
                }
            }
            _region = region;
        }
        return _region;
    }
}
