/*
 * Copyright (C) 2017 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.dialer.assisteddialing;

import android.content.Context;
import android.support.annotation.NonNull;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import com.android.dialer.common.LogUtil;
import com.android.dialer.logging.DialerImpression;
import com.android.dialer.logging.Logger;
import com.android.dialer.phonenumberutil.PhoneNumberHelper;
import com.android.dialer.strictmode.StrictModeUtils;
import com.google.i18n.phonenumbers.NumberParseException;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber.CountryCodeSource;
import java.util.Locale;
import java.util.Optional;

/** Ensures that a number is eligible for Assisted Dialing */
final class Constraints {
  private final PhoneNumberUtil phoneNumberUtil;
  private final Context context;
  private final CountryCodeProvider countryCodeProvider;

  /**
   * Create a new instance of Constraints.
   *
   * @param context The context used to determine whether or not a number is an emergency number.
   * @param countryCodeProvider A csv of supported country codes, e.g. "US,CA"
   */
  public Constraints(@NonNull Context context, @NonNull CountryCodeProvider countryCodeProvider) {
    if (context == null) {
      throw new NullPointerException("Provided context cannot be null");
    }
    this.context = context;

    if (countryCodeProvider == null) {
      throw new NullPointerException("Provided configProviderCountryCodes cannot be null");
    }

    this.countryCodeProvider = countryCodeProvider;
    this.phoneNumberUtil = StrictModeUtils.bypass(() -> PhoneNumberUtil.getInstance());
  }

  /**
   * Determines whether or not we think Assisted Dialing is possible given the provided parameters.
   *
   * @param numberToCheck A string containing the phone number.
   * @param userHomeCountryCode A string containing an ISO 3166-1 alpha-2 country code representing
   *     the user's home country.
   * @param userRoamingCountryCode A string containing an ISO 3166-1 alpha-2 country code
   *     representing the user's roaming country.
   * @return A boolean indicating whether or not the provided values are eligible for assisted
   *     dialing.
   */
  boolean meetsPreconditions(
      @NonNull String numberToCheck,
      @NonNull String userHomeCountryCode,
      @NonNull String userRoamingCountryCode) {

    if (TextUtils.isEmpty(numberToCheck)) {
      LogUtil.i("Constraints.meetsPreconditions", "numberToCheck was empty");
      return false;
    }

    if (TextUtils.isEmpty(userHomeCountryCode)) {
      LogUtil.i("Constraints.meetsPreconditions", "userHomeCountryCode was empty");
      return false;
    }

    if (TextUtils.isEmpty(userRoamingCountryCode)) {
      LogUtil.i("Constraints.meetsPreconditions", "userRoamingCountryCode was empty");
      return false;
    }

    userHomeCountryCode = userHomeCountryCode.toUpperCase(Locale.US);
    userRoamingCountryCode = userRoamingCountryCode.toUpperCase(Locale.US);

    Optional<PhoneNumber> parsedPhoneNumber = parsePhoneNumber(numberToCheck, userHomeCountryCode);

    if (!parsedPhoneNumber.isPresent()) {
      LogUtil.i("Constraints.meetsPreconditions", "parsedPhoneNumber was empty");
      return false;
    }

    return areSupportedCountryCodes(userHomeCountryCode, userRoamingCountryCode)
        && isUserRoaming(userHomeCountryCode, userRoamingCountryCode)
        && isNotInternationalNumber(parsedPhoneNumber)
        && isNotEmergencyNumber(numberToCheck, context)
        && isValidNumber(parsedPhoneNumber)
        && doesNotHaveExtension(parsedPhoneNumber);
  }

  /** Returns a boolean indicating the value equivalence of the provided country codes. */
  private boolean isUserRoaming(
      @NonNull String userHomeCountryCode, @NonNull String userRoamingCountryCode) {
    boolean result = !userHomeCountryCode.equals(userRoamingCountryCode);
    LogUtil.i("Constraints.isUserRoaming", String.valueOf(result));
    return result;
  }

  /**
   * Returns a boolean indicating the support of both provided country codes for assisted dialing.
   * Both country codes must be allowed for the return value to be true.
   */
  private boolean areSupportedCountryCodes(
      @NonNull String userHomeCountryCode, @NonNull String userRoamingCountryCode) {
    if (TextUtils.isEmpty(userHomeCountryCode)) {
      LogUtil.i("Constraints.areSupportedCountryCodes", "userHomeCountryCode was empty");
      return false;
    }

    if (TextUtils.isEmpty(userRoamingCountryCode)) {
      LogUtil.i("Constraints.areSupportedCountryCodes", "userRoamingCountryCode was empty");
      return false;
    }

    boolean result =
        countryCodeProvider.isSupportedCountryCode(userHomeCountryCode)
            && countryCodeProvider.isSupportedCountryCode(userRoamingCountryCode);
    LogUtil.i("Constraints.areSupportedCountryCodes", String.valueOf(result));
    return result;
  }

  /**
   * A convenience method to take a number as a String and a specified country code, and return a
   * PhoneNumber object.
   */
  private Optional<PhoneNumber> parsePhoneNumber(
      @NonNull String numberToParse, @NonNull String userHomeCountryCode) {
    return StrictModeUtils.bypass(
        () -> {
          try {
            return Optional.of(
                phoneNumberUtil.parseAndKeepRawInput(numberToParse, userHomeCountryCode));
          } catch (NumberParseException e) {
            Logger.get(context)
                .logImpression(DialerImpression.Type.ASSISTED_DIALING_CONSTRAINT_PARSING_FAILURE);
            LogUtil.i("Constraints.parsePhoneNumber", "could not parse the number");
            return Optional.empty();
          }
        });
  }

  /** Returns a boolean indicating if the provided number is already internationally formatted. */
  private boolean isNotInternationalNumber(@NonNull Optional<PhoneNumber> parsedPhoneNumber) {

    if (parsedPhoneNumber.get().hasCountryCode()
        && parsedPhoneNumber.get().getCountryCodeSource()
            != CountryCodeSource.FROM_DEFAULT_COUNTRY) {
      Logger.get(context)
          .logImpression(DialerImpression.Type.ASSISTED_DIALING_CONSTRAINT_NUMBER_HAS_COUNTRY_CODE);
      LogUtil.i(
          "Constraints.isNotInternationalNumber", "phone number already provided the country code");
      return false;
    }
    return true;
  }

  /**
   * Returns a boolean indicating if the provided number has an extension.
   *
   * <p>Extensions are currently stripped when formatting a number for mobile dialing, so we don't
   * want to purposefully truncate a number.
   */
  private boolean doesNotHaveExtension(@NonNull Optional<PhoneNumber> parsedPhoneNumber) {

    if (parsedPhoneNumber.get().hasExtension()
        && !TextUtils.isEmpty(parsedPhoneNumber.get().getExtension())) {
      Logger.get(context)
          .logImpression(DialerImpression.Type.ASSISTED_DIALING_CONSTRAINT_NUMBER_HAS_EXTENSION);
      LogUtil.i("Constraints.doesNotHaveExtension", "phone number has an extension");
      return false;
    }
    return true;
  }

  /** Returns a boolean indicating if the provided number is considered to be a valid number. */
  private boolean isValidNumber(@NonNull Optional<PhoneNumber> parsedPhoneNumber) {
    boolean result =
        StrictModeUtils.bypass(() -> phoneNumberUtil.isValidNumber(parsedPhoneNumber.get()));
    LogUtil.i("Constraints.isValidNumber", String.valueOf(result));

    return result;
  }

  /** Returns a boolean indicating if the provided number is an emergency number. */
  private boolean isNotEmergencyNumber(@NonNull String numberToCheck, @NonNull Context context) {
    // isEmergencyNumber may depend on network state, so also use isLocalEmergencyNumber when
    // roaming and out of service.
    boolean result =
        !PhoneNumberUtils.isEmergencyNumber(numberToCheck)
            && !PhoneNumberHelper.isLocalEmergencyNumber(context, numberToCheck);
    LogUtil.i("Constraints.isNotEmergencyNumber", String.valueOf(result));
    return result;
  }
}
