/*
 * Copyright (C) 2015 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.traceur;

import android.annotation.Nullable;
import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.PackageManager;
import android.icu.text.MessageFormat;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.preference.ListPreference;
import androidx.preference.MultiSelectListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragment;
import androidx.preference.PreferenceManager;
import androidx.preference.SwitchPreference;

import com.android.settingslib.HelpUtils;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;

public class MainFragment extends PreferenceFragment {

    static final String TAG = TraceUtils.TAG;

    public static final String ACTION_REFRESH_TAGS = "com.android.traceur.REFRESH_TAGS";

    private static final String BETTERBUG_PACKAGE_NAME =
            "com.google.android.apps.internal.betterbug";

    private static final String ROOT_MIME_TYPE = "vnd.android.document/root";
    private static final String STORAGE_URI = "content://com.android.traceur.documents/root";

    private SwitchPreference mTracingOn;
    private SwitchPreference mStackSamplingOn;
    private SwitchPreference mHeapDumpOn;

    private AlertDialog mAlertDialog;
    private SharedPreferences mPrefs;

    private MultiSelectListPreference mTags;
    private MultiSelectListPreference mHeapDumpProcesses;

    private boolean mRefreshing;

    private BroadcastReceiver mRefreshReceiver;

    OnSharedPreferenceChangeListener mSharedPreferenceChangeListener =
        new OnSharedPreferenceChangeListener () {
              public void onSharedPreferenceChanged(
                      SharedPreferences sharedPreferences, String key) {
                  refreshUi();
              }
        };

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Receiver.updateDeveloperOptionsWatcher(getContext(), /* fromBootIntent */ false);

        mPrefs = PreferenceManager.getDefaultSharedPreferences(
                getActivity().getApplicationContext());

        mTracingOn = (SwitchPreference) findPreference(
                getActivity().getString(R.string.pref_key_tracing_on));
        mStackSamplingOn = (SwitchPreference) findPreference(
                getActivity().getString(R.string.pref_key_stack_sampling_on));
        mHeapDumpOn = (SwitchPreference) findPreference(
                getActivity().getString(R.string.pref_key_heap_dump_on));

        mTracingOn.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
            @Override
            public boolean onPreferenceClick(Preference preference) {
                Receiver.updateTracing(getContext());
                // Disable the stack sampling and heap dump toggles if the trace toggle is enabled.
                mStackSamplingOn.setEnabled(!((SwitchPreference) preference).isChecked());
                mHeapDumpOn.setEnabled(!((SwitchPreference) preference).isChecked());
                return true;
            }
        });

        mStackSamplingOn.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
            @Override
            public boolean onPreferenceClick(Preference preference) {
                Receiver.updateTracing(getContext());
                // Disable the trace and heap dump toggles if the stack sampling toggle is enabled.
                mTracingOn.setEnabled(!((SwitchPreference) preference).isChecked());
                mHeapDumpOn.setEnabled(!((SwitchPreference) preference).isChecked());
                return true;
            }
        });

        mHeapDumpOn.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
            @Override
            public boolean onPreferenceClick(Preference preference) {
                Receiver.updateTracing(getContext());
                // Disable the trace and stack sampling toggles if the heap dump toggle is enabled.
                mTracingOn.setEnabled(!((SwitchPreference) preference).isChecked());
                mStackSamplingOn.setEnabled(!((SwitchPreference) preference).isChecked());
                return true;
            }
        });

        mHeapDumpProcesses = (MultiSelectListPreference) findPreference(
                getContext().getString(R.string.pref_key_heap_dump_processes));

        mTags = (MultiSelectListPreference) findPreference(getContext().getString(R.string.pref_key_tags));
        mTags.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
            @Override
            public boolean onPreferenceChange(Preference preference, Object newValue) {
                if (mRefreshing) {
                    return true;
                }
                Set<String> set = (Set<String>) newValue;
                TreeMap<String, String> available = TraceUtils.listCategories();
                ArrayList<String> clean = new ArrayList<>(set.size());

                for (String s : set) {
                    if (available.containsKey(s)) {
                        clean.add(s);
                    }
                }
                set.clear();
                set.addAll(clean);
                return true;
            }
        });

        findPreference("restore_default_tags").setOnPreferenceClickListener(
                new Preference.OnPreferenceClickListener() {
                    @Override
                    public boolean onPreferenceClick(Preference preference) {
                        refreshUi(/* restoreDefaultTags =*/ true,
                                /* clearHeapDumpProcesses =*/ false);
                        Toast.makeText(getContext(),
                            getContext().getString(R.string.default_categories_restored),
                                Toast.LENGTH_SHORT).show();
                        return true;
                    }
                });

        findPreference("clear_heap_dump_processes").setOnPreferenceClickListener(
                new Preference.OnPreferenceClickListener() {
                    @Override
                    public boolean onPreferenceClick(Preference preference) {
                        refreshUi(/* restoreDefaultTags =*/ false,
                                /* clearHeapDumpProcesses =*/ true);
                        Toast.makeText(getContext(),
                            getContext().getString(R.string.clear_heap_dump_processes_toast),
                                Toast.LENGTH_SHORT).show();
                        return true;
                    }
                });

        findPreference(getString(R.string.pref_key_tracing_quick_setting))
            .setOnPreferenceClickListener(
                new Preference.OnPreferenceClickListener() {
                    @Override
                    public boolean onPreferenceClick(Preference preference) {
                        Receiver.updateTracingQuickSettings(getContext());
                        return true;
                    }
                });

        findPreference(getString(R.string.pref_key_stack_sampling_quick_setting))
            .setOnPreferenceClickListener(
                new Preference.OnPreferenceClickListener() {
                    @Override
                    public boolean onPreferenceClick(Preference preference) {
                        Receiver.updateStackSamplingQuickSettings(getContext());
                        return true;
                    }
                });

        findPreference("clear_saved_files").setOnPreferenceClickListener(
                new Preference.OnPreferenceClickListener() {
                    @Override
                    public boolean onPreferenceClick(Preference preference) {
                        new AlertDialog.Builder(getContext())
                            .setTitle(R.string.clear_saved_files_question)
                            .setMessage(R.string.all_recordings_will_be_deleted)
                            .setPositiveButton(R.string.clear,
                                new DialogInterface.OnClickListener() {
                                    public void onClick(DialogInterface dialog, int which) {
                                        TraceUtils.clearSavedTraces();
                                    }
                                })
                            .setNegativeButton(android.R.string.cancel,
                                new DialogInterface.OnClickListener() {
                                    public void onClick(DialogInterface dialog, int which) {
                                        dialog.dismiss();
                                    }
                                })
                            .create()
                            .show();
                        return true;
                    }
                });

        findPreference("trace_link_button")
            .setOnPreferenceClickListener(
                new Preference.OnPreferenceClickListener() {
                    @Override
                    public boolean onPreferenceClick(Preference preference) {
                        Intent intent = buildTraceFileViewIntent();
                        try {
                            startActivity(intent);
                        } catch (ActivityNotFoundException e) {
                            return false;
                        }
                        return true;
                    }
                });

        // This disables "Attach to bugreports" when long traces are enabled. This cannot be done in
        // main.xml because there are some other settings there that are enabled with long traces.
        SwitchPreference attachToBugreport = findPreference(
            getString(R.string.pref_key_attach_to_bugreport));
        findPreference(getString(R.string.pref_key_long_traces))
            .setOnPreferenceClickListener(
                new Preference.OnPreferenceClickListener() {
                    @Override
                    public boolean onPreferenceClick(Preference preference) {
                        if (((SwitchPreference) preference).isChecked()) {
                            attachToBugreport.setEnabled(false);
                        } else {
                            attachToBugreport.setEnabled(true);
                        }
                        return true;
                    }
                });

        refreshUi();

        mRefreshReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                refreshUi();
            }
        };

    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        setHasOptionsMenu(true);
        return super.onCreateView(inflater, container, savedInstanceState);
    }

    @Override
    public void onStart() {
        super.onStart();
        getPreferenceScreen().getSharedPreferences()
            .registerOnSharedPreferenceChangeListener(mSharedPreferenceChangeListener);
        getActivity().registerReceiver(mRefreshReceiver, new IntentFilter(ACTION_REFRESH_TAGS),
                Context.RECEIVER_NOT_EXPORTED);
        TraceUtils.cleanupOlderFiles();
        Receiver.updateTracing(getContext());
    }

    @Override
    public void onStop() {
        getPreferenceScreen().getSharedPreferences()
            .unregisterOnSharedPreferenceChangeListener(mSharedPreferenceChangeListener);
        getActivity().unregisterReceiver(mRefreshReceiver);

        if (mAlertDialog != null) {
            mAlertDialog.cancel();
            mAlertDialog = null;
        }

        super.onStop();
    }

    @Override
    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
        addPreferencesFromResource(R.xml.main);
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        HelpUtils.prepareHelpMenuItem(getActivity(), menu, R.string.help_url,
            this.getClass().getName());
    }

    private Intent buildTraceFileViewIntent() {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setDataAndType(Uri.parse(STORAGE_URI), ROOT_MIME_TYPE);
        return intent;
    }

    private void refreshUi() {
        refreshUi(/* restoreDefaultTags =*/ false, /* clearHeapDumpProcesses =*/ false);
    }

    /*
     * Refresh the preferences UI to make sure it reflects the current state of the preferences and
     * system.
     */
    private void refreshUi(boolean restoreDefaultTags, boolean clearHeapDumpProcesses) {
        Context context = getContext();

        // Make sure the Record trace, Record CPU profile, and Record heap dump toggles match their
        // preference values.
        mTracingOn.setChecked(mPrefs.getBoolean(mTracingOn.getKey(), false));
        mStackSamplingOn.setChecked(mPrefs.getBoolean(mStackSamplingOn.getKey(), false));
        mHeapDumpOn.setChecked(mPrefs.getBoolean(mHeapDumpOn.getKey(), false));

        SwitchPreference stopOnReport =
                (SwitchPreference) findPreference(getString(R.string.pref_key_stop_on_bugreport));
        stopOnReport.setChecked(mPrefs.getBoolean(stopOnReport.getKey(), false));

        SwitchPreference continuousHeapDump = (SwitchPreference) findPreference(
                getString(R.string.pref_key_continuous_heap_dump));
        continuousHeapDump.setChecked(mPrefs.getBoolean(continuousHeapDump.getKey(), false));

        // Update category list to match the categories available on the system.
        Set<Entry<String, String>> availableTags = TraceUtils.listCategories().entrySet();
        ArrayList<String> entries = new ArrayList<String>(availableTags.size());
        ArrayList<String> values = new ArrayList<String>(availableTags.size());
        for (Entry<String, String> entry : availableTags) {
            entries.add(entry.getKey() + ": " + entry.getValue());
            values.add(entry.getKey());
        }

        // We keep selected processes in the list in case a user is interested in a process that AM
        // is not yet aware of (e.g. an app that hasn't started up).
        Set<String> runningProcesses = TraceUtils.getRunningAppProcesses(context);
        Set<String> selectedProcesses = mHeapDumpProcesses.getValues();
        runningProcesses.addAll(selectedProcesses);

        List<String> sortedProcesses = new ArrayList<>(runningProcesses);
        Collections.sort(sortedProcesses);

        mRefreshing = true;
        try {
            mTags.setEntries(entries.toArray(new String[0]));
            mTags.setEntryValues(values.toArray(new String[0]));
            if (restoreDefaultTags || !mPrefs.contains(context.getString(R.string.pref_key_tags))) {
                mTags.setValues(PresetTraceConfigs.getDefaultTags());
            }
            mHeapDumpProcesses.setEntries(sortedProcesses.toArray(new String[0]));
            mHeapDumpProcesses.setEntryValues(sortedProcesses.toArray(new String[0]));
            if (clearHeapDumpProcesses ||
                    !mPrefs.contains(context.getString(R.string.pref_key_heap_dump_processes))) {
                mHeapDumpProcesses.setValues(new HashSet<String>());
            }
        } finally {
            mRefreshing = false;
        }

        // Enable or disable each toggle based on the state of the others. This path exists in case
        // the tracing state was updated with the QS tile or the ongoing-trace notification, which
        // would not call the toggles' OnClickListeners.
        mTracingOn.setEnabled(!(mStackSamplingOn.isChecked() || mHeapDumpOn.isChecked()));
        mStackSamplingOn.setEnabled(!(mTracingOn.isChecked() || mHeapDumpOn.isChecked()));

        // Disallow heap dumps if no process is selected, or if tracing/stack sampling is active.
        boolean heapDumpProcessSelected = mHeapDumpProcesses.getValues().size() > 0;
        mHeapDumpOn.setEnabled(heapDumpProcessSelected &&
                !(mTracingOn.isChecked() || mStackSamplingOn.isChecked()));
        mHeapDumpOn.setSummary(heapDumpProcessSelected
                ? context.getString(R.string.record_heap_dump_summary_enabled)
                : context.getString(R.string.record_heap_dump_summary_disabled));

        // Update subtitles on this screen.
        Set<String> categories = mTags.getValues();
        MessageFormat msgFormat = new MessageFormat(
                getResources().getString(R.string.num_categories_selected),
                Locale.getDefault());
        Map<String, Object> arguments = new HashMap<>();
        arguments.put("count", categories.size());
        mTags.setSummary(PresetTraceConfigs.getDefaultTags().equals(categories)
                         ? context.getString(R.string.default_categories)
                         : msgFormat.format(arguments));

        ListPreference bufferSize = (ListPreference)findPreference(
                context.getString(R.string.pref_key_buffer_size));
        bufferSize.setSummary(bufferSize.getEntry());

        ListPreference maxLongTraceSize = (ListPreference)findPreference(
                context.getString(R.string.pref_key_max_long_trace_size));
        maxLongTraceSize.setSummary(maxLongTraceSize.getEntry());

        ListPreference maxLongTraceDuration = (ListPreference)findPreference(
                context.getString(R.string.pref_key_max_long_trace_duration));
        maxLongTraceDuration.setSummary(maxLongTraceDuration.getEntry());

        ListPreference continuousHeapDumpInterval = (ListPreference)findPreference(
                context.getString(R.string.pref_key_continuous_heap_dump_interval));
        continuousHeapDumpInterval.setSummary(continuousHeapDumpInterval.getEntry());

        // Check if BetterBug is installed to see if Traceur should display either the toggle for
        // 'attach_to_bugreport' or 'stop_on_bugreport'.
        try {
            context.getPackageManager().getPackageInfo(BETTERBUG_PACKAGE_NAME,
                    PackageManager.MATCH_SYSTEM_ONLY);
            findPreference(getString(R.string.pref_key_attach_to_bugreport)).setVisible(true);
            findPreference(getString(R.string.pref_key_stop_on_bugreport)).setVisible(false);
            // Changes the long traces summary to add that they cannot be attached to bugreports.
            findPreference(getString(R.string.pref_key_long_traces))
                    .setSummary(getString(R.string.long_traces_summary_betterbug));
        } catch (PackageManager.NameNotFoundException e) {
            // attach_to_bugreport must be disabled here because it's true by default.
            mPrefs.edit().putBoolean(
                    getString(R.string.pref_key_attach_to_bugreport), false).commit();
            findPreference(getString(R.string.pref_key_attach_to_bugreport)).setVisible(false);
            findPreference(getString(R.string.pref_key_stop_on_bugreport)).setVisible(true);
            // Sets long traces summary to the default in case Betterbug was removed.
            findPreference(getString(R.string.pref_key_long_traces))
                    .setSummary(getString(R.string.long_traces_summary));
        }

        // Check if an activity exists to handle the trace_link_button intent. If not, hide the UI
        // element
        PackageManager packageManager = context.getPackageManager();
        Intent intent = buildTraceFileViewIntent();
        if (intent.resolveActivity(packageManager) == null) {
            findPreference("trace_link_button").setVisible(false);
        }
    }
}
