
package app.crossword.yourealwaysbe.forkyz

import java.io.IOException
import java.util.Objects
import java.util.function.Consumer
import java.util.regex.Pattern
import kotlinx.coroutines.launch
import kotlinx.coroutines.delay

import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Looper
import android.speech.RecognizerIntent
import android.speech.tts.TextToSpeech
import android.util.Log
import android.view.View
import android.view.accessibility.AccessibilityManager
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Arrangement.Center
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.type
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.fromHtml
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.net.toUri
import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle

import dagger.hilt.android.AndroidEntryPoint

import app.crossword.yourealwaysbe.forkyz.exttools.AppNotFoundException
import app.crossword.yourealwaysbe.forkyz.exttools.ExternalToolLauncher
import app.crossword.yourealwaysbe.forkyz.inttools.InternalToolLauncher
import app.crossword.yourealwaysbe.forkyz.menu.BaseMenuModifier
import app.crossword.yourealwaysbe.forkyz.menu.MenuDropdownHeading
import app.crossword.yourealwaysbe.forkyz.menu.MenuHelp
import app.crossword.yourealwaysbe.forkyz.menu.MenuSettings
import app.crossword.yourealwaysbe.forkyz.menu.MenuSubEntry
import app.crossword.yourealwaysbe.forkyz.menu.MenuText
import app.crossword.yourealwaysbe.forkyz.settings.KeyboardLayout
import app.crossword.yourealwaysbe.forkyz.util.ImaginaryTimer
import app.crossword.yourealwaysbe.forkyz.util.InputConnectionMediator
import app.crossword.yourealwaysbe.forkyz.util.SpeechContract
import app.crossword.yourealwaysbe.forkyz.util.VoiceCommands.VoiceCommand
import app.crossword.yourealwaysbe.forkyz.view.BoardWordEdit
import app.crossword.yourealwaysbe.forkyz.view.ChooseFlagColorDialog
import app.crossword.yourealwaysbe.forkyz.view.ForkyzKeyboard
import app.crossword.yourealwaysbe.forkyz.view.OKCancelDialog
import app.crossword.yourealwaysbe.forkyz.view.OKDialog
import app.crossword.yourealwaysbe.forkyz.view.PlayboardTextRenderer
import app.crossword.yourealwaysbe.forkyz.view.PuzzleFinishedDialog
import app.crossword.yourealwaysbe.forkyz.view.PuzzleInfoDialog
import app.crossword.yourealwaysbe.forkyz.view.SpecialEntryDialog
import app.crossword.yourealwaysbe.forkyz.view.SpecialKey
import app.crossword.yourealwaysbe.forkyz.view.StringDialog
import app.crossword.yourealwaysbe.forkyz.view.TwoButtonDialog
import app.crossword.yourealwaysbe.puz.Clue
import app.crossword.yourealwaysbe.puz.ClueID
import app.crossword.yourealwaysbe.puz.Playboard
import app.crossword.yourealwaysbe.puz.Playboard.PlayboardChanges
import app.crossword.yourealwaysbe.puz.Playboard.Word
import app.crossword.yourealwaysbe.puz.Position
import app.crossword.yourealwaysbe.puz.Puzzle
import app.crossword.yourealwaysbe.puz.Zone
import app.crossword.yourealwaysbe.puz.MovementStrategy

@AndroidEntryPoint
abstract class PuzzleActivity() : ForkyzActivity() {
    private val TAG = "ForkyzPuzzleActivity"

    protected val displayWidth : Int by lazy {
        val metrics = getResources().getDisplayMetrics()
        (metrics.widthPixels / metrics.density).toInt()
    }
    protected val displayHeight : Int by lazy {
        val metrics = getResources().getDisplayMetrics()
        (metrics.heightPixels / metrics.density).toInt()
    }
    protected val isPortrait : Boolean by lazy {
        displayWidth <= displayHeight
    }

    /**
     * Get currently known value of volume activates voice
     *
     * Needs to be non-blocking so that onKeyEvent can return the
     * right value immediately, so use a live value that may be unknown
     * (defaults to false)
     */
    protected val isVolumeDownActivatesVoicePref : Boolean
        get() {
            return viewModel.voiceState.value?.volumeActivatesVoice ?: false
        }

    protected val isAnnounceClueEquals : Boolean
        get() {
            return viewModel.voiceState.value?.equalsAnnounceClue ?: false
        }

    protected val inputConnectionMediator = InputConnectionMediator()
    private var ttsService : TextToSpeech? = null
    private var ttsReady : Boolean = false

    abstract protected val viewModel : PuzzleActivityViewModel

    private val voiceInputLauncher : ActivityResultLauncher<String> =
        registerForActivityResult(SpeechContract()) { text ->
            viewModel.dispatchVoiceCommand(text)
        }

    private val isAccessibilityServiceRunning : Boolean
        get() {
            val manager = getSystemService(
                Context.ACCESSIBILITY_SERVICE
            ) as AccessibilityManager?
            return manager?.isEnabled() ?: false
        }

    // only enabled when non-native keyboard is showing and back will cause it
    // to hide
    private val backCallback = object : OnBackPressedCallback(false) {
        override fun handleOnBackPressed() {
            viewModel.hideKeyboard(true)
        }
    }

    override fun onCreate(savedInstanceState : Bundle?) {
        super.onCreate(savedInstanceState)
        onBackPressedDispatcher.addCallback(this, backCallback)
    }

    override fun onPause() {
        viewModel.stopTimer()
        viewModel.saveBoard()

        ttsService?.let {
            it.shutdown()
            ttsService = null
            ttsReady = false
        }

        super.onPause()
    }

    override fun onResume() {
        super.onResume()

        if (!viewModel.canResume) {
            Log.i(TAG, "No puzzle board, puzzle activity finishing.")
            finish()
            return
        }

        viewModel.startTimer()
    }


    // used for the puzzle info dialog (maybe remove when it has a view
    // model)
    fun getTimer() : ImaginaryTimer? {
        return viewModel.timer
    }

    /**
     * Miniboard current word view
     *
     * Shows current word on board. Assumes should take immediate focus
     * and setup as view targeted by keyboard.
     *
     * @param word if not null show this word not the currently selected
     * @param onClick onClick method for BoardClickListener
     * @param onLongClick onLongClick method for BoardClickListener
     * @param longClickAccessibilityAction string ID of long click
     * action description
     * @param incognitoMode whether to make the view invisible and take
     * no space (ok, 1 pixel)
     * @param useSuppressNotesList whether to use the given
     * suppressNotesList or let BoardWordEditView use its default
     * @param suppressNotesList for PlayboardRenderer.drawBox
     * @param movementStrategy the movement strategy to use for the miniboard
     */
    @Composable
    protected fun Miniboard(
        modifier : Modifier = Modifier,
        word : Word? = null,
        onClick : (Position?, Word?) -> Unit,
        onLongClick : (Position?) -> Unit,
        longClickAccessibilityAction : Int,
        incognitoMode : Boolean = false,
        ignoreScratchMode : Boolean = false,
        useSuppressNotesList : Boolean = false,
        suppressNotesList : Set<String>? = null,
        onFocusChanged : (Boolean) -> Unit = { },
        movementStrategy : MovementStrategy = MovementStrategy.STOP_ON_END,
    ) {
        viewModel.getBoard()?.let { board ->
            BoardWordEdit(
                modifier = modifier,
                board = board,
                word = word,
                contentDescription = stringResource(R.string.current_word),
                onClick = onClick,
                onLongClick = onLongClick,
                longClickAccessibilityAction
                    = stringResource(longClickAccessibilityAction),
                inputConnectionMediator = inputConnectionMediator,
                incognitoMode = incognitoMode,
                ignoreScratchMode = ignoreScratchMode,
                useSuppressNotesList = useSuppressNotesList,
                suppressNotesList = suppressNotesList,
                onFocusChanged = onFocusChanged,
                movementStrategy = movementStrategy,
            )
        }
    }

    /**
     * Use if you want to update your UI based on the timer
     *
     * Callback is a launched effect.
     */
    @Composable
    protected fun OnTimerUpdate(onTimerUpdate : () -> Unit) {
        val state by viewModel.uiState.observeAsState()
        val showTimer = state?.showTimer ?: false
        LaunchedEffect(showTimer) {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                while (showTimer) {
                    delay(1000L)
                    onTimerUpdate()
                }
            }
        }
    }

    @Composable
    protected fun Modifier.puzzleStatusBarColor() : Modifier {
        val state by viewModel.menuState.observeAsState()

        // always return same padding
        // not returning status bar padding means the app freezes if
        // imepadding is added afterwards.
        // potentially just a material bug that might be fixed in
        // future

        val color = if (state?.showErrorsState?.isShowing ?: false)
            themeHelper.cheatedColor
        else
            themeHelper.statusBarColor

        // for older devices where Compose doesn't draw over status bar
        // for some reason
        LaunchedEffect(color) {
            utils.legacySetStatusBarColor(this@PuzzleActivity, color.toArgb())
        }

        return this.background(color).statusBarsPadding()
    }

    /**
     * Clue text for top app bar
     *
     * If null, then displays a placeholder
     */
    @Composable
    protected fun ClueText(
        modifier : Modifier = Modifier,
        clueText : String? = null,
        onClick : () -> Unit = { },
        onClickDescription : String = "",
        onLongClick : () -> Unit = { },
        onLongClickDescription : String = "",
    ) {
        val fullModifier = modifier.combinedClickable(
            onClick = onClick,
            onClickLabel = onClickDescription,
            onLongClick = onLongClick,
            onLongClickLabel = onLongClickDescription,
        )
        val text = clueText ?: stringResource(R.string.unknown_hint)

        // Text doesn't support autoSize yet, so rebuild BasicText
        // from (or close enough)
        // https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/material/material/src/commonMain/kotlin/androidx/compose/material/Text.kt
        val color = LocalContentColor.current
        val clueSize = getResources().getInteger(R.integer.clue_text_size)
        BasicText(
            modifier = fullModifier,
            text = AnnotatedString.fromHtml(text),
            autoSize =  TextAutoSize.StepBased(
                minFontSize = 5.sp,
                maxFontSize = clueSize.sp,
                stepSize = 1.sp,
            ),
            color = { color },
        )
    }

    protected open fun onKeyEvent(event : KeyEvent) : Boolean {
        return when (event.key) {
            Key.Equals -> {
                if (isAnnounceClueEquals) {
                    if (event.type == KeyEventType.KeyUp)
                        viewModel.announceClue(false)
                    true
                } else false
            }
            Key.VolumeDown -> {
                if (isVolumeDownActivatesVoicePref) {
                    if (event.type == KeyEventType.KeyUp)
                        launchVoiceInput()
                    true
                } else false
            }
            else -> false
        }
    }

    /**
     * Convert letter/digit key event to uppercase char
     *
     * Or null if not a char key
     */
    protected fun letterOrDigitKeyToChar(key : Key) : Char?
        = when (key) {
            // must be a better way..
            Key.A -> 'A'
            Key.B -> 'B'
            Key.C -> 'C'
            Key.D -> 'D'
            Key.E -> 'E'
            Key.F -> 'F'
            Key.G -> 'G'
            Key.H -> 'H'
            Key.I -> 'I'
            Key.J -> 'J'
            Key.K -> 'K'
            Key.L -> 'L'
            Key.M -> 'M'
            Key.N -> 'N'
            Key.O -> 'O'
            Key.P -> 'P'
            Key.Q -> 'Q'
            Key.R -> 'R'
            Key.S -> 'S'
            Key.T -> 'T'
            Key.U -> 'U'
            Key.V -> 'V'
            Key.W -> 'W'
            Key.X -> 'X'
            Key.Y -> 'Y'
            Key.Z -> 'Z'
            Key.Zero -> '0'
            Key.One -> '1'
            Key.Two -> '2'
            Key.Three -> '3'
            Key.Four -> '4'
            Key.Five -> '5'
            Key.Six -> '6'
            Key.Seven -> '7'
            Key.Eight -> '8'
            Key.Nine -> '9'
            else -> null
        }

    protected fun doBackAction() {
        if (backCallback.isEnabled)
            backCallback.handleOnBackPressed()
        else
            this.finish()
    }

    /**
     * Handle events and things that need to be launched because of
     *
     * If overriding, call super
     */
    @Composable
    protected open fun Launchers() {
        val announcement by viewModel.announceEvent.observeAsState()
        val view = LocalView.current
        LaunchedEffect(announcement) {
            announcement?.let { data ->
                announce(data, view)
                viewModel.clearAnnounceEvent()
            }
        }
        val internalTool by viewModel.internalToolEvent.observeAsState()
        LaunchedEffect(internalTool) {
            internalTool?.let { tool ->
                tool.accept(InternalToolLauncher(this@PuzzleActivity))
                viewModel.clearInternalToolEvent()
            }
        }
        val externalTool by viewModel.externalToolEvent.observeAsState()
        LaunchedEffect(externalTool) {
            externalTool?.let { tool ->
                viewModel.clearExternalToolEvent()
                try {
                    tool.accept(ExternalToolLauncher(this@PuzzleActivity))
                } catch (e : AppNotFoundException) {
                    viewModel.handleAppNotFoundException(e)
                }
            }
        }
        val sendToast by viewModel.sendToastEvent.observeAsState()
        LaunchedEffect(sendToast) {
            sendToast?.let { data ->
                Toast.makeText(
                    this@PuzzleActivity,
                    data.text,
                    Toast.LENGTH_LONG,
                ).show()
                viewModel.clearSendToast()
            }
        }
        val backEvent by viewModel.backEvent.observeAsState()
        LaunchedEffect(backEvent) {
            if (backEvent ?: false) {
                doBackAction()
                viewModel.clearBackEvent()
            }
        }
        viewModel.keyboardState.observe(this) { state ->
            backCallback.isEnabled = state.hideOnBack
        }
    }

    @Composable
    @OptIn(ExperimentalMaterial3Api::class)
    protected fun VoiceButtons() {
        val state by viewModel.voiceState.observeAsState()

        val showVoiceButton = state?.showVoiceButton ?: false
        val showAnnounceButton = state?.showAnnounceButton ?: false

        if (showVoiceButton || showAnnounceButton) {
            Row(
                modifier = Modifier.fillMaxWidth()
                    .background(MaterialTheme.colorScheme.surface)
                    .padding(bottom = 5.dp),
                horizontalArrangement = Arrangement.Center,
            ) {
                if (showVoiceButton) {
                    FilledIconButton(
                        modifier = Modifier.padding(horizontal=10.dp)
                            .width(50.dp),
                        onClick = { launchVoiceInput() },
                    ) {
                        Icon(
                            modifier = Modifier.width(50.dp),
                            painter = painterResource(R.drawable.ic_voice),
                            contentDescription
                                = stringResource(R.string.voice_command),
                        )
                    }
                }
                if (showAnnounceButton) {
                    FilledIconButton(
                        modifier = Modifier.padding(horizontal=10.dp)
                            .width(50.dp),
                        onClick = { viewModel.announceClue() },
                    ) {
                        Icon(
                            modifier = Modifier.width(50.dp),
                            painter = painterResource(R.drawable.ic_announce),
                            contentDescription = stringResource(
                                R.string.announce_clue_label,
                            ),
                        )
                    }
                }
            }
        }
    }

    @Composable
    protected fun Keyboard(
        showSpecialKeys : Boolean = false,
        onSpecialKeyDown : (SpecialKey) -> Unit = { },
        onSpecialKeyUp : (SpecialKey) -> Unit = { },
    ) {
        val state by viewModel.keyboardState.observeAsState()

        LaunchedEffect(state?.needsRefresh) {
            if (state?.needsRefresh ?: false) {
                if (state?.visibility == KeyboardVisibility.NATIVE) {
                    inputConnectionMediator.showNative(
                        state?.forceCaps ?: false
                    )
                } else {
                    inputConnectionMediator.hideNative()
                }
                if (state?.visibility == KeyboardVisibility.FORKYZ) {
                    inputConnectionMediator.focusReceiver()
                }
                viewModel.markKeyboardRefreshed()
            }
        }

        if (state?.visibility == KeyboardVisibility.FORKYZ) {
            ForkyzKeyboard(
                modifier = Modifier.fillMaxWidth()
                    .background(MaterialTheme.colorScheme.surface),
                inputConnection
                    = inputConnectionMediator.forkyzKeyboardInputConnection,
                onHideClick = { viewModel.hideKeyboard(true) },
                onSpecialKeyDown = onSpecialKeyDown,
                onSpecialKeyUp = onSpecialKeyUp,
                compact = state?.compact ?: false,
                hapticFeedback = state?.hapticFeedback ?: false,
                showHideButton = state?.showHideButton ?: false,
                showSpecialKeys = showSpecialKeys,
                layout = state?.layout ?: KeyboardLayout.KL_QWERTY,
                repeatDelay = state?.repeatDelay ?: 300,
                repeatInterval = state?.repeatInterval ?: 75,
            )
        }
    }

    /**
     * Always call to display dialogs
     */
    @Composable
    protected fun PuzzleDialogs() {
        val infoDialogViewModel
            by viewModel.infoDialogViewModel.collectAsStateWithLifecycle()
        infoDialogViewModel?.let { infoDialogViewModel ->
            PuzzleInfoDialog(
                viewModel = infoDialogViewModel,
                onClose = viewModel::closePuzzleInfoDialog,
            )
        }

        val finishedDialogViewModel
            by viewModel.finishedDialogViewModel.collectAsStateWithLifecycle()
        finishedDialogViewModel?.let { finishedDialogViewModel ->
            PuzzleFinishedDialog(
                viewModel = finishedDialogViewModel,
                onDone = viewModel::closePuzzleFinishedDialog,
                onShare = viewModel::shareCompletion,
            )
        }

        val state by viewModel.uiState.observeAsState()
        state?.showDialog?.let {
            when (it) {
                PuzzleDialog.REVEAL_ALL -> {
                    OKCancelDialog(
                        title = R.string.reveal_puzzle,
                        summary = R.string.are_you_sure,
                        onCancel = viewModel::clearRevealDialog,
                        onOK = viewModel::confirmRevealPuzzle,
                    )
                }
                else -> { /* show nothing */ }
            }
        }

        val flagClueDialogState by viewModel
            .flagClueDialogState
            .collectAsStateWithLifecycle()
        flagClueDialogState?.let { flagClueDialogState ->
            ChooseFlagColorDialog(
                selectedColor = flagClueDialogState.flagColor,
                onCancel = viewModel::clearFlagClueDialog,
                onChoose = { color ->
                    viewModel.flagClue(
                        flagClueDialogState.clueID,
                        color,
                    )
                },
            )
        }

        val flagCellDialogState by viewModel
            .flagCellDialogState
            .collectAsStateWithLifecycle()
        flagCellDialogState?.let { flagCellDialogState ->
            ChooseFlagColorDialog(
                selectedColor = flagCellDialogState.flagColor,
                onCancel = viewModel::clearFlagCellDialog,
                onChoose = { color ->
                    viewModel.flagCell(
                        flagCellDialogState.position,
                        color,
                    )
                },
            )
        }

        val editClueDialogState by viewModel
            .editClueDialogState
            .collectAsStateWithLifecycle()
        editClueDialogState?.let { editClueDialogState ->
            StringDialog(
                title = R.string.edit_clue,
                hint = R.string.clue_hint,
                value = editClueDialogState.hint,
                onValueChange = { hint ->
                    viewModel.confirmEditClue(
                        editClueDialogState.clueID,
                        hint,
                    )
                },
                onDismissRequest = viewModel::clearEditClueDialog,
            )
        }

        val dialogMessage by viewModel
            .dialogMessage
            .collectAsStateWithLifecycle()
        dialogMessage?.let { dm ->
            OKDialog(
                title = dm.title,
                summary = dm.message,
                onClose = viewModel::clearDialogMessage,
            )
        }

        val appNotFoundDialogState by viewModel
            .appNotFoundDialogState
            .collectAsStateWithLifecycle()
        appNotFoundDialogState?.let { appNotFoundDialogState ->
            TwoButtonDialog(
                title = stringResource(
                    R.string.external_app_not_installed,
                    appNotFoundDialogState.appName,
                ),
                summary = stringResource(
                    R.string.external_app_install_msg,
                    appNotFoundDialogState.appName,
                ),
                negativeText= stringResource(R.string.website),
                onNegative = {
                    viewModel.clearAppNotFoundDialog()
                    val intent = Intent(Intent.ACTION_VIEW)
                    intent.setData(appNotFoundDialogState.appURL.toUri())
                    startActivity(intent)
                },
                positiveText = stringResource(android.R.string.ok),
                onPositive = viewModel::clearAppNotFoundDialog,
            )
        }

        val specialEntryDialogViewModel by viewModel
            .specialEntryDialogViewModel
            .collectAsStateWithLifecycle()
        specialEntryDialogViewModel?.let { specialEntryDialogViewModel ->
            SpecialEntryDialog(
                viewModel = specialEntryDialogViewModel,
                onClose = viewModel::clearSpecialEntry,
            )
        }
    }

    @Composable
    protected fun MenuCheckedItem(
        checked : Boolean,
        text : @Composable () -> Unit,
        onClick : () -> Unit,
    ) {
        DropdownMenuItem(
            text = text,
            onClick = onClick,
            trailingIcon = {
                Checkbox(
                    checked = checked,
                    onCheckedChange = { onClick() },
                )
            }
        )
    }

    @Composable
    protected fun MenuNotes() {
        MenuSubEntry(
            text = { MenuText(stringResource(R.string.menu_notes)) },
            onClick = { viewModel.expandMenu(PuzzleSubMenu.NOTES) },
        )
    }

    @Composable
    protected fun MenuNotesSub() {
        val state by viewModel.menuState.observeAsState()

        DropdownMenu(
            modifier = BaseMenuModifier,
            expanded = state?.expanded == PuzzleSubMenu.NOTES,
            onDismissRequest = viewModel::dismissMenu,
        ) {
            MenuDropdownHeading(
                R.string.menu_notes,
                onClick = viewModel::closeMenu,
            )
            DropdownMenuItem(
                text = { MenuText(stringResource(R.string.menu_notes_clue)) },
                onClick = {
                    viewModel.launchCurrentClueNotes()
                    viewModel.dismissMenu()
                },
            )
            DropdownMenuItem(
                text = {
                    MenuText(stringResource(R.string.menu_notes_puzzle))
                },
                onClick = {
                    viewModel.launchPuzzleNotes()
                    viewModel.dismissMenu()
                },
            )
            DropdownMenuItem(
                text = {
                    MenuText(stringResource(R.string.menu_flag_clue_toggle))
                },
                onClick = {
                    viewModel.toggleClueFlag()
                    viewModel.dismissMenu()
                }
            )
            DropdownMenuItem(
                text = {
                    MenuText(stringResource(R.string.menu_flag_cell_toggle))
                },
                onClick = {
                    viewModel.toggleCellFlag()
                    viewModel.dismissMenu()
                }
            )
        }
    }

    @Composable
    protected fun MenuScratchMode() {
        val state by viewModel.menuState.observeAsState()
        MenuCheckedItem(
            checked = state?.isScratchMode ?: false,
            text = { MenuText(stringResource(R.string.scratch_mode)) },
            onClick = {
                viewModel.toggleScratchMode()
                viewModel.dismissMenu()
            }
        )
    }

    @Composable
    protected fun MenuSpecialEntry() {
        DropdownMenuItem(
            text = { MenuText(stringResource(R.string.special_entry)) },
            onClick = {
                viewModel.doSpecialEntry()
                viewModel.dismissMenu()
            }
        )
    }

    @Composable
    protected fun MenuShowErrors() {
        val state by viewModel.menuState.observeAsState()
        if (!(state?.showErrorsState?.enabled ?: false))
            return

        MenuSubEntry(
            text = {
                if (state?.showErrorsState?.isShowing ?: false)
                    MenuText(stringResource(R.string.showing_errors))
                else
                    MenuText(stringResource(R.string.show_errors))
            },
            onClick = { viewModel.expandMenu(PuzzleSubMenu.SHOW_ERRORS) },
        )
    }

    @Composable
    protected fun MenuShowErrorsSub() {
        val state by viewModel.menuState.observeAsState()
        if (!(state?.showErrorsState?.enabled ?: false))
            return

        DropdownMenu(
            modifier = BaseMenuModifier,
            expanded = state?.expanded == PuzzleSubMenu.SHOW_ERRORS,
            onDismissRequest = viewModel::dismissMenu,
        ) {
            MenuDropdownHeading(
                R.string.show_errors,
                onClick = viewModel::closeMenu,
            )
            MenuCheckedItem(
                checked
                    = state?.showErrorsState?.isShowingErrorsCursor ?: false,
                text = {
                    MenuText(stringResource(R.string.show_errors_cursor))
                },
                onClick = {
                    viewModel.toggleShowErrorsCursor()
                    viewModel.dismissMenu()
                }
            )
            MenuCheckedItem(
                checked = state?.showErrorsState?.isShowingErrorsClue ?: false,
                text = { MenuText(stringResource(R.string.show_errors_clue)) },
                onClick = {
                    viewModel.toggleShowErrorsClue()
                    viewModel.dismissMenu()
                }
            )
            MenuCheckedItem(
                checked = state?.showErrorsState?.isShowingErrorsGrid ?: false,
                text = {
                    MenuText(stringResource(R.string.show_errors_full_grid))
                },
                onClick = {
                    viewModel.toggleShowErrorsGrid()
                    viewModel.dismissMenu()
                }
            )
        }
    }

    @Composable
    protected fun MenuReveal() {
        val state by viewModel.menuState.observeAsState()
        if (!(state?.canReveal ?: false))
            return

        MenuSubEntry(
            text = { MenuText(stringResource(R.string.reveal)) },
            onClick = { viewModel.expandMenu(PuzzleSubMenu.REVEAL) },
        )
    }

    @Composable
    protected fun MenuRevealSub() {
        val state by viewModel.menuState.observeAsState()
        if (!(state?.canReveal ?: false))
            return

        DropdownMenu(
            modifier = BaseMenuModifier,
            expanded = state?.expanded == PuzzleSubMenu.REVEAL,
            onDismissRequest = viewModel::dismissMenu,
        ) {
            MenuDropdownHeading(
                R.string.reveal,
                onClick = viewModel::closeMenu,
            )
            if (state?.canRevealInitialBoxLetter ?: false) {
                DropdownMenuItem(
                    text = {
                        MenuText(stringResource(R.string.initial_letter))
                    },
                    onClick = {
                        viewModel.revealInitialLetter()
                        viewModel.dismissMenu()
                    },
                )
            }
            DropdownMenuItem(
                text = { MenuText(stringResource(R.string.letter)) },
                onClick = {
                    viewModel.revealLetter()
                    viewModel.dismissMenu()
                },
            )
            DropdownMenuItem(
                text = { MenuText(stringResource(R.string.word)) },
                onClick = {
                    viewModel.revealWord()
                    viewModel.dismissMenu()
                },
            )
            if (state?.canRevealInitialLetters ?: false) {
                DropdownMenuItem(
                    text = {
                        MenuText(stringResource(R.string.initial_letters))
                    },
                    onClick = {
                        viewModel.revealInitialLetters()
                        viewModel.dismissMenu()
                    },
                )
            }
            DropdownMenuItem(
                text = { MenuText(stringResource(R.string.errors)) },
                onClick = {
                    viewModel.revealErrors()
                    viewModel.dismissMenu()
                },
            )
            DropdownMenuItem(
                text = { MenuText(stringResource(R.string.puzzle)) },
                onClick = {
                    viewModel.revealPuzzle()
                    viewModel.dismissMenu()
                },
            )
        }
    }

    @Composable
    protected fun MenuZoom() {
        MenuSubEntry(
            text = { MenuText(stringResource(R.string.zoom)) },
            onClick = { viewModel.expandMenu(PuzzleSubMenu.ZOOM) },
        )
    }

    @Composable
    protected fun MenuZoomSub(
        onZoomIn : () -> Unit,
        onZoomInMax : () -> Unit,
        onZoomOut : () -> Unit,
        onZoomFit : () -> Unit,
        onZoomReset : () -> Unit,
    ) {
        val state by viewModel.menuState.observeAsState()

        DropdownMenu(
            modifier = BaseMenuModifier,
            expanded = state?.expanded == PuzzleSubMenu.ZOOM,
            onDismissRequest = viewModel::dismissMenu,
        ) {
            MenuDropdownHeading(
                R.string.zoom,
                onClick = viewModel::closeMenu,
            )
            DropdownMenuItem(
                text = { MenuText(stringResource(R.string.zoom_in)) },
                onClick = {
                    onZoomIn()
                    viewModel.dismissMenu()
                },
            )
            DropdownMenuItem(
                text = { MenuText(stringResource(R.string.zoom_in_max)) },
                onClick = {
                    onZoomInMax()
                    viewModel.dismissMenu()
                },
            )
            DropdownMenuItem(
                text = {
                    MenuText(stringResource(R.string.zoom_out))
                },
                onClick = {
                    onZoomOut()
                    viewModel.dismissMenu()
                },
            )
            DropdownMenuItem(
                text = { MenuText(stringResource(R.string.fit_to_screen)) },
                onClick = {
                    onZoomFit()
                    viewModel.dismissMenu()
                },
            )
            DropdownMenuItem(
                text = { MenuText(stringResource(R.string.zoom_reset)) },
                onClick = {
                    onZoomReset()
                    viewModel.dismissMenu()
                },
            )
        }
    }

    @Composable
    protected fun MenuExternalTools() {
        val state by viewModel.menuState.observeAsState()
        if (!(state?.externalToolsState?.hasExternal ?: false))
            return

        MenuSubEntry(
            text = { MenuText(stringResource(R.string.external_tools)) },
            onClick = { viewModel.expandMenu(PuzzleSubMenu.EXTERNAL_TOOLS) },
        )
    }

    /**
     * External tools sub menu
     *
     * @param onCallExternalDictionary call back for when external dictionary
     * called
     * @param onCallCrosswordSolver call back when crossword solver
     * called
     * @param showAnagramSolver whether to show the anagram solver if enabled
     * in settings.
     * @param onCallAnagramSolver call back when anagram solver called (does
     * nothing if null)
     */
    @Composable
    protected fun MenuExternalToolsSub(
        onCallExternalDictionary : () -> Unit
            = viewModel::callExternalDictionary,
        onCallCrosswordSolver : () -> Unit = viewModel::callCrosswordSolver,
        showAnagramSolver : Boolean = false,
        onCallAnagramSolver : () -> Unit = { },
    ) {
        val state by viewModel.menuState.observeAsState()
        if (!(state?.externalToolsState?.hasExternal ?: false))
            return

        DropdownMenu(
            modifier = BaseMenuModifier,
            expanded = state?.expanded == PuzzleSubMenu.EXTERNAL_TOOLS,
            onDismissRequest = viewModel::dismissMenu,
        ) {
            MenuDropdownHeading(
                R.string.external_tools,
                onClick = viewModel::closeMenu,
            )
            if (state?.externalToolsState?.hasExternalDictionary ?: false) {
                DropdownMenuItem(
                    text = {
                        MenuText(
                            stringResource(R.string.help_external_dictionary)
                        )
                    },
                    onClick = {
                        viewModel.dismissMenu()
                        onCallExternalDictionary()
                    },
                )
            }
            if (state?.externalToolsState?.hasCrosswordSolver ?: false) {
                DropdownMenuItem(
                    text = {
                        MenuText(stringResource(R.string.help_crossword_solver))
                    },
                    onClick = {
                        viewModel.dismissMenu()
                        onCallCrosswordSolver()
                    },
                )
                if (showAnagramSolver) {
                    DropdownMenuItem(
                        text = {
                            MenuText(
                                stringResource(
                                    R.string.help_crossword_solver_anagram
                                )
                            )
                        },
                        onClick = {
                            viewModel.dismissMenu()
                            onCallAnagramSolver()
                        },
                    )

                }
            }
            if (state?.externalToolsState?.hasChatGPT ?: false) {
                DropdownMenuItem(
                    text = {
                        MenuText(stringResource(R.string.help_query_chat_gpt))
                    },
                    onClick = {
                        viewModel.requestHelpForCurrentClue()
                        viewModel.dismissMenu()
                    },
                )
            }
            if (state?.externalToolsState?.hasDuckDuckGo ?: false) {
                DropdownMenuItem(
                    text = {
                        MenuText(stringResource(R.string.help_duckduckgo))
                    },
                    onClick = {
                        viewModel.searchDuckDuckGo()
                        viewModel.dismissMenu()
                    },
                )
            }
            if (state?.externalToolsState?.hasFifteenSquared ?: false) {
                DropdownMenuItem(
                    text = {
                        MenuText(stringResource(R.string.help_fifteen_squared))
                    },
                    onClick = {
                        viewModel.searchFifteenSquared()
                        viewModel.dismissMenu()
                    },
                )
            }
        }
    }

    @Composable
    protected fun MenuShare() {
        MenuSubEntry(
            text = { MenuText(stringResource(R.string.share)) },
            onClick = { viewModel.expandMenu(PuzzleSubMenu.SHARE) },
        )
    }

    @Composable
    protected fun MenuShareSub() {
        val state by viewModel.menuState.observeAsState()

        DropdownMenu(
            modifier = BaseMenuModifier,
            expanded = state?.expanded == PuzzleSubMenu.SHARE,
            onDismissRequest = viewModel::dismissMenu,
        ) {
            MenuDropdownHeading(
                R.string.share,
                onClick = viewModel::closeMenu,
            )
            DropdownMenuItem(
                text = { MenuText(stringResource(R.string.share_clue)) },
                onClick = {
                    viewModel.shareClue(false)
                    viewModel.dismissMenu()
                },
            )
            DropdownMenuItem(
                text = {
                    MenuText(stringResource(R.string.share_clue_response))
                },
                onClick = {
                    viewModel.shareClue(true)
                    viewModel.dismissMenu()
                },
            )
            DropdownMenuItem(
                text = {
                    MenuText(stringResource(R.string.share_puzzle_full))
                },
                onClick = {
                    viewModel.sharePuzzle(false, false)
                    viewModel.dismissMenu()
                },
            )
            DropdownMenuItem(
                text = {
                    MenuText(
                        stringResource(R.string.share_puzzle_without_extensions)
                    )
                },
                onClick = {
                    viewModel.sharePuzzle(false, true)
                    viewModel.dismissMenu()
                },
            )
            DropdownMenuItem(
                text = { MenuText(stringResource(R.string.share_puzzle_orig)) },
                onClick = {
                    viewModel.sharePuzzle(true, true)
                    viewModel.dismissMenu()
                },
            )
            DropdownMenuItem(
                text = {
                    MenuText(
                        stringResource(R.string.share_puzzle_open_share_url)
                    )
                },
                onClick = {
                    openShareUrl()
                    viewModel.dismissMenu()
                },
            )
        }
    }

    /**
     * Edit clue menu entry
     *
     * @param onEditClue override for custom on edit behavior
     */
    @Composable
    protected fun MenuEditClue(
        onEditClue : () -> Unit = viewModel::editClue,
    ) {
        DropdownMenuItem(
            text = { MenuText(stringResource(R.string.edit_clue)) },
            onClick = {
                viewModel.dismissMenu()
                onEditClue()
            }
        )
    }

    @Composable
    protected fun MenuHelp() {
        MenuHelp(
            activity = this,
            helpHTMLAsset = "play.html",
            onDismiss = viewModel::dismissMenu,
        )
    }

    @Composable
    protected fun MenuSettings() {
        MenuSettings(this, viewModel::dismissMenu)
    }

    @Composable
    protected fun MenuPuzzleInfo() {
        DropdownMenuItem(
            text = { MenuText(stringResource(R.string.info)) },
            onClick = {
                viewModel.showInfoDialog()
                viewModel.dismissMenu()
            }
        )
    }

    @Composable
    protected fun MenuSupportPuzzleSource() {
        DropdownMenuItem(
            text = { MenuText(stringResource(R.string.support_puzzle_source)) },
            onClick = {
                actionSupportSource()
                viewModel.dismissMenu()
            }
        )
    }

    private fun actionSupportSource() {
        viewModel.supportURL?.let { url ->
            val i = Intent(Intent.ACTION_VIEW)
            i.setData(url.toUri())
            startActivity(i)
        }
    }

    private fun openShareUrl() {
        viewModel.shareURL?.let { url ->
            if (!url.isEmpty()) {
                val i = Intent(Intent.ACTION_VIEW)
                i.setData(url.toUri())
                startActivity(i)
            }
        }
    }

    /**
     * Announce text with accessibility if running or tts
     *
     * @param data what to announce
     * @param view the view to announce from (announceForAccessibility)
     * if accessibility service is running
     */
    private fun announce(data : AnnounceData, view : View) {
        val text = data.message
        if (isAccessibilityServiceRunning) {
            view.announceForAccessibility(text)
        } else {
            if (ttsService == null) {
                ttsService = TextToSpeech(
                    applicationContext,
                    { status ->
                        if (status == TextToSpeech.SUCCESS) {
                            ttsReady = true
                            announce(data, view)
                        }
                    }
                )
            } else if (!ttsReady) {
                // hopefully rare occasion where tts being prepared but not
                // ready yet
                Toast.makeText(
                    this,
                    R.string.speech_not_ready,
                    Toast.LENGTH_SHORT,
                ).show()
            } else {
                utils.speak(ttsService, text)
            }
        }
    }

    private fun launchVoiceInput() {
        try {
            voiceInputLauncher.launch(
                RecognizerIntent.LANGUAGE_MODEL_FREE_FORM
            )
        } catch (e : ActivityNotFoundException) {
            Toast.makeText(
                this,
                R.string.no_speech_recognition_available,
                Toast.LENGTH_LONG
            ).show()
        }
    }
}
