/*
 * Copyright (C) 2011 The Libphonenumber Authors
 *
 * 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.google.i18n.phonenumbers.geocoding;

import com.google.i18n.phonenumbers.NumberParseException;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberType;
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
import com.google.i18n.phonenumbers.prefixmapper.PrefixFileReader;

import java.util.List;
import java.util.Locale;

/**
 * An offline geocoder which provides geographical information related to a phone number.
 *
 * @author Shaopeng Jia
 */
public class PhoneNumberOfflineGeocoder {
  private static PhoneNumberOfflineGeocoder instance = null;
  private static final String MAPPING_DATA_DIRECTORY =
      "/com/google/i18n/phonenumbers/geocoding/data/";
  private PrefixFileReader prefixFileReader = null;

  private final PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();

  // @VisibleForTesting
  PhoneNumberOfflineGeocoder(String phonePrefixDataDirectory) {
    prefixFileReader = new PrefixFileReader(phonePrefixDataDirectory);
  }

  /**
   * Gets a {@link PhoneNumberOfflineGeocoder} instance to carry out international phone number
   * geocoding.
   *
   * <p> The {@link PhoneNumberOfflineGeocoder} is implemented as a singleton. Therefore, calling
   * this method multiple times will only result in one instance being created.
   *
   * @return  a {@link PhoneNumberOfflineGeocoder} instance
   */
  public static synchronized PhoneNumberOfflineGeocoder getInstance() {
    if (instance == null) {
      instance = new PhoneNumberOfflineGeocoder(MAPPING_DATA_DIRECTORY);
    }
    return instance;
  }

  /**
   * Returns the customary display name in the given language for the given territory the phone
   * number is from. If it could be from many territories, nothing is returned.
   */
  private String getCountryNameForNumber(PhoneNumber number, Locale language) {
    List<String> regionCodes =
        phoneUtil.getRegionCodesForCountryCode(number.getCountryCode());
    if (regionCodes.size() == 1) {
      return getRegionDisplayName(regionCodes.get(0), language);
    } else {
      String regionWhereNumberIsValid = "ZZ";
      for (String regionCode : regionCodes) {
        if (phoneUtil.isValidNumberForRegion(number, regionCode)) {
          if (!regionWhereNumberIsValid.equals("ZZ")) {
            // If we can't assign the phone number as definitely belonging to only one territory,
            // then we return nothing.
            return "";
          }
          regionWhereNumberIsValid = regionCode;
        }
      }
      return getRegionDisplayName(regionWhereNumberIsValid, language);
    }
  }

  /**
   * Returns the customary display name in the given language for the given region.
   */
  private String getRegionDisplayName(String regionCode, Locale language) {
    return (regionCode == null || regionCode.equals("ZZ") ||
            regionCode.equals(PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY))
        ? "" : new Locale("", regionCode).getDisplayCountry(language);
  }

  /**
   * Returns a text description for the given phone number, in the language provided. The
   * description might consist of the name of the country where the phone number is from, or the
   * name of the geographical area the phone number is from if more detailed information is
   * available.
   *
   * <p>This method assumes the validity of the number passed in has already been checked, and that
   * the number is suitable for geocoding. We consider fixed-line and mobile numbers possible
   * candidates for geocoding.
   *
   * @param number  a valid phone number for which we want to get a text description
   * @param languageCode  the language code for which the description should be written
   * @return  a text description for the given language code for the given phone number
   */
  public String getDescriptionForValidNumber(PhoneNumber number, Locale languageCode) {
    String langStr = languageCode.getLanguage();
    String scriptStr = "";  // No script is specified
    String regionStr = languageCode.getCountry();

    String areaDescription;
    String mobileToken = PhoneNumberUtil.getCountryMobileToken(number.getCountryCode());
    String nationalNumber = phoneUtil.getNationalSignificantNumber(number);
    if (!mobileToken.equals("") && nationalNumber.startsWith(mobileToken)) {
      // In some countries, eg. Argentina, mobile numbers have a mobile token before the national
      // destination code, this should be removed before geocoding.
      nationalNumber = nationalNumber.substring(mobileToken.length());
      String region = phoneUtil.getRegionCodeForCountryCode(number.getCountryCode());
      PhoneNumber copiedNumber;
      try {
        copiedNumber = phoneUtil.parse(nationalNumber, region);
      } catch (NumberParseException e) {
        // If this happens, just reuse what we had.
        copiedNumber = number;
      }
      areaDescription = prefixFileReader.getDescriptionForNumber(copiedNumber, langStr, scriptStr,
                                                                 regionStr);
    } else {
      areaDescription = prefixFileReader.getDescriptionForNumber(number, langStr, scriptStr,
                                                                 regionStr);
    }
    return (areaDescription.length() > 0)
        ? areaDescription : getCountryNameForNumber(number, languageCode);
  }

  /**
   * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale)} but also considers the
   * region of the user. If the phone number is from the same region as the user, only a lower-level
   * description will be returned, if one exists. Otherwise, the phone number's region will be
   * returned, with optionally some more detailed information.
   *
   * <p>For example, for a user from the region "US" (United States), we would show "Mountain View,
   * CA" for a particular number, omitting the United States from the description. For a user from
   * the United Kingdom (region "GB"), for the same number we may show "Mountain View, CA, United
   * States" or even just "United States".
   *
   * <p>This method assumes the validity of the number passed in has already been checked.
   *
   * @param number  the phone number for which we want to get a text description
   * @param languageCode  the language code for which the description should be written
   * @param userRegion  the region code for a given user. This region will be omitted from the
   *     description if the phone number comes from this region. It is a two-letter uppercase ISO
   *     country code as defined by ISO 3166-1.
   * @return  a text description for the given language code for the given phone number, or empty
   *     string if the number passed in is invalid
   */
  public String getDescriptionForValidNumber(PhoneNumber number, Locale languageCode,
                                             String userRegion) {
    // If the user region matches the number's region, then we just show the lower-level
    // description, if one exists - if no description exists, we will show the region(country) name
    // for the number.
    String regionCode = phoneUtil.getRegionCodeForNumber(number);
    if (userRegion.equals(regionCode)) {
      return getDescriptionForValidNumber(number, languageCode);
    }
    // Otherwise, we just show the region(country) name for now.
    return getRegionDisplayName(regionCode, languageCode);
    // TODO: Concatenate the lower-level and country-name information in an appropriate
    // way for each language.
  }

  /**
   * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale)} but explicitly checks
   * the validity of the number passed in.
   *
   * @param number  the phone number for which we want to get a text description
   * @param languageCode  the language code for which the description should be written
   * @return  a text description for the given language code for the given phone number, or empty
   *     string if the number passed in is invalid
   */
  public String getDescriptionForNumber(PhoneNumber number, Locale languageCode) {
    PhoneNumberType numberType = phoneUtil.getNumberType(number);
    if (numberType == PhoneNumberType.UNKNOWN) {
      return "";
    } else if (!canBeGeocoded(numberType)) {
      return getCountryNameForNumber(number, languageCode);
    }
    return getDescriptionForValidNumber(number, languageCode);
  }

  /**
   * As per {@link #getDescriptionForValidNumber(PhoneNumber, Locale, String)} but
   * explicitly checks the validity of the number passed in.
   *
   * @param number  the phone number for which we want to get a text description
   * @param languageCode  the language code for which the description should be written
   * @param userRegion  the region code for a given user. This region will be omitted from the
   *     description if the phone number comes from this region. It is a two-letter uppercase ISO
   *     country code as defined by ISO 3166-1.
   * @return  a text description for the given language code for the given phone number, or empty
   *     string if the number passed in is invalid
   */
  public String getDescriptionForNumber(PhoneNumber number, Locale languageCode,
                                        String userRegion) {
    PhoneNumberType numberType = phoneUtil.getNumberType(number);
    if (numberType == PhoneNumberType.UNKNOWN) {
      return "";
    } else if (!canBeGeocoded(numberType)) {
      return getCountryNameForNumber(number, languageCode);
    }
    return getDescriptionForValidNumber(number, languageCode, userRegion);
  }

  /**
   * A similar method is implemented as PhoneNumberUtil.isNumberGeographical, which performs a
   * stricter check, as it determines if a number has a geographical association. Also, if new
   * phone number types were added, we should check if this other method should be updated too.
   */
  private boolean canBeGeocoded(PhoneNumberType numberType) {
    return (numberType == PhoneNumberType.FIXED_LINE ||
            numberType == PhoneNumberType.MOBILE ||
            numberType == PhoneNumberType.FIXED_LINE_OR_MOBILE);
  }
}
