// Copyright 2015 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.components.browser_ui.site_settings;

import static org.chromium.components.browser_ui.site_settings.WebsitePreferenceBridge.SITE_WILDCARD;

import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.Vibrator;
import android.provider.Settings;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;

import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.components.browser_ui.styles.SemanticColorUtils;
import org.chromium.components.browser_ui.widget.CheckBoxWithDescription;
import org.chromium.ui.KeyboardVisibilityDelegate;
import org.chromium.ui.text.EmptyTextWatcher;

/**
 * A utility class for the UI recording exceptions to the blocked list for site
 * settings.
 */
public class AddExceptionPreference
        extends Preference implements Preference.OnPreferenceClickListener {
    // The callback to notify when the user adds a site.
    private SiteAddedCallback mSiteAddedCallback;

    // The accent color to use for the icon and title view.
    private int mPrefAccentColor;

    // The custom message to show in the dialog.
    private String mDialogMessage;

    // The Site Settings Category of the exception we are adding.
    private final SiteSettingsCategory mCategory;

    // The colors for the site URL EditText
    private int mErrorColor;
    private int mDefaultColor;

    /**
     * An interface to implement to get a callback when a site exception needs to be added.
     */
    public interface SiteAddedCallback {
        /**
         * The callback for the site exception that needs to be added.
         * @param primaryPattern The primary pattern for the exception, usually the hostname to add,
         * or the wildcard indicating all hosts
         * @param secondaryPattern The secondary pattern for the exception, indicating on which
         * sites the primary pattern is affected. Usually the wildcard or a specific host (for
         * third-party cookies).
         */
        public void onAddSite(String primaryPattern, String secondaryPattern);
    }

    /**
     * Construct a AddException preference.
     * @param context The current context.
     * @param key The key to use for the preference.
     * @param message The custom message to show in the dialog.
     * @param callback A callback to receive notifications that an exception has been added.
     */
    public AddExceptionPreference(Context context, String key, String message,
            SiteSettingsCategory category, SiteAddedCallback callback) {
        super(context);
        mDialogMessage = message;
        mCategory = category;
        mSiteAddedCallback = callback;
        setOnPreferenceClickListener(this);

        setKey(key);
        Resources resources = getContext().getResources();
        mPrefAccentColor = SemanticColorUtils.getDefaultControlColorActive(getContext());
        mErrorColor = resources.getColor(R.color.default_red);
        mDefaultColor =
                AppCompatResources.getColorStateList(getContext(), R.color.default_text_color_list)
                        .getDefaultColor();

        Drawable plusIcon = ApiCompatibilityUtils.getDrawable(resources, R.drawable.plus);
        plusIcon.mutate();
        plusIcon.setColorFilter(mPrefAccentColor, PorterDuff.Mode.SRC_IN);
        setIcon(plusIcon);

        setTitle(resources.getString(R.string.website_settings_add_site));
    }

    @Override
    public void onBindViewHolder(PreferenceViewHolder holder) {
        super.onBindViewHolder(holder);
        TextView titleView = (TextView) holder.findViewById(android.R.id.title);
        titleView.setAllCaps(true);
        titleView.setTextColor(mPrefAccentColor);
    }

    @Override
    public boolean onPreferenceClick(Preference preference) {
        showAddExceptionDialog();
        return true;
    }

    /**
     * Show the dialog allowing the user to add a new website as an exception.
     */
    private void showAddExceptionDialog() {
        LayoutInflater inflater =
                (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View view = inflater.inflate(R.layout.add_site_dialog, null);
        final EditText input = view.findViewById(R.id.site);
        final CheckBoxWithDescription checkBox = view.findViewById(R.id.add_site_dialog_checkbox);

        if (mCategory.getType() == SiteSettingsCategory.Type.COOKIES) {
            checkBox.setVisibility(View.VISIBLE);
            checkBox.setPrimaryText(getContext().getString(
                    R.string.website_settings_third_party_cookies_exception_label));
        } else if (mCategory.getType() == SiteSettingsCategory.Type.REQUEST_DESKTOP_SITE) {
            // Default to domain level setting for Request Desktop Site.
            checkBox.setChecked(true);
            checkBox.setVisibility(View.VISIBLE);
            checkBox.setPrimaryText(getContext().getString(
                    R.string.website_settings_domain_desktop_site_exception_checkbox_primary));
            checkBox.setDescriptionText(getContext().getString(
                    R.string.website_settings_domain_desktop_site_exception_checkbox_description));
        }

        DialogInterface.OnClickListener onClickListener = new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int button) {
                if (button == AlertDialog.BUTTON_POSITIVE) {
                    int categoryType = mCategory.getType();
                    boolean isChecked = checkBox.isChecked();
                    String pattern = input.getText().toString().trim();
                    pattern = updatePatternIfNeeded(pattern, categoryType, isChecked);
                    String primary = getPrimaryPattern(pattern, categoryType, isChecked);
                    String secondary = getSecondaryPattern(pattern, categoryType, isChecked);
                    mSiteAddedCallback.onAddSite(primary, secondary);
                } else {
                    dialog.dismiss();
                }
            }
        };

        AlertDialog.Builder alert =
                new AlertDialog.Builder(getContext(), R.style.ThemeOverlay_BrowserUI_AlertDialog);
        AlertDialog alertDialog =
                alert.setTitle(R.string.website_settings_add_site_dialog_title)
                        .setMessage(mDialogMessage)
                        .setView(view)
                        .setPositiveButton(
                                R.string.website_settings_add_site_add_button, onClickListener)
                        .setNegativeButton(R.string.cancel, onClickListener)
                        .create();
        alertDialog.getDelegate().setHandleNativeActionModesEnabled(false);
        alertDialog.setOnShowListener(new DialogInterface.OnShowListener() {
            @Override
            public void onShow(DialogInterface dialog) {
                KeyboardVisibilityDelegate.getInstance().showKeyboard(input);
            }
        });
        alertDialog.show();
        final Button okButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
        okButton.setEnabled(false);

        input.addTextChangedListener(new EmptyTextWatcher() {
            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                // The intent is to capture a url pattern and register it as an exception.
                // But a pattern can be used to express things that are not supported, such
                // as domains, schemes and ports. Therefore we need to filter out invalid values
                // before passing them on to the validity checker for patterns.
                String pattern = s.toString().trim();
                boolean isValid = isPatternValid(pattern, mCategory.getType());

                // Vibrate when adding characters only, not when deleting them.
                if (!isValid && count != 0) {
                    if (Settings.System.getInt(getContext().getContentResolver(),
                                Settings.System.HAPTIC_FEEDBACK_ENABLED, 1)
                            == 1) {
                        ((Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE))
                                .vibrate(50);
                    }
                }

                okButton.setEnabled(isValid && pattern.length() > 0);
                input.setTextColor(isValid ? mDefaultColor : mErrorColor);
            }
        });
    }

    @VisibleForTesting
    static String updatePatternIfNeeded(@NonNull String pattern, int type, boolean isChecked) {
        if (type == SiteSettingsCategory.Type.REQUEST_DESKTOP_SITE) {
            if (isChecked) {
                return WebsitePreferenceBridge.toDomainWildcardPattern(pattern);
            } else {
                return WebsitePreferenceBridge.toHostOnlyPattern(pattern);
            }
        }
        return pattern;
    }

    @VisibleForTesting
    static String getPrimaryPattern(@NonNull String pattern, int type, boolean isChecked) {
        if (type == SiteSettingsCategory.Type.COOKIES) {
            // If a user clicks the third party checkbox, set wildcard as primary.
            return isChecked ? SITE_WILDCARD : pattern;
        } else if (type == SiteSettingsCategory.Type.THIRD_PARTY_COOKIES) {
            return SITE_WILDCARD;
        }
        return pattern;
    }

    @VisibleForTesting
    static String getSecondaryPattern(@NonNull String pattern, int type, boolean isChecked) {
        if (type == SiteSettingsCategory.Type.COOKIES) {
            // If a user clicks the third party checkbox, set pattern as secondary.
            return isChecked ? pattern : SITE_WILDCARD;
        } else if (type == SiteSettingsCategory.Type.THIRD_PARTY_COOKIES) {
            return pattern;
        }
        return SITE_WILDCARD;
    }

    @VisibleForTesting
    static boolean isPatternValid(@NonNull String pattern, int type) {
        if (pattern.length() == 0) {
            return true;
        }
        if (pattern.contains(":") && !isColonAllowed(type)) {
            return false;
        }
        if (pattern.contains(" ") || pattern.startsWith(".")) {
            return false;
        }
        return WebsitePreferenceBridgeJni.get().isContentSettingsPatternValid(pattern);
    }

    private static boolean isColonAllowed(int type) {
        return type == SiteSettingsCategory.Type.REQUEST_DESKTOP_SITE;
    }
}
