/*
 * Copyright (C) 2014 The Android Open Source Project
 * modified
 * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
 */

package helium314.keyboard.accessibility

import android.graphics.Rect
import android.os.SystemClock
import helium314.keyboard.latin.utils.Log
import android.util.SparseIntArray
import android.view.MotionEvent
import helium314.keyboard.accessibility.AccessibilityLongPressTimer.LongPressTimerCallback
import helium314.keyboard.keyboard.*
import helium314.keyboard.latin.R
import helium314.keyboard.latin.utils.SubtypeLocaleUtils

/**
 * This class represents a delegate that can be registered in [MainKeyboardView] to enhance
 * accessibility support via composition rather via inheritance.
 */
class MainKeyboardAccessibilityDelegate(
    mainKeyboardView: MainKeyboardView,
    keyDetector: KeyDetector
) : KeyboardAccessibilityDelegate<MainKeyboardView>(mainKeyboardView, keyDetector), LongPressTimerCallback {
    /** The most recently set keyboard mode.  */
    private var mLastKeyboardMode = KEYBOARD_IS_HIDDEN
    // The rectangle region to ignore hover events.
    private val mBoundsToIgnoreHoverEvent = Rect()
    private val mAccessibilityLongPressTimer = AccessibilityLongPressTimer(this /* callback */, mainKeyboardView.context)

    // Since this method is called even when accessibility is off, make sure
    // to check the state before announcing anything.
    // Announce the language name only when the language is changed.
    // Announce the mode only when the mode is changed.
    // Announce the keyboard type only when the type is changed.
    /**
     * {@inheritDoc}
     */
    override var keyboard: Keyboard?
        get() = super.keyboard
        set(keyboard) {
            if (keyboard == null) {
                return
            }
            val lastKeyboard = super.keyboard
            super.keyboard = keyboard
            val lastKeyboardMode = mLastKeyboardMode
            mLastKeyboardMode = keyboard.mId.mMode
            // Since this method is called even when accessibility is off, make sure
            // to check the state before announcing anything.
            if (!AccessibilityUtils.instance.isAccessibilityEnabled) {
                return
            }
            // Announce the language name only when the language is changed.
            if (lastKeyboard == null || keyboard.mId.mSubtype != lastKeyboard.mId.mSubtype) {
                announceKeyboardLanguage(keyboard)
                return
            }
            // Announce the mode only when the mode is changed.
            if (keyboard.mId.mMode != lastKeyboardMode) {
                announceKeyboardMode(keyboard)
                return
            }
            // Announce the keyboard type only when the type is changed.
            if (keyboard.mId.mElementId != lastKeyboard.mId.mElementId) {
                announceKeyboardType(keyboard, lastKeyboard)
                return
            }
        }

    /**
     * Called when the keyboard is hidden and accessibility is enabled.
     */
    fun onHideWindow() {
        if (mLastKeyboardMode != KEYBOARD_IS_HIDDEN) {
            announceKeyboardHidden()
        }
        mLastKeyboardMode = KEYBOARD_IS_HIDDEN
    }

    /**
     * Announces which language of keyboard is being displayed.
     *
     * @param keyboard The new keyboard.
     */
    private fun announceKeyboardLanguage(keyboard: Keyboard) {
        val languageText = SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(
                keyboard.mId.mSubtype.rawSubtype)
        sendWindowStateChanged(languageText)
    }

    /**
     * Announces which type of keyboard is being displayed.
     * If the keyboard type is unknown, no announcement is made.
     *
     * @param keyboard The new keyboard.
     */
    private fun announceKeyboardMode(keyboard: Keyboard) {
        val context = mKeyboardView.context
        val modeTextResId = KEYBOARD_MODE_RES_IDS[keyboard.mId.mMode]
        if (modeTextResId == 0) {
            return
        }
        val modeText = context.getString(modeTextResId)
        val text = context.getString(R.string.announce_keyboard_mode, modeText)
        sendWindowStateChanged(text)
    }

    /**
     * Announces which type of keyboard is being displayed.
     *
     * @param keyboard The new keyboard.
     * @param lastKeyboard The last keyboard.
     */
    private fun announceKeyboardType(keyboard: Keyboard, lastKeyboard: Keyboard) {
        val lastElementId = lastKeyboard.mId.mElementId
        val resId = when (keyboard.mId.mElementId) {
            KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED, KeyboardId.ELEMENT_ALPHABET -> {
                if (lastElementId == KeyboardId.ELEMENT_ALPHABET
                        || lastElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) {
                    // Transition between alphabet mode and automatic shifted mode should be silently
                    // ignored because it can be determined by each key's talk back announce.
                    return
                }
                R.string.spoken_description_mode_alpha
            }
            KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED -> {
                if (lastElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) {
                    // Resetting automatic shifted mode by pressing the shift key causes the transition
                    // from automatic shifted to manual shifted that should be silently ignored.
                    return
                }
                R.string.spoken_description_shiftmode_on
            }
            KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED -> {
                if (lastElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED) {
                    // Resetting caps locked mode by pressing the shift key causes the transition
                    // from shift locked to shift lock shifted that should be silently ignored.
                    return
                }
                R.string.spoken_description_shiftmode_locked
            }
            KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED -> R.string.spoken_description_shiftmode_locked
            KeyboardId.ELEMENT_SYMBOLS -> R.string.spoken_description_mode_symbol
            KeyboardId.ELEMENT_SYMBOLS_SHIFTED -> R.string.spoken_description_mode_symbol_shift
            KeyboardId.ELEMENT_PHONE -> R.string.spoken_description_mode_phone
            KeyboardId.ELEMENT_PHONE_SYMBOLS -> R.string.spoken_description_mode_phone_shift
            else -> return
        }
        sendWindowStateChanged(resId)
    }

    /**
     * Announces that the keyboard has been hidden.
     */
    private fun announceKeyboardHidden() {
        sendWindowStateChanged(R.string.announce_keyboard_hidden)
    }

    override fun performClickOn(key: Key) {
        val x = key.hitBox.centerX()
        val y = key.hitBox.centerY()
        if (DEBUG_HOVER) {
            Log.d(TAG, "performClickOn: key=" + key
                    + " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y))
        }
        if (mBoundsToIgnoreHoverEvent.contains(x, y)) {
            // This hover exit event points to the key that should be ignored.
            // Clear the ignoring region to handle further hover events.
            mBoundsToIgnoreHoverEvent.setEmpty()
            return
        }
        super.performClickOn(key)
    }

    override fun onHoverEnterTo(key: Key) {
        val x = key.hitBox.centerX()
        val y = key.hitBox.centerY()
        if (DEBUG_HOVER) {
            Log.d(TAG, "onHoverEnterTo: key=" + key
                    + " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y))
        }
        mAccessibilityLongPressTimer.cancelLongPress()
        if (mBoundsToIgnoreHoverEvent.contains(x, y)) {
            return
        }
        // This hover enter event points to the key that isn't in the ignoring region.
        // Further hover events should be handled.
        mBoundsToIgnoreHoverEvent.setEmpty()
        super.onHoverEnterTo(key)
        if (key.isLongPressEnabled) {
            mAccessibilityLongPressTimer.startLongPress(key)
        }
    }

    override fun onHoverExitFrom(key: Key) {
        val x = key.hitBox.centerX()
        val y = key.hitBox.centerY()
        if (DEBUG_HOVER) {
            Log.d(TAG, "onHoverExitFrom: key=" + key
                    + " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y))
        }
        mAccessibilityLongPressTimer.cancelLongPress()
        super.onHoverExitFrom(key)
    }

    override fun performLongClickOn(key: Key) {
        if (DEBUG_HOVER) {
            Log.d(TAG, "performLongClickOn: key=$key")
        }
        val tracker = PointerTracker.getPointerTracker(HOVER_EVENT_POINTER_ID)
        val eventTime = SystemClock.uptimeMillis()
        val x = key.hitBox.centerX()
        val y = key.hitBox.centerY()
        val downEvent = MotionEvent.obtain(eventTime, eventTime, MotionEvent.ACTION_DOWN, x.toFloat(), y.toFloat(), 0)
        // Inject a fake down event to {@link PointerTracker} to handle a long press correctly.
        tracker.processMotionEvent(downEvent, mKeyDetector)
        downEvent.recycle()
        // Invoke {@link PointerTracker#onLongPressed()} as if a long press timeout has passed.
        tracker.onLongPressed()
        // If {@link Key#hasNoPanelAutoPopupKeys()} is true (such as "0 +" key on the phone layout)
        // or a key invokes IME switcher dialog, we should just ignore the next
        // {@link #onRegisterHoverKey(Key,MotionEvent)}. It can be determined by whether
        // {@link PointerTracker} is in operation or not.
        if (tracker.isInOperation) {
            // This long press shows a popup keys keyboard and further hover events should be
            // handled.
            mBoundsToIgnoreHoverEvent.setEmpty()
            return
        }
        // This long press has handled at {@link MainKeyboardView#onLongPress(PointerTracker)}.
        // We should ignore further hover events on this key.
        mBoundsToIgnoreHoverEvent.set(key.hitBox)
        if (key.hasNoPanelAutoPopupKey()) {
            // This long press has registered a code point without showing a popup keys keyboard.
            // We should talk back the code point if possible.
            val codePointOfNoPanelAutoPopupKey = key.popupKeys?.get(0)?.mCode ?: return
            val text: String = KeyCodeDescriptionMapper.instance.getDescriptionForCodePoint(
                    mKeyboardView.context, codePointOfNoPanelAutoPopupKey) ?: return
            sendWindowStateChanged(text)
        }
    }

    companion object {
        private val TAG = MainKeyboardAccessibilityDelegate::class.java.simpleName
        /** Map of keyboard modes to resource IDs.  */
        private val KEYBOARD_MODE_RES_IDS = SparseIntArray().apply {
            put(KeyboardId.MODE_DATE, R.string.keyboard_mode_date)
            put(KeyboardId.MODE_DATETIME, R.string.keyboard_mode_date_time)
            put(KeyboardId.MODE_EMAIL, R.string.keyboard_mode_email)
            put(KeyboardId.MODE_IM, R.string.keyboard_mode_im)
            put(KeyboardId.MODE_NUMBER, R.string.keyboard_mode_number)
            put(KeyboardId.MODE_PHONE, R.string.keyboard_mode_phone)
            put(KeyboardId.MODE_TEXT, R.string.keyboard_mode_text)
            put(KeyboardId.MODE_TIME, R.string.keyboard_mode_time)
            put(KeyboardId.MODE_URL, R.string.keyboard_mode_url)
        }
        private const val KEYBOARD_IS_HIDDEN = -1
    }
}