content: /*
   * Copyright (C) 2013 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.timezonepicker;
  
  import android.content.Context;
  import android.content.res.AssetManager;
  import android.content.res.Resources;
  import android.text.format.DateFormat;
  import android.text.format.DateUtils;
  import android.util.Log;
  import android.util.SparseArray;
  
  import java.io.BufferedReader;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.InputStreamReader;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.Date;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.LinkedHashMap;
  import java.util.Locale;
  import java.util.TimeZone;
  
  public class TimeZoneData {
      private static final String TAG = "TimeZoneData";
      private static final boolean DEBUG = false;
      private static final int OFFSET_ARRAY_OFFSET = 20;
  
      private static final String PALESTINE_COUNTRY_CODE = "PS";
  
  
      ArrayList<TimeZoneInfo> mTimeZones;
      LinkedHashMap<String, ArrayList<Integer>> mTimeZonesByCountry;
      HashSet<String> mTimeZoneNames = new HashSet<String>();
  
      private long mTimeMillis;
      private HashMap<String, String> mCountryCodeToNameMap = new HashMap<String, String>();
  
      public String mDefaultTimeZoneId;
      public static boolean is24HourFormat;
      private TimeZoneInfo mDefaultTimeZoneInfo;
      private String mAlternateDefaultTimeZoneId;
      private String mDefaultTimeZoneCountry;
      private HashMap<String, TimeZoneInfo> mTimeZonesById;
      private boolean[] mHasTimeZonesInHrOffset = new boolean[40];
      SparseArray<ArrayList<Integer>> mTimeZonesByOffsets;
      private Context mContext;
      private String mPalestineDisplayName;
  
      public TimeZoneData(Context context, String defaultTimeZoneId, long timeMillis) {
          mContext = context;
          is24HourFormat = TimeZoneInfo.is24HourFormat = DateFormat.is24HourFormat(context);
          mDefaultTimeZoneId = mAlternateDefaultTimeZoneId = defaultTimeZoneId;
          long now = System.currentTimeMillis();
  
          if (timeMillis == 0) {
              mTimeMillis = now;
          } else {
              mTimeMillis = timeMillis;
          }
  
          mPalestineDisplayName = context.getResources().getString(R.string.palestine_display_name);
  
          loadTzs(context);
  
          Log.i(TAG, "Time to load time zones (ms): " + (System.currentTimeMillis() - now));
  
          // now = System.currentTimeMillis();
          // printTz();
          // Log.i(TAG, "Time to print time zones (ms): " +
          // (System.currentTimeMillis() - now));
      }
  
      public void setTime(long timeMillis) {
          mTimeMillis = timeMillis;
      }
  
      public TimeZoneInfo get(int position) {
          return mTimeZones.get(position);
      }
  
      public int size() {
          return mTimeZones.size();
      }
  
      public int getDefaultTimeZoneIndex() {
          return mTimeZones.indexOf(mDefaultTimeZoneInfo);
      }
  
      // TODO speed this up
      public int findIndexByTimeZoneIdSlow(String timeZoneId) {
          int idx = 0;
          for (TimeZoneInfo tzi : mTimeZones) {
              if (timeZoneId.equals(tzi.mTzId)) {
                  return idx;
              }
              idx++;
          }
          return -1;
      }
  
      void loadTzs(Context context) {
          mTimeZones = new ArrayList<TimeZoneInfo>();
          HashSet<String> processedTimeZones = loadTzsInZoneTab(context);
          String[] tzIds = TimeZone.getAvailableIDs();
  
          if (DEBUG) {
              Log.e(TAG, "Available time zones: " + tzIds.length);
          }
  
          for (String tzId : tzIds) {
              if (processedTimeZones.contains(tzId)) {
                  continue;
              }
  
              /*
               * Dropping non-GMT tzs without a country code. They are not really
               * needed and they are dups but missing proper country codes. e.g.
               * WET CET MST7MDT PST8PDT Asia/Khandyga Asia/Ust-Nera EST
               */
              if (!tzId.startsWith("Etc/GMT")) {
                  continue;
              }
  
              final TimeZone tz = TimeZone.getTimeZone(tzId);
              if (tz == null) {
                  Log.e(TAG, "Timezone not found: " + tzId);
                  continue;
              }
  
              TimeZoneInfo tzInfo = new TimeZoneInfo(tz, null);
  
              if (getIdenticalTimeZoneInTheCountry(tzInfo) == -1) {
                  if (DEBUG) {
                      Log.e(TAG, "# Adding time zone from getAvailId: " + tzInfo.toString());
                  }
                  mTimeZones.add(tzInfo);
              } else {
                  if (DEBUG) {
                      Log.e(TAG,
                              "# Dropping identical time zone from getAvailId: " + tzInfo.toString());
                  }
                  continue;
              }
              //
              // TODO check for dups
              // checkForNameDups(tz, tzInfo.mCountry, false /* dls */,
              // TimeZone.SHORT, groupIdx, !found);
              // checkForNameDups(tz, tzInfo.mCountry, false /* dls */,
              // TimeZone.LONG, groupIdx, !found);
              // if (tz.useDaylightTime()) {
              // checkForNameDups(tz, tzInfo.mCountry, true /* dls */,
              // TimeZone.SHORT, groupIdx,
              // !found);
              // checkForNameDups(tz, tzInfo.mCountry, true /* dls */,
              // TimeZone.LONG, groupIdx,
              // !found);
              // }
          }
  
          // Don't change the order of mTimeZones after this sort
          Collections.sort(mTimeZones);
  
          mTimeZonesByCountry = new LinkedHashMap<String, ArrayList<Integer>>();
          mTimeZonesByOffsets = new SparseArray<ArrayList<Integer>>(mHasTimeZonesInHrOffset.length);
          mTimeZonesById = new HashMap<String, TimeZoneInfo>(mTimeZones.size());
          for (TimeZoneInfo tz : mTimeZones) {
              // /////////////////////
              // Lookup map for id -> tz
              mTimeZonesById.put(tz.mTzId, tz);
          }
          populateDisplayNameOverrides(mContext.getResources());
  
          Date date = new Date(mTimeMillis);
          Locale defaultLocal = Locale.getDefault();
  
          int idx = 0;
          for (TimeZoneInfo tz : mTimeZones) {
              // /////////////////////
              // Populate display name
              if (tz.mDisplayName == null) {
                  tz.mDisplayName = tz.mTz.getDisplayName(tz.mTz.inDaylightTime(date),
                          TimeZone.LONG, defaultLocal);
              }
  
              // /////////////////////
              // Grouping tz's by country for search by country
              ArrayList<Integer> group = mTimeZonesByCountry.get(tz.mCountry);
              if (group == null) {
                  group = new ArrayList<Integer>();
                  mTimeZonesByCountry.put(tz.mCountry, group);
              }
  
              group.add(idx);
  
              // /////////////////////
              // Grouping tz's by GMT offsets
              indexByOffsets(idx, tz);
  
              // Skip all the GMT+xx:xx style display names from search
              if (!tz.mDisplayName.endsWith(":00")) {
                  mTimeZoneNames.add(tz.mDisplayName);
              } else if (DEBUG) {
                  Log.e(TAG, "# Hiding from pretty name search: " +
                          tz.mDisplayName);
              }
  
              idx++;
          }
  
          // printTimeZones();
      }
  
      private void printTimeZones() {
          TimeZoneInfo last = null;
          boolean first = true;
          for (TimeZoneInfo tz : mTimeZones) {
              // All
              if (false) {
                  Log.e("ALL", tz.toString());
              }
  
              // GMT
              if (true) {
                  String name = tz.mTz.getDisplayName();
                  if (name.startsWith("GMT") && !tz.mTzId.startsWith("Etc/GMT")) {
                      Log.e("GMT", tz.toString());
                  }
              }
  
              // Dups
              if (true && last != null) {
                  if (last.compareTo(tz) == 0) {
                      if (first) {
                          Log.e("SAME", last.toString());
                          first = false;
                      }
                      Log.e("SAME", tz.toString());
                  } else {
                      first = true;
                  }
              }
              last = tz;
          }
          Log.e(TAG, "Total number of tz's = " + mTimeZones.size());
      }
  
      private void populateDisplayNameOverrides(Resources resources) {
          String[] ids = resources.getStringArray(R.array.timezone_rename_ids);
          String[] labels = resources.getStringArray(R.array.timezone_rename_labels);
  
          int length = ids.length;
          if (ids.length != labels.length) {
              Log.e(TAG, "timezone_rename_ids len=" + ids.length + " timezone_rename_labels len="
                      + labels.length);
              length = Math.min(ids.length, labels.length);
          }
  
          for (int i = 0; i < length; i++) {
              TimeZoneInfo tzi = mTimeZonesById.get(ids[i]);
              if (tzi != null) {
                  tzi.mDisplayName = labels[i];
              } else {
                  Log.e(TAG, "Could not find timezone with label: "+labels[i]);
              }
          }
      }
  
      public boolean hasTimeZonesInHrOffset(int offsetHr) {
          int index = OFFSET_ARRAY_OFFSET + offsetHr;
          if (index >= mHasTimeZonesInHrOffset.length || index < 0) {
              return false;
          }
          return mHasTimeZonesInHrOffset[index];
      }
  
      private void indexByOffsets(int idx, TimeZoneInfo tzi) {
          int offsetMillis = tzi.getNowOffsetMillis();
          int index = OFFSET_ARRAY_OFFSET + (int) (offsetMillis / DateUtils.HOUR_IN_MILLIS);
          mHasTimeZonesInHrOffset[index] = true;
  
          ArrayList<Integer> group = mTimeZonesByOffsets.get(index);
          if (group == null) {
              group = new ArrayList<Integer>();
              mTimeZonesByOffsets.put(index, group);
          }
          group.add(idx);
      }
  
      public ArrayList<Integer> getTimeZonesByOffset(int offsetHr) {
          int index = OFFSET_ARRAY_OFFSET + offsetHr;
          if (index >= mHasTimeZonesInHrOffset.length || index < 0) {
              return null;
          }
          return mTimeZonesByOffsets.get(index);
      }
  
      private HashSet<String> loadTzsInZoneTab(Context context) {
          HashSet<String> processedTimeZones = new HashSet<String>();
          AssetManager am = context.getAssets();
          InputStream is = null;
  
          /*
           * The 'backward' file contain mappings between new and old time zone
           * ids. We will explicitly ignore the old ones.
           */
          try {
              is = am.open("backward");
              BufferedReader reader = new BufferedReader(new InputStreamReader(is));
              String line;
  
              while ((line = reader.readLine()) != null) {
                  // Skip comment lines
                  if (!line.startsWith("#") && line.length() > 0) {
                      // 0: "Link"
                      // 1: New tz id
                      // Last: Old tz id
                      String[] fields = line.split("\t+");
                      String newTzId = fields[1];
                      String oldTzId = fields[fields.length - 1];
  
                      final TimeZone tz = TimeZone.getTimeZone(newTzId);
                      if (tz == null) {
                          Log.e(TAG, "Timezone not found: " + newTzId);
                          continue;
                      }
  
                      processedTimeZones.add(oldTzId);
  
                      if (DEBUG) {
                          Log.e(TAG, "# Dropping identical time zone from backward: " + oldTzId);
                      }
  
                      // Remember the cooler/newer time zone id
                      if (mDefaultTimeZoneId != null && mDefaultTimeZoneId.equals(oldTzId)) {
                          mAlternateDefaultTimeZoneId = newTzId;
                      }
                  }
              }
          } catch (IOException ex) {
              Log.e(TAG, "Failed to read 'backward' file.");
          } finally {
              try {
                  if (is != null) {
                      is.close();
                  }
              } catch (IOException ignored) {
              }
          }
  
          /*
           * zone.tab contains a list of time zones and country code. They are
           * "sorted first by country, then an order within the country that (1)
           * makes some geographical sense, and (2) puts the most populous zones
           * first, where that does not contradict (1)."
           */
          try {
              String lang = Locale.getDefault().getLanguage();
              is = am.open("zone.tab");
              BufferedReader reader = new BufferedReader(new InputStreamReader(is));
              String line;
              while ((line = reader.readLine()) != null) {
                  if (!line.startsWith("#")) { // Skip comment lines
                      // 0: country code
                      // 1: coordinates
                      // 2: time zone id
                      // 3: comments
                      final String[] fields = line.split("\t");
                      final String timeZoneId = fields[2];
                      final String countryCode = fields[0];
                      final TimeZone tz = TimeZone.getTimeZone(timeZoneId);
                      if (tz == null) {
                          Log.e(TAG, "Timezone not found: " + timeZoneId);
                          continue;
                      }
  
                      /*
                       * Dropping non-GMT tzs without a country code. They are not
                       * really needed and they are dups but missing proper
                       * country codes. e.g. WET CET MST7MDT PST8PDT Asia/Khandyga
                       * Asia/Ust-Nera EST
                       */
                      if (countryCode == null && !timeZoneId.startsWith("Etc/GMT")) {
                          processedTimeZones.add(timeZoneId);
                          continue;
                      }
  
                      // Remember the mapping between the country code and display
                      // name
                      String country = mCountryCodeToNameMap.get(countryCode);
                      if (country == null) {
                          country = getCountryNames(lang, countryCode);
                          mCountryCodeToNameMap.put(countryCode, country);
                      }
  
                      // TODO Don't like this here but need to get the country of
                      // the default tz.
  
                      // Find the country of the default tz
                      if (mDefaultTimeZoneId != null && mDefaultTimeZoneCountry == null
                              && timeZoneId.equals(mAlternateDefaultTimeZoneId)) {
                          mDefaultTimeZoneCountry = country;
                          TimeZone defaultTz = TimeZone.getTimeZone(mDefaultTimeZoneId);
                          if (defaultTz != null) {
                              mDefaultTimeZoneInfo = new TimeZoneInfo(defaultTz, country);
  
                              int tzToOverride = getIdenticalTimeZoneInTheCountry(mDefaultTimeZoneInfo);
                              if (tzToOverride == -1) {
                                  if (DEBUG) {
                                      Log.e(TAG, "Adding default time zone: "
                                              + mDefaultTimeZoneInfo.toString());
                                  }
                                  mTimeZones.add(mDefaultTimeZoneInfo);
                              } else {
                                  mTimeZones.add(tzToOverride, mDefaultTimeZoneInfo);
                                  if (DEBUG) {
                                      TimeZoneInfo tzInfoToOverride = mTimeZones.get(tzToOverride);
                                      String tzIdToOverride = tzInfoToOverride.mTzId;
                                      Log.e(TAG, "Replaced by default tz: "
                                              + tzInfoToOverride.toString());
                                      Log.e(TAG, "Adding default time zone: "
                                              + mDefaultTimeZoneInfo.toString());
                                  }
                              }
                          }
                      }
  
                      // Add to the list of time zones if the time zone is unique
                      // in the given country.
                      TimeZoneInfo timeZoneInfo = new TimeZoneInfo(tz, country);
                      int identicalTzIdx = getIdenticalTimeZoneInTheCountry(timeZoneInfo);
                      if (identicalTzIdx == -1) {
                          if (DEBUG) {
                              Log.e(TAG, "# Adding time zone: " + timeZoneId + " ## " +
                                      tz.getDisplayName());
                          }
                          mTimeZones.add(timeZoneInfo);
                      } else {
                          if (DEBUG) {
                              Log.e(TAG, "# Dropping identical time zone: " + timeZoneId + " ## " +
                                      tz.getDisplayName());
                          }
                      }
                      processedTimeZones.add(timeZoneId);
                  }
              }
  
          } catch (IOException ex) {
              Log.e(TAG, "Failed to read 'zone.tab'.");
          } finally {
              try {
                  if (is != null) {
                      is.close();
                  }
              } catch (IOException ignored) {
              }
          }
  
          return processedTimeZones;
      }
  
      private static Locale mBackupCountryLocale;
      private static String[] mBackupCountryCodes;
      private static String[] mBackupCountryNames;
  
      private String getCountryNames(String lang, String countryCode) {
          final Locale defaultLocale = Locale.getDefault();
          String countryDisplayName;
          if (PALESTINE_COUNTRY_CODE.equalsIgnoreCase(countryCode)) {
              countryDisplayName = mPalestineDisplayName;
          } else {
              countryDisplayName = new Locale(lang, countryCode).getDisplayCountry(defaultLocale);
          }
  
          if (!countryCode.equals(countryDisplayName)) {
              return countryDisplayName;
          }
  
          if (mBackupCountryCodes == null || !defaultLocale.equals(mBackupCountryLocale)) {
              mBackupCountryLocale = defaultLocale;
              mBackupCountryCodes = mContext.getResources().getStringArray(
                      R.array.backup_country_codes);
              mBackupCountryNames = mContext.getResources().getStringArray(
                      R.array.backup_country_names);
          }
  
          int length = Math.min(mBackupCountryCodes.length, mBackupCountryNames.length);
  
          for (int i = 0; i < length; i++) {
              if (mBackupCountryCodes[i].equals(countryCode)) {
                  return mBackupCountryNames[i];
              }
          }
  
          return countryCode;
      }
  
      private int getIdenticalTimeZoneInTheCountry(TimeZoneInfo timeZoneInfo) {
          int idx = 0;
          for (TimeZoneInfo tzi : mTimeZones) {
              if (tzi.hasSameRules(timeZoneInfo)) {
                  if (tzi.mCountry == null) {
                      if (timeZoneInfo.mCountry == null) {
                          return idx;
                      }
                  } else if (tzi.mCountry.equals(timeZoneInfo.mCountry)) {
                      return idx;
                  }
              }
              ++idx;
          }
          return -1;
      }
  }
  