/*
 * Copyright (C) 2018 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.settings.datetime.timezone;

import android.app.Activity;
import android.app.AlarmManager;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.icu.util.TimeZone;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;

import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceCategory;

import com.android.settings.R;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.datetime.timezone.model.FilteredCountryTimeZones;
import com.android.settings.datetime.timezone.model.TimeZoneData;
import com.android.settings.datetime.timezone.model.TimeZoneDataLoader;
import com.android.settingslib.core.AbstractPreferenceController;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;

/**
 * The class displays a time zone picker either by regions or fixed offset time zones.
 */
public class TimeZoneSettings extends DashboardFragment {

    private static final String TAG = "TimeZoneSettings";

    private static final int MENU_BY_REGION = Menu.FIRST;
    private static final int MENU_BY_OFFSET = Menu.FIRST + 1;

    private static final int REQUEST_CODE_REGION_PICKER = 1;
    private static final int REQUEST_CODE_ZONE_PICKER = 2;
    private static final int REQUEST_CODE_FIXED_OFFSET_ZONE_PICKER = 3;

    private static final String PREF_KEY_REGION = "time_zone_region";
    private static final String PREF_KEY_REGION_CATEGORY = "time_zone_region_preference_category";
    private static final String PREF_KEY_FIXED_OFFSET_CATEGORY =
            "time_zone_fixed_offset_preference_category";

    private Locale mLocale;
    private boolean mSelectByRegion;
    private TimeZoneData mTimeZoneData;
    private Intent mPendingZonePickerRequestResult;

    private String mSelectedTimeZoneId;
    private TimeZoneInfo.Formatter mTimeZoneInfoFormatter;

    @Override
    public int getMetricsCategory() {
        return SettingsEnums.ZONE_PICKER;
    }

    @Override
    protected int getPreferenceScreenResId() {
        return R.xml.time_zone_prefs;
    }

    @Override
    protected String getLogTag() {
        return TAG;
    }

    /**
     * Called during onAttach
     */
    @VisibleForTesting
    @Override
    public List<AbstractPreferenceController> createPreferenceControllers(Context context) {
        mLocale = context.getResources().getConfiguration().getLocales().get(0);
        mTimeZoneInfoFormatter = new TimeZoneInfo.Formatter(mLocale, new Date());
        final List<AbstractPreferenceController> controllers = new ArrayList<>();
        RegionPreferenceController regionPreferenceController =
                new RegionPreferenceController(context);
        regionPreferenceController.setOnClickListener(this::startRegionPicker);
        RegionZonePreferenceController regionZonePreferenceController =
                new RegionZonePreferenceController(context);
        regionZonePreferenceController.setOnClickListener(this::onRegionZonePreferenceClicked);
        FixedOffsetPreferenceController fixedOffsetPreferenceController =
                new FixedOffsetPreferenceController(context);
        fixedOffsetPreferenceController.setOnClickListener(this::startFixedOffsetPicker);

        controllers.add(regionPreferenceController);
        controllers.add(regionZonePreferenceController);
        controllers.add(fixedOffsetPreferenceController);
        return controllers;
    }

    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        // Hide all interactive preferences
        setPreferenceCategoryVisible((PreferenceCategory) findPreference(
            PREF_KEY_REGION_CATEGORY), false);
        setPreferenceCategoryVisible((PreferenceCategory) findPreference(
            PREF_KEY_FIXED_OFFSET_CATEGORY), false);

        // Start loading TimeZoneData
        getLoaderManager().initLoader(0, null, new TimeZoneDataLoader.LoaderCreator(
                getContext(), this::onTimeZoneDataReady));
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (resultCode != Activity.RESULT_OK || data == null) {
            return;
        }

        switch (requestCode) {
            case REQUEST_CODE_REGION_PICKER:
            case REQUEST_CODE_ZONE_PICKER: {
                if (mTimeZoneData == null) {
                    mPendingZonePickerRequestResult = data;
                } else {
                    onZonePickerRequestResult(mTimeZoneData, data);
                }
                break;
            }
            case REQUEST_CODE_FIXED_OFFSET_ZONE_PICKER: {
                String tzId = data.getStringExtra(FixedOffsetPicker.EXTRA_RESULT_TIME_ZONE_ID);
                // Ignore the result if user didn't change the time zone.
                if (tzId != null && !tzId.equals(mSelectedTimeZoneId)) {
                    onFixedOffsetZoneChanged(tzId);
                }
                break;
            }
        }
    }

    @VisibleForTesting
    void setTimeZoneData(TimeZoneData timeZoneData) {
        mTimeZoneData = timeZoneData;
    }

    private void onTimeZoneDataReady(TimeZoneData timeZoneData) {
        if (mTimeZoneData == null && timeZoneData != null) {
            mTimeZoneData = timeZoneData;
            setupForCurrentTimeZone();
            getActivity().invalidateOptionsMenu();
            if (mPendingZonePickerRequestResult != null) {
                onZonePickerRequestResult(timeZoneData, mPendingZonePickerRequestResult);
                mPendingZonePickerRequestResult = null;
            }
        }
    }

    private void startRegionPicker() {
        startPickerFragment(RegionSearchPicker.class, new Bundle(), REQUEST_CODE_REGION_PICKER);
    }

    private void onRegionZonePreferenceClicked() {
        final Bundle args = new Bundle();
        args.putString(RegionZonePicker.EXTRA_REGION_ID,
                use(RegionPreferenceController.class).getRegionId());
        startPickerFragment(RegionZonePicker.class, args, REQUEST_CODE_ZONE_PICKER);
    }

    private void startFixedOffsetPicker() {
        startPickerFragment(FixedOffsetPicker.class, new Bundle(),
                REQUEST_CODE_FIXED_OFFSET_ZONE_PICKER);
    }

    private void startPickerFragment(Class<? extends BaseTimeZonePicker> fragmentClass, Bundle args,
            int resultRequestCode) {
        new SubSettingLauncher(getContext())
                .setDestination(fragmentClass.getCanonicalName())
                .setArguments(args)
                .setSourceMetricsCategory(getMetricsCategory())
                .setResultListener(this, resultRequestCode)
                .launch();
    }

    private void setDisplayedRegion(String regionId) {
        use(RegionPreferenceController.class).setRegionId(regionId);
        updatePreferenceStates();
    }

    private void setDisplayedTimeZoneInfo(String regionId, String tzId) {
        final TimeZoneInfo tzInfo = tzId == null ? null : mTimeZoneInfoFormatter.format(tzId);
        final FilteredCountryTimeZones countryTimeZones =
                mTimeZoneData.lookupCountryTimeZones(regionId);

        use(RegionZonePreferenceController.class).setTimeZoneInfo(tzInfo);
        // Only clickable when the region has more than 1 time zones or no time zone is selected.

        use(RegionZonePreferenceController.class).setClickable(tzInfo == null ||
                (countryTimeZones != null && countryTimeZones.getTimeZoneIds().size() > 1));
        use(TimeZoneInfoPreferenceController.class).setTimeZoneInfo(tzInfo);

        updatePreferenceStates();
    }

    private void setDisplayedFixedOffsetTimeZoneInfo(String tzId) {
        if (isFixedOffset(tzId)) {
            use(FixedOffsetPreferenceController.class).setTimeZoneInfo(
                    mTimeZoneInfoFormatter.format(tzId));
        } else {
            use(FixedOffsetPreferenceController.class).setTimeZoneInfo(null);
        }
        updatePreferenceStates();
    }

    private void onZonePickerRequestResult(TimeZoneData timeZoneData, Intent data) {
        String regionId = data.getStringExtra(RegionSearchPicker.EXTRA_RESULT_REGION_ID);
        String tzId = data.getStringExtra(RegionZonePicker.EXTRA_RESULT_TIME_ZONE_ID);
        // Ignore the result if user didn't change the region or time zone.
        if (Objects.equals(regionId, use(RegionPreferenceController.class).getRegionId())
            && Objects.equals(tzId, mSelectedTimeZoneId)) {
            return;
        }

        FilteredCountryTimeZones countryTimeZones =
                timeZoneData.lookupCountryTimeZones(regionId);
        if (countryTimeZones == null || !countryTimeZones.getTimeZoneIds().contains(tzId)) {
            Log.e(TAG, "Unknown time zone id is selected: " + tzId);
            return;
        }

        mSelectedTimeZoneId = tzId;
        setDisplayedRegion(regionId);
        setDisplayedTimeZoneInfo(regionId, mSelectedTimeZoneId);
        saveTimeZone(regionId, mSelectedTimeZoneId);

        // Switch to the region mode if the user switching from the fixed offset
        setSelectByRegion(true);
    }

    private void onFixedOffsetZoneChanged(String tzId) {
        mSelectedTimeZoneId = tzId;
        setDisplayedFixedOffsetTimeZoneInfo(tzId);
        saveTimeZone(null, mSelectedTimeZoneId);

        // Switch to the fixed offset mode if the user switching from the region mode
        setSelectByRegion(false);
    }

    private void saveTimeZone(String regionId, String tzId) {
        SharedPreferences.Editor editor = getPreferenceManager().getSharedPreferences().edit();
        if (regionId == null) {
            editor.remove(PREF_KEY_REGION);
        } else {
            editor.putString(PREF_KEY_REGION, regionId);
        }
        editor.apply();
        getActivity().getSystemService(AlarmManager.class).setTimeZone(tzId);
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        menu.add(0, MENU_BY_REGION, 0, R.string.zone_menu_by_region);
        menu.add(0, MENU_BY_OFFSET, 0, R.string.zone_menu_by_offset);
        super.onCreateOptionsMenu(menu, inflater);
    }

    @Override
    public void onPrepareOptionsMenu(Menu menu) {
        // Do not show menu when data is not ready,
        menu.findItem(MENU_BY_REGION).setVisible(mTimeZoneData != null && !mSelectByRegion);
        menu.findItem(MENU_BY_OFFSET).setVisible(mTimeZoneData != null && mSelectByRegion);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case MENU_BY_REGION:
                startRegionPicker();
                return true;

            case MENU_BY_OFFSET:
                startFixedOffsetPicker();
                return true;

            default:
                return false;
        }
    }

    private void setupForCurrentTimeZone() {
        mSelectedTimeZoneId = TimeZone.getDefault().getID();
        setSelectByRegion(!isFixedOffset(mSelectedTimeZoneId));
    }

    private static boolean isFixedOffset(String tzId) {
        return tzId.startsWith("Etc/GMT") || tzId.equals("Etc/UTC");
    }

    /**
     * Switch the current view to select region or select fixed offset time zone.
     * When showing the selected region, it guess the selected region from time zone id.
     * See {@link #findRegionIdForTzId} for more info.
     */
    private void setSelectByRegion(boolean selectByRegion) {
        mSelectByRegion = selectByRegion;
        setPreferenceCategoryVisible((PreferenceCategory) findPreference(
            PREF_KEY_REGION_CATEGORY), selectByRegion);
        setPreferenceCategoryVisible((PreferenceCategory) findPreference(
            PREF_KEY_FIXED_OFFSET_CATEGORY), !selectByRegion);
        final String localeRegionId = getLocaleRegionId();
        final Set<String> allCountryIsoCodes = mTimeZoneData.getRegionIds();

        String displayRegion = allCountryIsoCodes.contains(localeRegionId) ? localeRegionId : null;
        setDisplayedRegion(displayRegion);
        setDisplayedTimeZoneInfo(displayRegion, null);

        if (!mSelectByRegion) {
            setDisplayedFixedOffsetTimeZoneInfo(mSelectedTimeZoneId);
            return;
        }

        String regionId = findRegionIdForTzId(mSelectedTimeZoneId);
        if (regionId != null) {
            setDisplayedRegion(regionId);
            setDisplayedTimeZoneInfo(regionId, mSelectedTimeZoneId);
        }
    }

    /**
     * Find the a region associated with the specified time zone, based on the time zone data.
     * If there are multiple regions associated with the given time zone, the priority will be given
     * to the region the user last picked and the country in user's locale.
     * @return null if no region associated with the time zone
     */
    private String findRegionIdForTzId(String tzId) {
        return findRegionIdForTzId(tzId,
                getPreferenceManager().getSharedPreferences().getString(PREF_KEY_REGION, null),
                getLocaleRegionId());
    }

    @VisibleForTesting
    String findRegionIdForTzId(String tzId, String sharePrefRegionId, String localeRegionId) {
        final Set<String> matchedRegions = mTimeZoneData.lookupCountryCodesForZoneId(tzId);
        if (matchedRegions.size() == 0) {
            return null;
        }
        if (sharePrefRegionId != null && matchedRegions.contains(sharePrefRegionId)) {
            return sharePrefRegionId;
        }
        if (localeRegionId != null && matchedRegions.contains(localeRegionId)) {
            return localeRegionId;
        }

        return matchedRegions.toArray(new String[matchedRegions.size()])[0];
    }

    private void setPreferenceCategoryVisible(PreferenceCategory category,
        boolean isVisible) {
        // Hiding category doesn't hide all the children preference. Set visibility of its children.
        // Do not care grandchildren as time_zone_pref.xml has only 2 levels.
        category.setVisible(isVisible);
        for (int i = 0; i < category.getPreferenceCount(); i++) {
            category.getPreference(i).setVisible(isVisible);
        }
    }

    private String getLocaleRegionId() {
        return mLocale.getCountry().toUpperCase(Locale.US);
    }
}
