/*
* Copyright 2015 Qianqian Zhu <[email protected]> All rights reserved.
*
* 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.z299studio.pb;

import android.Manifest;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.TypedArray;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.SwitchCompat;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import java.text.DateFormat;
import java.util.Date;

public class Settings extends AppCompatActivity implements AdapterView.OnItemClickListener,
        SettingListDialog.OnOptionSelected, ImportExportTask.TaskListener,
        ActionDialog.ActionDialogListener, SyncService.SyncListener,
        BiometricAuthHelper.BiometricListener, DecryptTask.OnTaskFinishListener{
    
    private static final String TAG_DIALOG = "action_dialog";
    private static final int PERMISSION_REQUEST = 1;

    private String mText;
    private int mActionType;
    private int mOperation;
    private int mOption;
    private byte[] mData;

    private interface ActionListener {
        void onAction(SettingItem sender);
    }
    
    private class SettingItem {
        static final int TYPE_CATEGORY = 0;
        static final int TYPE_SWITCH = 1;
        static final int TYPE_SELECTION = 2;
        static final int TYPE_ACTION = 3;
        int mType;
        int mId;
        String mTitle;
        String mDescription;
        
        SettingItem(int id, String title, String description) {
            mId = id;
            mTitle = title;
            mDescription = description;
            mType = TYPE_CATEGORY;
        }
        public void onClick(View view) {}
        public Object getValue() {return null;}
    }
    
    private class SettingItemSwitch extends SettingItem {
        private boolean mValue;
        
        SettingItemSwitch(int id, String title, String description) {
            super(id, title, description);
            mType = TYPE_SWITCH;
        }
        @Override
        public void onClick(View view) {
            mValue = !mValue;            
        }
        
        @Override
        public Object getValue() {
            return mValue;            
        }        
        
        public SettingItemSwitch setValue(boolean initial) {
            mValue = initial;
            return this;
        }
    }
    
    private class SettingItemAction extends  SettingItem {
        private ActionListener mListener;
        
        SettingItemAction(int id, String title, String description) {
            super(id, title, description);
            mType = TYPE_ACTION; 
        }
        
        public SettingItemAction setListener(ActionListener l) {
            mListener = l;
            return  this;
        }
        @Override
        public void onClick(View view) {
            if(mListener!=null) {
                mListener.onAction(this);
            }
        }      
    }
    
    private class SettingItemSelection extends  SettingItem {
        private String[] mOptions;
        private int mSelection;

        SettingItemSelection(int id, String title, String description) {
            super(id, title, description);
            mType = TYPE_SELECTION; 
        }
        
        public SettingItemSelection setOptions(String[] options) {
            mOptions = options;
            return this;
        }
        
        @Override
        public void onClick(View view) {
            SettingListDialog.build(mTitle, mOptions, mSelection)
                    .show(getSupportFragmentManager(), TAG_DIALOG);
        }
        
        @Override
        public Object getValue() {
            return mSelection;            
        }
        
        public SettingItemSelection setValue(int initial) {
            mSelection = initial;
            mDescription = mOptions[mSelection];
            return this;
        }
        
        public String getText() {
            return mOptions[mSelection];
        }
        
    }
    
    private class SettingItemAdapter extends BaseAdapter {

        private Context mContext;
        private SettingItem[] mItems;
        
        @Override
        public int getCount() {
            return mItems.length;
        }

        @Override
        public Object getItem(int position) {
            return mItems[position];
        }

        @Override
        public long getItemId(int position) {
            return mItems[position].mId;
        }
        
        @Override
        public boolean isEnabled(int position) {
            return mItems[position].mType != SettingItem.TYPE_CATEGORY;
        }
        
        @Override
        public View getView(final int position, View convertView, ViewGroup parent) {
            final SettingItem item = mItems[position];
            LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
                    Context.LAYOUT_INFLATER_SERVICE);
            assert inflater != null;
            View view;
            switch (item.mType) {
                default:
                case SettingItem.TYPE_CATEGORY:
                    view = inflater.inflate(R.layout.list_item_title, parent, false);
                    break;
                case SettingItem.TYPE_SWITCH:
                    view = inflater.inflate(R.layout.list_item_switch, parent, false);
                    break;
                case SettingItem.TYPE_SELECTION:
                case SettingItem.TYPE_ACTION:
                    view = inflater.inflate(R.layout.list_item_selection, parent, false);
                    break;
            }
            TextView description = view.findViewById(R.id.description);
            ((TextView)view.findViewById(R.id.title)).setText(item.mTitle);
            if(item.mDescription!=null) {
                description.setText(item.mDescription);
                view.setTag(description);
            }
            else {
                if(description!=null) {
                    description.setVisibility(View.GONE);
                }
            }
            if(item.mType == SettingItem.TYPE_SWITCH) {
                SwitchCompat sc = view.findViewById(R.id.switch_ctrl);
                sc.setChecked((boolean) item.getValue());
                view.setTag(sc);
                sc.setOnClickListener(v -> onItemClick(null, null, position, item.mId));
            }            
            return view;
        }
        
        SettingItemAdapter(Context context, SettingItem[] items) {
            mContext = context;
            mItems = items;           
        }
        
        void updateDescription(String text, int position, ListView listView) {
            int firstVisible = listView.getFirstVisiblePosition();
            View view = listView.getChildAt(position - firstVisible);
            if(view != null) {
                TextView description = (TextView)view.getTag();
                description.setText(text);
            }
        }
    }

    private ActionListener mActionListener = sender -> {
        int type = 0;
        switch(sender.mId) {
            case R.string.export_data:
                type = ActionDialog.ACTION_EXPORT;
                break;
            case R.string.import_data:
                type = ActionDialog.ACTION_IMPORT;
                break;
            case R.string.change_pwd:
                type = ActionDialog.ACTION_RESET_PWD;
                break;
            case R.string.licence:
                type = ActionDialog.ACTION_LICENSE;
                break;
            case R.string.credits:
                type = ActionDialog.ACTION_CREDITS;
                break;
            case R.string.last_sync:
                if(Application.Options.mSync != C.Sync.NONE) {
                    SyncService.getInstance(Application.Options.mSync)
                            .initialize(Settings.this).setListener(Settings.this)
                            .connect(this, Application.getInstance().getLocalVersion());
                }
                return;
            case R.string.guide:
                Intent intent = new Intent(Settings.this, TourActivity.class);
                intent.putExtra(C.ACTIVITY, C.Activity.SETTINGS);
                startActivity(intent);
                overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
                return;
        }
        ActionDialog.create(type).show(getSupportFragmentManager(), TAG_DIALOG);
    };
    
    private SettingItemAdapter mAdapter;
    private boolean mShowOtherInitial;
    private int mRequestingPosition;
    private ListView mListView;
    
    private SettingItemAdapter initSettings() {
        int itemSize = 19;
        if(Application.Options.mFpStatus != C.Fingerprint.UNKNOWN) {
            itemSize += 1;
        }
        SettingItem[] items = new SettingItem[itemSize];
        int index = 0;
        String desc;
        items[index++] = new SettingItem(0, getString(R.string.general), null);
        items[index++] = new SettingItemAction(R.string.import_data,
                getString(R.string.import_data), null).setListener(mActionListener);
        items[index++] = new SettingItemAction(R.string.export_data,
                getString(R.string.export_data), null).setListener(mActionListener);
        items[index++] = new SettingItemSwitch(R.string.show_ungrouped, 
                getString(R.string.show_ungrouped), null)
                .setValue(Application.Options.mShowOther);
        items[index++] = new SettingItemSelection(R.string.theme, getString(R.string.theme), null)
                .setOptions(getResources().getStringArray(R.array.theme_names))
                .setValue(Application.Options.mTheme);
        items[index++] = new SettingItemAction(R.string.guide, getString(R.string.guide), null)
                .setListener(mActionListener);
        items[index++] = new SettingItem(0, getString(R.string.sync), null);
        items[index++] = new SettingItemSelection(R.string.sync_server, 
                getString(R.string.sync_server), null)
                .setOptions(getResources().getStringArray(R.array.sync_methods))
                .setValue(Application.Options.mSync);
        if(Application.Options.mSyncTime.after(new Date(0L))) {
            DateFormat df = DateFormat.getDateTimeInstance();
            desc = df.format(Application.Options.mSyncTime);
        }
        else{
            desc = getString(R.string.never);
        }
        items[index++] = new SettingItemAction(R.string.last_sync,
                getString(R.string.last_sync), desc).setListener(mActionListener);
        items[index++] = new SettingItemSwitch(R.string.sync_msg, getString(R.string.sync_msg), 
                null).setValue(Application.Options.mSyncMsg);
        items[index++] = new SettingItem(0, getString(R.string.security), null);
        int[] lock_options = {1000, 5 * 60 * 1000, 30 * 60 * 1000, 0};
        int selection = 0;
        int saved = Application.Options.mAutoLock;
        for(int i = 0; i < lock_options.length; ++i) {
            if(lock_options[i] == saved) {
                selection = i;
            }
        }
        items[index++] = new SettingItemSelection(R.string.auto_lock, getString(R.string.auto_lock),
                null).setOptions(getResources().getStringArray(R.array.lock_options))
                .setValue(selection);
        items[index++] = new SettingItemSwitch(R.string.show_password,
                getString(R.string.show_password), null)
                .setValue(Application.Options.mAlwaysShowPwd);
        items[index++] = new SettingItemSwitch(R.string.warn_copy, getString(R.string.warn_copy),
                null).setValue(Application.Options.mWarnCopyPwd);
        items[index++] = new SettingItemAction(R.string.change_pwd, getString(R.string.change_pwd),
                null).setListener(mActionListener);
        if(Application.Options.mFpStatus != C.Fingerprint.UNKNOWN) {
            items[index++] = new SettingItemSwitch(R.string.fp_title, getString(R.string.fp_title),
                    getString(R.string.fp_desc))
                    .setValue(Application.Options.mFpStatus == C.Fingerprint.ENABLED);
        }

        items[index++] = new SettingItem(0, getString(R.string.about), null);
        items[index++] = new SettingItemAction(R.string.licence, getString(R.string.licence),
                null).setListener(mActionListener);
        items[index++] = new SettingItemAction(R.string.credits, getString(R.string.credits),
                null).setListener(mActionListener);
        String versionName;
        try {
            versionName = this.getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
        } catch (PackageManager.NameNotFoundException e) {
            versionName = "2.2.0";
        }
        desc = getString(R.string.version, versionName);
        items[index] = new SettingItemAction(R.string.build, getString(R.string.build), desc);
        mAdapter = new SettingItemAdapter(this, items);
        return mAdapter;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        if(Application.getInstance() == null || Application.getInstance().getAccountManager() == null) {
            super.onCreate(savedInstanceState);
            startActivity(new Intent(this, HomeActivity.class));
            this.finish();
            return;
        }
        if(savedInstanceState != null) {
            mRequestingPosition = savedInstanceState.getInt("requested_position");
        }
        setTheme(C.THEMES[Application.Options.mTheme]);
        if(Application.getInstance().queryChange(Application.THEME)) {
            int[] primaryColors = {R.attr.colorPrimary, R.attr.colorPrimaryDark,
                    R.attr.colorAccent, R.attr.textColorNormal, R.attr.iconColorNormal};
            TypedArray ta = obtainStyledAttributes(primaryColors);
            for(int i = 0; i < C.ThemedColors.length; ++i) {
                C.ThemedColors[i] = ta.getColor(i, 0);
            }
            ta.recycle();
        }
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_settings);
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            View v = findViewById(R.id.activity_root);
            v.setBackgroundColor(C.ThemedColors[C.colorPrimary]);
        }
        mShowOtherInitial = Application.Options.mShowOther;
        setSupportActionBar(findViewById(R.id.toolbar));
        ActionBar actionBar = getSupportActionBar();
        if(actionBar!=null) {
            getSupportActionBar().setDisplayShowHomeEnabled(true);
            getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        }
        mListView = findViewById(R.id.list);
        mListView.setAdapter(initSettings());
        mListView.setOnItemClickListener(this);
    }
    
    @Override
    protected void onResume() {
        super.onResume();
        if(Application.getInstance().needAuth()) {
            Application.getInstance().setTimedOut();
            finish();
        }
    }
    
    @Override
    protected void onPause() {
        super.onPause();
        Application.getInstance().onPause(this);
    }
    
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        outState.putInt("requested_position", mRequestingPosition);
        super.onSaveInstanceState(outState);
    }

    @Override
    public void onBackPressed() {
        if(mShowOtherInitial != Application.Options.mShowOther) {
            Application.getInstance().notifyChange(Application.DATA_OTHER);
        }
        super.onBackPressed();
    }
    
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (item.getItemId() == android.R.id.home) {
            onBackPressed();
        }
        return true;
    }

    @Override
    protected void onActivityResult(final int requestCode, final int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        switch (requestCode) {
            case SyncService.REQ_RESOLUTION:
                SyncService.getInstance().onActivityResult(this, requestCode, resultCode, data);
                break;
            case ActionDialog.REQ_CODE_FILE_SELECTION:
                ActionDialog dialog = (ActionDialog)getSupportFragmentManager()
                        .findFragmentByTag(TAG_DIALOG);
                if (dialog != null) {
                    dialog.onFileSelected(this, resultCode, data);
                }
                Application.getInstance().ignoreNextPause();
                break;
        }
    }
    
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        SettingItem item = (SettingItem)mAdapter.getItem(position);
        item.onClick(view);
        mRequestingPosition = position;
        if(item.mType == SettingItem.TYPE_SWITCH) {
            boolean value = (boolean)item.getValue();
            if(view!=null) {
                SwitchCompat sc = (SwitchCompat) view.getTag();
                sc.setChecked(value);
            }            
            handleSwitchOption(item.mId, value);
        }
    }

    @Override
    public void onSelected(int selection) {
        SettingItemSelection item = (SettingItemSelection)mAdapter.getItem(mRequestingPosition);
        item.setValue(selection);
        mAdapter.updateDescription(item.getText(), mRequestingPosition, mListView);
        SharedPreferences.Editor editor = Application.getInstance().mSP.edit();
        switch (item.mId) {
            case R.string.theme:
                Application.getInstance().notifyChange(Application.THEME);
                Application.Options.mTheme = (int)item.getValue();
                editor.putInt(C.Keys.THEME, Application.Options.mTheme);
                startActivity(new Intent(this, Settings.class));
                finish();
                overridePendingTransition(0,0);
                break;
            case R.string.sync_server:
                Application.Options.mSync = (int)item.getValue();
                if(Application.Options.mSync == C.Sync.NONE) {
                    editor.putInt(C.Sync.SERVER, Application.Options.mSync);
                }
                else {
                    SyncService.getInstance(Application.Options.mSync).initialize(this)
                            .setListener(this).connect(this, Application.getInstance().getLocalVersion());
                    mRequestingPosition += 1;  // Bad design
                }
                break;
            case R.string.auto_lock:
                int[] lock_options = {1000, 5 * 60 * 1000, 30 * 60 * 1000, 0};
                Application.Options.mAutoLock = lock_options[(int)item.getValue()];
                editor.putInt(C.Keys.AUTO_LOCK_TIME, Application.Options.mAutoLock);
                break;
        }
        editor.apply();
    }

    @Override
    public void onFinish(boolean authenticate, int operation, String result) {
        if(result== null) {
            if(authenticate) {
                ActionDialog.create(ActionDialog.ACTION_AUTHENTICATE)
                        .show(getSupportFragmentManager(), TAG_DIALOG);
                return;
            }
            Application.showToast(Settings.this, operation == ActionDialog.ACTION_EXPORT ?
                    R.string.export_failed : R.string.import_failed, Toast.LENGTH_LONG);
        }
        else {
            if(operation==ActionDialog.ACTION_EXPORT) {
                Application.showToast(Settings.this,
                        getResources().getString(R.string.export_success, result), 
                        Toast.LENGTH_LONG);
            }
            else {
                if(Application.getInstance().getAccountManager().saveRequired()) {
                    Application.getInstance().saveData(this);
                    Application.getInstance().notifyChange(Application.DATA_ALL);
                }
                Application.showToast(Settings.this, R.string.import_success, Toast.LENGTH_LONG);
            }
        }
    }

    @Override
    public byte[] saveData() {
        if(Application.getInstance().getAccountManager().saveRequired()) {
            Application.getInstance().saveData(this);
        }
        return Application.getInstance().getData(this);
    }

    @Override
    public void onConfirm(String text, int type, int operation, int option) {

        if(operation == ActionDialog.ACTION_AUTHENTICATE) {
            new ImportExportTask(this, text).execute();
        }
        else if(operation == ActionDialog.ACTION_RESET_PWD && Application.Options.mFpStatus ==
                C.Fingerprint.ENABLED) {
            showFingerprintDialogIfPossible();
        }
        else if(operation == ActionDialog.ACTION_AUTHENTICATE2) {
            Application.FileHeader header = Application.FileHeader.parse(mData);
            if(text!=null) {
                new DecryptTask(mData, header, this ).execute(text);
            }
            else {
                Application.getInstance().increaseVersion(this,header.revision);
                SyncService.getInstance().send(Application.getInstance().getData(this));
            }
        }
        else {
            if(operation == ActionDialog.ACTION_IMPORT ||
                    operation == ActionDialog.ACTION_EXPORT) {
                getPermission2Do(text, type, operation, option);
            }
        }
    }

    @TargetApi(Build.VERSION_CODES.M)
    void getPermission2Do(String text, int type, int operation, int option){
        String permission = operation == ActionDialog.ACTION_IMPORT ?
                Manifest.permission.READ_EXTERNAL_STORAGE :
                Manifest.permission.WRITE_EXTERNAL_STORAGE;

        if (ActivityCompat.checkSelfPermission(this, permission)
                != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[]{permission},
                    PERMISSION_REQUEST);
            mActionType = type;
            mText = text;
            mOperation = operation;
            mOption = option;
        }
        else {
            new ImportExportTask(this, text, Application.getInstance().getPassword(),
                    type, operation, option).execute();
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                           @NonNull int[] grantResult){
        if(requestCode == PERMISSION_REQUEST) {
            if(grantResult[0] == PackageManager.PERMISSION_GRANTED) {
                new ImportExportTask(this, mText, Application.getInstance().getPassword(),
                        mActionType, mOperation, mOption).execute();
            }
        }
    }

    @Override
    public void onSyncFailed(int errorCode) { }
    
    @Override
    public void onSyncProgress(int actionCode) {
        String time = null;
        Application app = Application.getInstance();
        app.mSP.edit().putInt(C.Sync.SERVER, Application.Options.mSync).apply();
        if(actionCode == SyncService.CA.AUTH) {
            app.ignoreNextPause();
        } else if(actionCode == SyncService.CA.DATA_RECEIVED) {
            time = app.onSyncSucceed();
            byte[] data = SyncService.getInstance().requestData();
            Application.FileHeader fh = Application.FileHeader.parse(data);
            if(fh.valid && fh.revision > app.getLocalVersion()) {
                new DecryptTask(data, fh, this).execute(app.getPassword());
            }
            else if(fh.revision < app.getLocalVersion()){
                SyncService.getInstance().send(app.getData(this));
            }
            if(fh.revision != Application.Options.mSyncVersion) {
                app.onVersionUpdated(fh.revision);
            }
        } else if(actionCode == SyncService.CA.DATA_SENT) {
            time = app.onSyncSucceed();
            Application.showToast(this, R.string.sync_success_server, Toast.LENGTH_SHORT);
            app.onVersionUpdated(app.getLocalVersion());
        }
        if(time!=null) {
            mAdapter.updateDescription(time, mRequestingPosition, mListView);
        }
    }

    @Override
    public void preExecute() {  }

    @Override
    public void onFinished(boolean isSuccessful, AccountManager manager, String password,
                           byte[] data, Application.FileHeader header, Crypto crypto) {
        if(isSuccessful) {
            Application app = Application.getInstance();
            Application.showToast(this, R.string.sync_success_local, Toast.LENGTH_SHORT);
            Application.Options.mSyncVersion = header.revision;
            app.setAccountManager(manager, -1, getString(R.string.def_category));
            app.setCrypto(crypto);
            app.saveData(this,data, header);
            app.onVersionUpdated(header.revision);
            app.notifyChange(Application.DATA_ALL);
            if( !app.getPassword().equals(password)) {
                app.setPassword(password, false);
                if(Application.Options.mFpStatus == C.Fingerprint.ENABLED) {
                    showFingerprintDialogIfPossible();
                }
            }
        }
        else {
            mData = data;
            ActionDialog.create(ActionDialog.ACTION_AUTHENTICATE2).show(
                    getSupportFragmentManager(), "dialog_auth2");
        }
    }

    private void showFingerprintDialogIfPossible() {
        BiometricAuthHelper authHelper = new BiometricAuthHelper(true, this, this);
        authHelper.authenticate();
    }

    private void handleSwitchOption(int id, boolean value) {
        SharedPreferences.Editor editor = Application.getInstance().mSP.edit();
        switch(id) {
            case R.string.show_ungrouped:
                Application.Options.mShowOther = value;
                editor.putBoolean(C.Keys.SHOW_OTHER, value);
                break;
            case R.string.sync_msg:
                Application.Options.mSyncMsg = value;
                editor.putBoolean(C.Sync.MSG, value);
                break;
            case R.string.show_password:
                Application.Options.mAlwaysShowPwd = value;
                editor.putBoolean(C.Keys.SHOW_PWD, value);
                break;
            case R.string.warn_copy:
                Application.Options.mWarnCopyPwd = value;
                editor.putBoolean(C.Keys.WARN_COPY, value);
                break;
            case R.string.fp_title:
                if(value) {
                    showFingerprintDialogIfPossible();
                }
                else {
                    Application.getInstance().clearFpData();
                }
        }
        editor.apply();
    }

    @Override
    public void onCanceled(boolean isFirstTime) { }

    @Override
    public void onConfirmed(boolean isFirstTime, byte[] password) { }

}