package org.mariotaku.twidere.util;

import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.collection.SparseArrayCompat;
import android.text.TextUtils;
import android.view.KeyEvent;

import org.mariotaku.twidere.R;
import org.mariotaku.twidere.activity.ComposeActivity;
import org.mariotaku.twidere.activity.QuickSearchBarActivity;
import org.mariotaku.twidere.constant.KeyboardShortcutConstants;

import java.util.HashMap;
import java.util.Locale;
import java.util.Map.Entry;

import javax.inject.Singleton;

import static org.mariotaku.twidere.TwidereConstants.KEYBOARD_SHORTCUTS_PREFERENCES_NAME;
import static org.mariotaku.twidere.constant.IntentConstants.INTENT_ACTION_COMPOSE;
import static org.mariotaku.twidere.constant.IntentConstants.INTENT_ACTION_QUICK_SEARCH;

@Singleton
public class KeyboardShortcutsHandler implements KeyboardShortcutConstants {

    public static final int MODIFIER_FLAG_CTRL = 0x00000001;
    public static final int MODIFIER_FLAG_SHIFT = 0x00000002;
    public static final int MODIFIER_FLAG_ALT = 0x00000004;
    public static final int MODIFIER_FLAG_META = 0x00000008;
    public static final int MODIFIER_FLAG_FN = 0x000000010;

    private static final String KEYCODE_STRING_PREFIX = "KEYCODE_";

    private static final HashMap<String, Integer> sActionLabelMap = new HashMap<>();
    private static final SparseArrayCompat<String> sMetaNameMap = new SparseArrayCompat<>();

    static {
        sActionLabelMap.put(ACTION_COMPOSE, R.string.action_compose);
        sActionLabelMap.put(ACTION_SEARCH, R.string.action_search);
        sActionLabelMap.put(ACTION_MESSAGE, R.string.new_direct_message);
        sActionLabelMap.put(ACTION_HOME_ACCOUNTS_DASHBOARD, R.string.open_accounts_dashboard);
        sActionLabelMap.put(ACTION_STATUS_REPLY, R.string.action_reply);
        sActionLabelMap.put(ACTION_STATUS_RETWEET, R.string.action_retweet);
        sActionLabelMap.put(ACTION_STATUS_FAVORITE, R.string.action_like);
        sActionLabelMap.put(ACTION_NAVIGATION_PREVIOUS, R.string.previous_item);
        sActionLabelMap.put(ACTION_NAVIGATION_NEXT, R.string.next_item);
        sActionLabelMap.put(ACTION_NAVIGATION_PAGE_DOWN, R.string.page_down);
        sActionLabelMap.put(ACTION_NAVIGATION_PAGE_UP, R.string.page_up);
        sActionLabelMap.put(ACTION_NAVIGATION_TOP, R.string.jump_to_top);
        sActionLabelMap.put(ACTION_NAVIGATION_REFRESH, R.string.action_refresh);
        sActionLabelMap.put(ACTION_NAVIGATION_PREVIOUS_TAB, R.string.previous_tab);
        sActionLabelMap.put(ACTION_NAVIGATION_NEXT_TAB, R.string.next_tab);
        sActionLabelMap.put(ACTION_NAVIGATION_BACK, R.string.keyboard_shortcut_back);

        sMetaNameMap.put(KeyEvent.META_FUNCTION_ON, "fn");
        sMetaNameMap.put(KeyEvent.META_META_ON, "meta");
        sMetaNameMap.put(KeyEvent.META_CTRL_ON, "ctrl");
        sMetaNameMap.put(KeyEvent.META_ALT_ON, "alt");
        sMetaNameMap.put(KeyEvent.META_SHIFT_ON, "shift");
    }

    private final SharedPreferences mPreferences;

    public KeyboardShortcutsHandler(final Context context) {
        mPreferences = context.getSharedPreferences(KEYBOARD_SHORTCUTS_PREFERENCES_NAME, Context.MODE_PRIVATE);
    }

    public String findAction(@NonNull KeyboardShortcutSpec spec) {
        return mPreferences.getString(spec.getRawKey(), null);
    }

    public KeyboardShortcutSpec findKey(String action) {
        for (Entry<String, ?> entry : mPreferences.getAll().entrySet()) {
            if (action.equals(entry.getValue())) {
                final KeyboardShortcutSpec spec = new KeyboardShortcutSpec(entry.getKey(), action);
                if (spec.isValid()) return spec;
            }
        }
        return null;
    }

    public static String getActionLabel(Context context, String action) {
        if (!sActionLabelMap.containsKey(action)) return null;
        final int labelRes = sActionLabelMap.get(action);
        return context.getString(labelRes);
    }

    @Nullable
    public String getKeyAction(final String contextTag, final int keyCode, final KeyEvent event, int metaState) {
        if (!isValidForHotkey(keyCode, event)) return null;
        final String key = getKeyEventKey(contextTag, keyCode, event, metaState);
        return mPreferences.getString(key, null);
    }

    public static String getKeyEventKey(String contextTag, int keyCode, KeyEvent event, int metaState) {
        if (!isValidForHotkey(keyCode, event)) return null;
        final StringBuilder keyNameBuilder = new StringBuilder();
        if (!TextUtils.isEmpty(contextTag)) {
            keyNameBuilder.append(contextTag);
            keyNameBuilder.append(".");
        }
        final int normalizedMetaState = KeyEvent.normalizeMetaState(metaState | event.getMetaState());

        for (int i = 0, j = sMetaNameMap.size(); i < j; i++) {
            if ((sMetaNameMap.keyAt(i) & normalizedMetaState) != 0) {
                keyNameBuilder.append(sMetaNameMap.valueAt(i));
                keyNameBuilder.append("+");
            }
        }
        final String keyCodeString = KeyEvent.keyCodeToString(keyCode);
        if (keyCodeString.startsWith(KEYCODE_STRING_PREFIX)) {
            keyNameBuilder.append(keyCodeString.substring(KEYCODE_STRING_PREFIX.length()).toLowerCase(Locale.US));
        }
        return keyNameBuilder.toString();
    }

    public static String getKeyEventKey(String contextTag, int metaState, String keyName) {
        final StringBuilder keyNameBuilder = new StringBuilder();
        if (!TextUtils.isEmpty(contextTag)) {
            keyNameBuilder.append(contextTag);
            keyNameBuilder.append(".");
        }

        for (int i = 0, j = sMetaNameMap.size(); i < j; i++) {
            if ((sMetaNameMap.keyAt(i) & metaState) != 0) {
                keyNameBuilder.append(sMetaNameMap.valueAt(i));
                keyNameBuilder.append("+");
            }
        }
        keyNameBuilder.append(keyName);
        return keyNameBuilder.toString();
    }

    public static int getKeyEventMeta(String name) {
        for (int i = 0, j = sMetaNameMap.size(); i < j; i++) {
            if (sMetaNameMap.valueAt(i).equalsIgnoreCase(name)) return sMetaNameMap.keyAt(i);
        }
        return 0;
    }

    public static KeyboardShortcutSpec getKeyboardShortcutSpec(String contextTag, int keyCode, KeyEvent event, int metaState) {
        if (!isValidForHotkey(keyCode, event)) return null;
        int metaStateNormalized = 0;
        for (int i = 0, j = sMetaNameMap.size(); i < j; i++) {
            if ((sMetaNameMap.keyAt(i) & metaState) != 0) {
                metaStateNormalized |= sMetaNameMap.keyAt(i);
            }
        }
        final String keyCodeString = KeyEvent.keyCodeToString(keyCode);
        if (keyCodeString.startsWith(KEYCODE_STRING_PREFIX)) {
            final String keyName = keyCodeString.substring(KEYCODE_STRING_PREFIX.length()).toLowerCase(Locale.US);
            return new KeyboardShortcutSpec(contextTag, metaStateNormalized, keyName, null);
        }
        return null;
    }

    public boolean handleKey(final Context context, final String contextTag, final int keyCode, final KeyEvent event, int metaState) {
        final String action = getKeyAction(contextTag, keyCode, event, metaState);
        if (action == null) return false;
        switch (action) {
            case ACTION_COMPOSE: {
                context.startActivity(new Intent(context, ComposeActivity.class).setAction(INTENT_ACTION_COMPOSE));
                return true;
            }
            case ACTION_SEARCH: {
                context.startActivity(new Intent(context, QuickSearchBarActivity.class).setAction(INTENT_ACTION_QUICK_SEARCH));
                return true;
            }
            case ACTION_MESSAGE: {
                // TODO: open message creator
                return true;
            }
        }
        return false;
    }

    public static boolean isValidForHotkey(int keyCode, KeyEvent event) {
        // These keys must use with modifiers
        switch (keyCode) {
            case KeyEvent.KEYCODE_DPAD_CENTER:
            case KeyEvent.KEYCODE_DPAD_DOWN:
            case KeyEvent.KEYCODE_DPAD_LEFT:
            case KeyEvent.KEYCODE_DPAD_RIGHT:
            case KeyEvent.KEYCODE_DPAD_UP:
            case KeyEvent.KEYCODE_ENTER:
            case KeyEvent.KEYCODE_NUMPAD_ENTER:
            case KeyEvent.KEYCODE_TAB: {
                if (event.hasNoModifiers()) return false;
                break;
            }
        }
        return !isNavigationKey(keyCode) && !KeyEvent.isModifierKey(keyCode) && keyCode != KeyEvent.KEYCODE_UNKNOWN;
    }

    private static boolean isNavigationKey(int keyCode) {
        return keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_HOME
                || keyCode == KeyEvent.KEYCODE_MENU;
    }

    public static String metaToFriendlyString(int metaState) {
        final StringBuilder keyNameBuilder = new StringBuilder();
        for (int i = 0, j = sMetaNameMap.size(); i < j; i++) {
            if ((sMetaNameMap.keyAt(i) & metaState) != 0) {
                final String value = sMetaNameMap.valueAt(i);
                keyNameBuilder.append(value.substring(0, 1).toUpperCase(Locale.US));
                keyNameBuilder.append(value.substring(1));
                keyNameBuilder.append("+");
            }
        }
        return keyNameBuilder.toString();
    }

    public void register(KeyboardShortcutSpec spec, String action) {
        unregister(action);
        mPreferences.edit().putString(spec.getRawKey(), action).apply();
    }

    public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
        mPreferences.registerOnSharedPreferenceChangeListener(listener);
    }

    public void reset() {
        final Editor editor = mPreferences.edit();
        editor.clear();
        editor.putString("n", ACTION_COMPOSE);
        editor.putString("m", ACTION_MESSAGE);
        editor.putString("slash", ACTION_SEARCH);
        editor.putString("home.q", ACTION_HOME_ACCOUNTS_DASHBOARD);
        editor.putString("navigation.period", ACTION_NAVIGATION_REFRESH);
        editor.putString("navigation.j", ACTION_NAVIGATION_NEXT);
        editor.putString("navigation.k", ACTION_NAVIGATION_PREVIOUS);
        editor.putString("navigation.h", ACTION_NAVIGATION_PREVIOUS_TAB);
        editor.putString("navigation.l", ACTION_NAVIGATION_NEXT_TAB);
        editor.putString("navigation.u", ACTION_NAVIGATION_TOP);
        editor.putString("status.f", ACTION_STATUS_FAVORITE);
        editor.putString("status.r", ACTION_STATUS_REPLY);
        editor.putString("status.t", ACTION_STATUS_RETWEET);
        editor.apply();
    }

    public void unregister(String action) {
        final Editor editor = mPreferences.edit();
        for (Entry<String, ?> entry : mPreferences.getAll().entrySet()) {
            if (action.equals(entry.getValue())) {
                final KeyboardShortcutSpec spec = new KeyboardShortcutSpec(entry.getKey(), action);
                if (spec.isValid()) {
                    editor.remove(spec.getRawKey());
                }
            }
        }
        editor.apply();
    }

    public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
        mPreferences.unregisterOnSharedPreferenceChangeListener(listener);
    }

    public static int getMetaStateForKeyCode(int keyCode) {
        switch (keyCode) {
            case KeyEvent.KEYCODE_CTRL_LEFT:
                return KeyEvent.META_CTRL_LEFT_ON;
            case KeyEvent.KEYCODE_CTRL_RIGHT:
                return KeyEvent.META_CTRL_RIGHT_ON;
            case KeyEvent.KEYCODE_SHIFT_LEFT:
                return KeyEvent.META_SHIFT_LEFT_ON;
            case KeyEvent.KEYCODE_SHIFT_RIGHT:
                return KeyEvent.META_SHIFT_RIGHT_ON;
            case KeyEvent.KEYCODE_ALT_LEFT:
                return KeyEvent.META_ALT_LEFT_ON;
            case KeyEvent.KEYCODE_ALT_RIGHT:
                return KeyEvent.META_ALT_RIGHT_ON;
            case KeyEvent.KEYCODE_META_LEFT:
                return KeyEvent.META_META_LEFT_ON;
            case KeyEvent.KEYCODE_META_RIGHT:
                return KeyEvent.META_META_RIGHT_ON;
            case KeyEvent.KEYCODE_FUNCTION:
                return KeyEvent.META_FUNCTION_ON;
        }
        return 0;
    }

    public interface KeyboardShortcutCallback extends KeyboardShortcutConstants {

        boolean handleKeyboardShortcutRepeat(@NonNull KeyboardShortcutsHandler handler, int keyCode,
                                             int repeatCount, @NonNull KeyEvent event, int metaState);

        boolean handleKeyboardShortcutSingle(@NonNull KeyboardShortcutsHandler handler, int keyCode,
                                             @NonNull KeyEvent event, int metaState);

        boolean isKeyboardShortcutHandled(@NonNull KeyboardShortcutsHandler handler, int keyCode,
                                          @NonNull KeyEvent event, int metaState);
    }

    public interface TakeAllKeyboardShortcut {

    }

    /**
     * Created by mariotaku on 15/4/11.
     */
    public static final class KeyboardShortcutSpec {

        private String action;
        @Nullable
        private String contextTag;
        private int keyMeta;
        private String keyName;

        public KeyboardShortcutSpec(@Nullable String contextTag, int keyMeta, String keyName, String action) {
            this.contextTag = contextTag;
            this.keyMeta = keyMeta;
            this.keyName = keyName;
            this.action = action;
        }

        public KeyboardShortcutSpec(String key, String action) {
            final int contextDotIdx = key.indexOf('.');
            if (contextDotIdx != -1) {
                contextTag = key.substring(0, contextDotIdx);
            }
            int idx = contextDotIdx, previousIdx = idx;
            while ((idx = key.indexOf('+', idx + 1)) != -1) {
                keyMeta |= getKeyEventMeta(key.substring(previousIdx + 1, idx));
                previousIdx = idx;
            }
            keyName = key.substring(previousIdx + 1);
            this.action = action;
        }

        public KeyboardShortcutSpec copy() {
            return new KeyboardShortcutSpec(contextTag, keyMeta, keyName, action);
        }

        public String getAction() {
            return action;
        }

        @Nullable
        public String getContextTag() {
            return contextTag;
        }

        public void setContextTag(@Nullable String contextTag) {
            this.contextTag = contextTag;
        }

        public int getKeyMeta() {
            return keyMeta;
        }

        public String getKeyName() {
            return keyName;
        }

        public String getRawKey() {
            return getKeyEventKey(contextTag, keyMeta, keyName);
        }

        public String getValueName(Context context) {
            return getActionLabel(context, action);
        }

        public boolean isValid() {
            return keyName != null;
        }

        public String toKeyString() {
            return metaToFriendlyString(keyMeta) + keyToFriendlyString(keyName);
        }

        @Override
        public String toString() {
            return "KeyboardShortcutSpec{" +
                    "action='" + action + '\'' +
                    ", contextTag='" + contextTag + '\'' +
                    ", keyMeta=" + keyMeta +
                    ", keyName='" + keyName + '\'' +
                    '}';
        }

        private static String keyToFriendlyString(String keyName) {
            if (keyName == null) return null;
            final String upperName = keyName.toUpperCase(Locale.US);
            final int keyCode = KeyEvent.keyCodeFromString(KEYCODE_STRING_PREFIX + upperName);
            if (keyCode == KeyEvent.KEYCODE_UNKNOWN) return upperName;
            if (keyCode == KeyEvent.KEYCODE_DEL) return "Backspace";
            if (keyCode == KeyEvent.KEYCODE_FORWARD_DEL) return "Delete";
            if (keyCode == KeyEvent.KEYCODE_SPACE) return "Space";
            final char displayLabel = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode).getDisplayLabel();
            if (displayLabel == 0) return keyName.toUpperCase(Locale.US);
            return String.valueOf(displayLabel);
        }
    }
}
