// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.payments;

import static org.chromium.chrome.browser.autofill.editors.EditorProperties.ALLOW_DELETE;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.ALL_KEYS;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.CANCEL_RUNNABLE;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.DONE_RUNNABLE;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.DropdownFieldProperties.DROPDOWN_ALL_KEYS;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.DropdownFieldProperties.DROPDOWN_CALLBACK;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.DropdownFieldProperties.DROPDOWN_HINT;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.DropdownFieldProperties.DROPDOWN_KEY_VALUE_LIST;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.EDITOR_FIELDS;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.EDITOR_TITLE;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.FORM_VALID;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.FieldProperties.IS_REQUIRED;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.FieldProperties.LABEL;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.FieldProperties.VALIDATOR;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.FieldProperties.VALUE;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.ItemType.DROPDOWN;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.ItemType.TEXT_INPUT;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.SHOW_REQUIRED_INDICATOR;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.TextFieldProperties.TEXT_ALL_KEYS;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.TextFieldProperties.TEXT_FIELD_TYPE;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.TextFieldProperties.TEXT_FORMATTER;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.TextFieldProperties.TEXT_SUGGESTIONS;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.VISIBLE;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.validateForm;

import android.app.ProgressDialog;
import android.text.TextUtils;

import androidx.annotation.Nullable;

import org.chromium.base.Callback;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.autofill.AddressValidationType;
import org.chromium.chrome.browser.autofill.AutofillAddress;
import org.chromium.chrome.browser.autofill.AutofillProfileBridge;
import org.chromium.chrome.browser.autofill.AutofillProfileBridge.AutofillAddressUiComponent;
import org.chromium.chrome.browser.autofill.PersonalDataManager;
import org.chromium.chrome.browser.autofill.PersonalDataManager.GetSubKeysRequestDelegate;
import org.chromium.chrome.browser.autofill.PhoneNumberUtil;
import org.chromium.chrome.browser.autofill.editors.EditorBase;
import org.chromium.chrome.browser.autofill.editors.EditorDialogViewBinder;
import org.chromium.chrome.browser.autofill.editors.EditorFieldValidator;
import org.chromium.chrome.browser.autofill.editors.EditorProperties.FieldItem;
import org.chromium.chrome.browser.autofill.editors.EditorProperties.ItemType;
import org.chromium.components.autofill.AutofillProfile;
import org.chromium.components.autofill.ServerFieldType;
import org.chromium.payments.mojom.AddressErrors;
import org.chromium.ui.modelutil.ListModel;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.function.Predicate;

/**
 * An address editor. Can be used for either shipping or billing address editing.
 *
 * Note that this class is used by PaymentRequest only and will be removed when not needed any more.
 * Please use {@link org.chromium.chrome.browser.autofill.settings.AddressEditor} instead.
 */
@Deprecated
public class AddressEditor
        extends EditorBase<AutofillAddress> implements GetSubKeysRequestDelegate {
    private final Map<Integer, PropertyModel> mAddressFields = new HashMap<>();
    private final Set<String> mPhoneNumbers = new HashSet<>();
    private final boolean mSaveToDisk;
    private final PhoneNumberUtil.CountryAwareFormatTextWatcher mPhoneFormatter;
    @Nullable
    private AutofillProfileBridge mAutofillProfileBridge;
    @Nullable
    private PropertyModel mCountryField;
    @Nullable
    private PropertyModel mPhoneField;
    @Nullable
    private PropertyModel mAdminAreaField;
    private @ItemType int mAdminAreaFieldType;
    @Nullable
    private List<AutofillAddressUiComponent> mAddressUiComponents;
    private boolean mAdminAreasLoaded;
    private String mRecentlySelectedCountry;
    private Callback<AutofillAddress> mDoneCallback;
    private Callback<AutofillAddress> mCancelCallback;
    private boolean mAddressNew;
    private AutofillAddress mAddress;
    private AutofillProfile mProfile;
    private ProgressDialog mProgressDialog;
    @Nullable
    private AddressErrors mAddressErrors;

    private PropertyModel getFieldForFieldType(@ServerFieldType int fieldType) {
        if (!mAddressFields.containsKey(fieldType)) {
            mAddressFields.put(fieldType,
                    new PropertyModel.Builder(TEXT_ALL_KEYS)
                            .with(TEXT_FIELD_TYPE, fieldType)
                            .build());
        }

        return mAddressFields.get(fieldType);
    }

    /**
     * Builds an address editor.
     *
     * @param saveToDisk Whether to save changes to disk after editing.
     */
    public AddressEditor(boolean saveToDisk) {
        mSaveToDisk = saveToDisk;
        mPhoneFormatter = new PhoneNumberUtil.CountryAwareFormatTextWatcher();
    }

    /**
     * Adds the given phone number to the autocomplete set, if it's valid.
     * Note that here we consider all non-null and non-empty numbers as valid
     * since we are doing strict validation of Autofill data.
     *
     * @param phoneNumber The phone number to possibly add.
     */
    public void addPhoneNumberIfValid(@Nullable String phoneNumber) {
        if (!TextUtils.isEmpty(phoneNumber)) mPhoneNumbers.add(phoneNumber);
    }

    /**
     * Sets the address errors to indicate error messages from merchant's retry() call.
     *
     * @param errors The address errors from merchant's retry() call.
     */
    public void setAddressErrors(@Nullable AddressErrors errors) {
        mAddressErrors = errors;
    }

    private String getAddressError(int field) {
        if (mAddressErrors == null) return null;

        switch (field) {
            case ServerFieldType.ADDRESS_HOME_COUNTRY:
                return mAddressErrors.country;
            case ServerFieldType.ADDRESS_HOME_STATE:
                return mAddressErrors.region;
            case ServerFieldType.ADDRESS_HOME_CITY:
                return mAddressErrors.city;
            case ServerFieldType.ADDRESS_HOME_DEPENDENT_LOCALITY:
                return mAddressErrors.dependentLocality;
            case ServerFieldType.ADDRESS_HOME_SORTING_CODE:
                return mAddressErrors.sortingCode;
            case ServerFieldType.ADDRESS_HOME_ZIP:
                return mAddressErrors.postalCode;
            case ServerFieldType.ADDRESS_HOME_STREET_ADDRESS:
                return mAddressErrors.addressLine;
            case ServerFieldType.COMPANY_NAME:
                return mAddressErrors.organization;
            case ServerFieldType.NAME_FULL:
                return mAddressErrors.recipient;
            default:
                assert false : "Unrecognized server field type: " + field;
                return null;
        }
    }

    /**
     * Allows calling |edit| with a single callback used for both 'done' and 'cancel'.
     * @see #edit(AutofillAddress, Callback, Callback)
     */
    public void edit(
            @Nullable final AutofillAddress toEdit, final Callback<AutofillAddress> callback) {
        edit(toEdit, callback, callback);
    }

    /**
     * Builds and shows an editor model with the following fields.
     *
     * [ country dropdown    ] <----- country dropdown is always present.
     * [ an address field    ] \
     * [ an address field    ]  \
     *         ...                <-- field order, presence, required, and labels depend on country.
     * [ an address field    ]  /
     * [ an address field    ] /
     * [ phone number field  ] <----- phone is always present.
     */
    @Override
    public void edit(@Nullable final AutofillAddress toEdit,
            final Callback<AutofillAddress> doneCallback,
            final Callback<AutofillAddress> cancelCallback) {
        super.edit(toEdit, doneCallback, cancelCallback);
        if (mAutofillProfileBridge == null) mAutofillProfileBridge = new AutofillProfileBridge();
        mDoneCallback = doneCallback;
        mCancelCallback = cancelCallback;

        // If |toEdit| is null, we're creating a new autofill profile with the country code of the
        // default locale on this device.
        final String editTitle;
        mAddressNew = toEdit == null;
        if (mAddressNew) {
            mAddress = new AutofillAddress(mContext, AutofillProfile.builder().build());
            editTitle = mContext.getString(R.string.autofill_create_profile);
        } else {
            mAddress = toEdit;
            editTitle = toEdit.getEditTitle();
        }

        mProfile = mAddress.getProfile();

        // When edit is called, a new form is started, so the country on the
        // dropdown list is not changed. => mRecentlySelectedCountry should be null.
        mRecentlySelectedCountry = null;

        // The country dropdown is always present on the editor.
        if (mCountryField == null) {
            mCountryField =
                    new PropertyModel.Builder(DROPDOWN_ALL_KEYS)
                            .with(LABEL,
                                    mContext.getString(R.string.autofill_profile_editor_country))
                            .with(DROPDOWN_KEY_VALUE_LIST,
                                    AutofillProfileBridge.getSupportedCountries())
                            .with(IS_REQUIRED, false)
                            .build();
        }

        mCountryField.set(DROPDOWN_CALLBACK, new Callback<String>() {
            /**
             * Load admin areas for the selected country.
             */
            @Override
            public void onResult(String countryCode) {
                showProgressDialog();
                mRecentlySelectedCountry = countryCode;
                mPhoneFormatter.setCountryCode(mRecentlySelectedCountry);
                loadAdminAreasForCountry(mRecentlySelectedCountry);
            }
        });

        // Country dropdown is cached, so the selected item needs to be updated for the new profile
        // that's being edited. This will not fire the dropdown callback.
        mCountryField.set(VALUE, AutofillAddress.getCountryCode(mProfile));

        // Phone number validator and formatter are cached, so their contry code needs to be updated
        // for the new profile that's being edited.
        final String countryValue = mCountryField.get(VALUE);
        assert countryValue != null;
        mPhoneFormatter.setCountryCode(countryValue);

        // Phone number is present for all countries.
        if (mPhoneField == null) {
            mPhoneField = new PropertyModel.Builder(TEXT_ALL_KEYS)
                                  .with(TEXT_FIELD_TYPE, ServerFieldType.PHONE_HOME_WHOLE_NUMBER)
                                  .with(LABEL,
                                          mContext.getString(
                                                  R.string.autofill_profile_editor_phone_number))
                                  .with(TEXT_SUGGESTIONS, new ArrayList<>(mPhoneNumbers))
                                  .with(TEXT_FORMATTER, mPhoneFormatter)
                                  .with(IS_REQUIRED, true)
                                  .build();
        }

        // Phone number field is cached, so its value needs to be updated for every new profile
        // that's being edited.
        mPhoneField.set(VALUE, mProfile.getInfo(ServerFieldType.PHONE_HOME_WHOLE_NUMBER));

        mEditorModel = new PropertyModel.Builder(ALL_KEYS)
                               .with(EDITOR_TITLE, editTitle)
                               .with(SHOW_REQUIRED_INDICATOR, true)
                               .with(EDITOR_FIELDS, new ListModel())
                               .with(DONE_RUNNABLE, this::onDone)
                               .with(CANCEL_RUNNABLE, this::onCancel)
                               .with(FORM_VALID, true)
                               .with(ALLOW_DELETE, false)
                               .build();
        mEditorMCP = PropertyModelChangeProcessor.create(
                mEditorModel, mEditorDialog, EditorDialogViewBinder::bindEditorDialogView);

        loadAdminAreasForCountry(mCountryField.get(VALUE));
        boolean formValid = mAddressErrors == null;
        if (!mAddressNew) {
            // Form validation must be performed only for non-empty address profiles.
            formValid |= validateForm(mEditorModel);
        }
        mEditorModel.set(FORM_VALID, formValid);
    }

    private void onDone() {
        if (!validateForm(mEditorModel)) {
            // Note: triggering editor error messages and focused field update using temporary
            // property.
            // TODO(crbug.com/1435314): remove this temporary logic.
            mEditorModel.set(FORM_VALID, true);
            mEditorModel.set(FORM_VALID, false);
            return;
        }
        mEditorModel.set(VISIBLE, false);

        // This makes sure that onSubKeysReceived returns early if it's
        // ever called when Done has already occurred.
        mAdminAreasLoaded = true;
        PersonalDataManager.getInstance().cancelPendingGetSubKeys();

        // Commit changes to the address and send modified address to the caller.
        commitChanges(mProfile);
        mAddress.completeAddress(mProfile);
        mDoneCallback.onResult(mAddress);

        // Clean up the state of this editor.
        reset();
    }

    private void onCancel() {
        mEditorModel.set(VISIBLE, false);

        // This makes sure that onSubKeysReceived returns early if it's
        // ever called when Cancel has already occurred.
        mAdminAreasLoaded = true;
        PersonalDataManager.getInstance().cancelPendingGetSubKeys();

        // Send unchanged address to the caller.
        mCancelCallback.onResult(mAddressNew ? null : mAddress);

        // Clean up the state of this editor.
        reset();
    }

    private void showProgressDialog() {
        mProgressDialog = new ProgressDialog(mContext);
        mProgressDialog.setMessage(mContext.getText(R.string.payments_loading_message));
        mProgressDialog.show();
    }

    private void dismissProgressDialog() {
        if (mProgressDialog != null && mProgressDialog.isShowing()) {
            mProgressDialog.dismiss();
        }
        mProgressDialog = null;
    }

    /** Saves the edited profile on disk. */
    private void commitChanges(AutofillProfile profile) {
        // Country code and phone number are always required and are always collected from the
        // editor model.
        profile.setInfo(ServerFieldType.ADDRESS_HOME_COUNTRY, mCountryField.get(VALUE));
        profile.setInfo(ServerFieldType.PHONE_HOME_WHOLE_NUMBER, mPhoneField.get(VALUE));

        // Autofill profile bridge normalizes the language code for the autofill profile.
        profile.setLanguageCode(mAutofillProfileBridge.getCurrentBestLanguageCode());

        // Collect data from all visible fields and store it in the autofill profile.
        for (AutofillAddressUiComponent component : mAddressUiComponents) {
            PropertyModel fieldModel = component.id == ServerFieldType.ADDRESS_HOME_STATE
                    ? mAdminAreaField
                    : mAddressFields.get(component.id);
            if (component.id != ServerFieldType.ADDRESS_HOME_COUNTRY) {
                profile.setInfo(component.id, fieldModel.get(VALUE));
            }
        }

        // Save the edited autofill profile locally.
        if (mSaveToDisk) {
            profile.setGUID(PersonalDataManager.getInstance().setProfileToLocal(mProfile));
        }

        if (profile.getGUID().isEmpty()) {
            assert !mSaveToDisk;

            // Set a fake guid for a new temp AutofillProfile to be used in CardEditor. Note that
            // this temp AutofillProfile should not be saved to disk.
            profile.setGUID(UUID.randomUUID().toString());
        }

        profile.setIsLocal(true);
    }

    private void setAddressFieldValuesFromCache() {
        // Address fields are cached, so their values need to be updated for every new profile
        // that's being edited.
        for (Map.Entry<Integer, PropertyModel> entry : mAddressFields.entrySet()) {
            entry.getValue().set(VALUE, mProfile.getInfo(entry.getKey()));
        }
        mAdminAreaField.set(VALUE, mProfile.getInfo(ServerFieldType.ADDRESS_HOME_STATE));
    }

    @Override
    public void onSubKeysReceived(
            @Nullable String[] adminAreaCodes, @Nullable String[] adminAreaNames) {
        if (mAdminAreasLoaded) return;
        mAdminAreasLoaded = true;
        // If the dialog is already dismissed, it doesn't make sense to show it.
        // This can happen if the dialog is dismissed while we are getting the
        // subkeys.
        if (mEditorDialog.isDismissed()) return;

        initializeAdminAreaField(adminAreaCodes, adminAreaNames);

        // Admin areas need to be fetched in two cases:
        // 1. Initial loading of the form.
        // 2. When the selected country is changed in the form.
        // mRecentlySelectedCountry is not null if and only if it's the second case
        if (mRecentlySelectedCountry != null) {
            dismissProgressDialog();
            // Both country code and language code dictate which fields should be added to the
            // editor.
            // For example, "US" will not add dependent locality to the editor. A "JP" address will
            // start with a person's full name or a with a prefecture name, depending on whether the
            // language code is "ja-Latn" or "ja".
            addAddressFieldsToEditor(mRecentlySelectedCountry, Locale.getDefault().getLanguage());
        } else {
            addAddressFieldsToEditor(mCountryField.get(VALUE), mProfile.getLanguageCode());
            // Populate fields with values once they've been created.
            setAddressFieldValuesFromCache();
            mEditorModel.set(VISIBLE, true);
        }
    }

    private void initializeAdminAreaField(
            @Nullable String[] adminAreaCodes, @Nullable String[] adminAreaNames) {
        // When there is a timeout in the subkey request process, the admin area codes/names will be
        // null.
        if (adminAreaCodes == null || adminAreaNames == null || adminAreaCodes.length == 0
                || adminAreaCodes.length != adminAreaNames.length) {
            mAdminAreaField = new PropertyModel.Builder(TEXT_ALL_KEYS)
                                      .with(TEXT_FIELD_TYPE, ServerFieldType.ADDRESS_HOME_STATE)
                                      .build();
            mAdminAreaFieldType = TEXT_INPUT;
            return;
        }
        mAdminAreaField = new PropertyModel.Builder(DROPDOWN_ALL_KEYS)
                                  .with(DROPDOWN_KEY_VALUE_LIST,
                                          AutofillProfileBridge.getAdminAreaDropdownList(
                                                  adminAreaCodes, adminAreaNames))
                                  .with(DROPDOWN_HINT, mContext.getString(R.string.select))
                                  .build();
        mAdminAreaFieldType = DROPDOWN;
    }

    /** Requests the list of admin areas. */
    private void loadAdminAreasForCountry(String countryCode) {
        // Used to check if the callback is called (for the cancellation).
        mAdminAreasLoaded = false;
        mEditorDialog.setAsNotDismissed();

        // For tests, the time-out is set to 0. In this case, we should not
        // fetch the admin-areas, and show a text-field instead.
        // This is to have the tests independent of the network status.
        if (PersonalDataManager.getRequestTimeoutMS() == 0) {
            onSubKeysReceived(null, null);
            return;
        }

        // In each rule, admin area keys are saved under sub-keys of country.
        PersonalDataManager.getInstance().loadRulesForSubKeys(countryCode);
        PersonalDataManager.getInstance().getRegionSubKeys(countryCode, this);
    }

    /**
     * Adds fields to the editor model based on the country and language code of
     * the profile that's being edited.
     */
    private void addAddressFieldsToEditor(String countryCode, String languageCode) {
        ListModel<FieldItem> editorFields = new ListModel<>();
        mAddressUiComponents = mAutofillProfileBridge.getAddressUiComponents(
                countryCode, languageCode, AddressValidationType.PAYMENT_REQUEST);
        // In terms of order, country must be the first field.
        editorFields.add(new FieldItem(DROPDOWN, mCountryField, /*isFullLine=*/true));
        for (AutofillAddressUiComponent component : mAddressUiComponents) {
            final PropertyModel field;
            final @ItemType int fieldType;
            if (component.id == ServerFieldType.ADDRESS_HOME_STATE) {
                field = mAdminAreaField;
                fieldType = mAdminAreaFieldType;
            } else {
                field = getFieldForFieldType(component.id);
                fieldType = TEXT_INPUT;
            }

            // Labels depend on country, e.g., state is called province in some countries. These are
            // already localized.
            field.set(LABEL, component.label);

            field.set(VALIDATOR,
                    EditorFieldValidator.builder()
                            .withInitialErrorMessage(getAddressError(component.id))
                            .build());
            // Libaddressinput formats do not always require the full name (RECIPIENT), but
            // PaymentRequest does.
            if (component.isRequired || component.id == ServerFieldType.NAME_FULL) {
                field.set(IS_REQUIRED, true);
                field.get(VALIDATOR).setRequiredErrorMessage(mContext.getString(
                        R.string.pref_edit_dialog_field_required_validation_message));
            } else {
                field.set(IS_REQUIRED, false);
            }

            boolean isFullLine = component.isFullLine
                    || component.id == ServerFieldType.ADDRESS_HOME_CITY
                    || component.id == ServerFieldType.ADDRESS_HOME_DEPENDENT_LOCALITY;
            editorFields.add(new FieldItem(fieldType, field, isFullLine));
        }
        // Phone number (and email if applicable) are the last fields of the address.
        mPhoneField.set(VALIDATOR, getPhoneValidator(countryCode));
        editorFields.add(new FieldItem(TEXT_INPUT, mPhoneField, /*isFullLine=*/true));
        mEditorModel.set(EDITOR_FIELDS, editorFields);
    }

    private EditorFieldValidator getPhoneValidator(String countryCode) {
        // TODO(crbug.com/736387): Warn users when the phone number is a possible number but may be
        // invalid.
        // Note that isPossibleNumber is used since the metadata in libphonenumber has to be
        // updated frequently (daily) to do more strict validation.
        Predicate<String> validationPredicate = value
                -> !TextUtils.isEmpty(value)
                && PhoneNumberUtil.isPossibleNumber(value, countryCode);

        return EditorFieldValidator.builder()
                .withRequiredErrorMessage(mContext.getString(
                        R.string.pref_edit_dialog_field_required_validation_message))
                .withValidationPredicate(validationPredicate,
                        mContext.getString(R.string.payments_phone_invalid_validation_message))
                .withInitialErrorMessage(mAddressErrors != null ? mAddressErrors.phone : null)
                .build();
    }
}
