package com.pixplicity.cryptogram.activities

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.app.ProgressDialog
import android.content.ActivityNotFoundException
import android.content.Intent
import android.graphics.PointF
import android.graphics.Rect
import android.graphics.drawable.AnimatedVectorDrawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.text.InputType
import android.util.Log
import android.util.SparseBooleanArray
import android.view.*
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.GravityCompat
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import com.afollestad.materialdialogs.DialogAction
import com.afollestad.materialdialogs.MaterialDialog
import com.getkeepsafe.taptargetview.TapTarget
import com.getkeepsafe.taptargetview.TapTargetView
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInClient
import com.google.android.gms.common.images.ImageManager
import com.google.android.gms.games.Games
import com.google.android.gms.games.GamesActivityResultCodes
import com.google.android.gms.games.snapshot.SnapshotMetadata
import com.google.android.gms.games.snapshot.Snapshots
import com.google.android.material.button.MaterialButton
import com.google.firebase.Timestamp
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.PlayGamesAuthProvider
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.firebase.firestore.SetOptions
import com.google.firebase.firestore.ktx.firestore
import com.google.firebase.ktx.Firebase
import com.pixplicity.cryptogram.BuildConfig
import com.pixplicity.cryptogram.CryptogramApp
import com.pixplicity.cryptogram.R
import com.pixplicity.cryptogram.adapters.PuzzleAdapter
import com.pixplicity.cryptogram.db.PuzzleStateDao
import com.pixplicity.cryptogram.events.PuzzleEvent
import com.pixplicity.cryptogram.events.PuzzlesEvent
import com.pixplicity.cryptogram.events.RemoteConfigEvent
import com.pixplicity.cryptogram.models.Puzzle
import com.pixplicity.cryptogram.models.PuzzleState
import com.pixplicity.cryptogram.utils.*
import com.pixplicity.cryptogram.utils.BillingUtils.DONATIONS_ENABLED
import com.pixplicity.cryptogram.views.CryptogramView
import com.pixplicity.easyprefs.library.Prefs
import com.skydoves.balloon.ArrowOrientation
import com.skydoves.balloon.BalloonAnimation
import com.skydoves.balloon.createBalloon
import com.squareup.otto.Subscribe
import kotlinx.android.synthetic.main.activity_cryptogram.*
import kotlinx.android.synthetic.main.in_drawer.*
import kotlinx.android.synthetic.main.in_statistics.*
import net.soulwolf.widget.ratiolayout.widget.RatioFrameLayout
import java.util.*

class CryptogramActivity : BaseActivity() {

    companion object {
        private val TAG = CryptogramActivity::class.java.simpleName

        private const val RC_UNUSED = 1000
        private const val RC_PLAY_GAMES = 1001
        private const val RC_SAVED_GAMES = 1002

        private const val ONBOARDING_PAGES = 2

        const val EXTRA_LAUNCH_SETTINGS = "launch_settings"
    }

    private var keyboardView: View? = null

    private lateinit var puzzleAdapter: PuzzleAdapter

    private lateinit var googleSignInClient: GoogleSignInClient
    private lateinit var auth: FirebaseAuth
    private val db = Firebase.firestore

    private var lastConnectionError: Int = 0

    private var freshInstall: Boolean = false

    private var activityVisible = false
        set(value) {
            field = value
            // Also persist it
            CryptogramApp.activityVisible = value
        }

    private lateinit var puzzleState: PuzzleState

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_cryptogram)

        // Create the GoogleSignIn client with access to Games
        googleSignInClient = GoogleSignIn.getClient(this, SignInUtil.getOptions(this, googleSignIn = true))
        googleSignInClient.silentSignIn().addOnCompleteListener { task ->
            updateGooglePlayGames()
            if (task.isSuccessful) {
                task.result?.serverAuthCode?.let { authCode ->
                    val credential = PlayGamesAuthProvider.getCredential(authCode)
                    auth.currentUser?.linkWithCredential(credential)?.addOnCompleteListener { task ->
                        if (task.isSuccessful) {
                            val user = task.result?.user
                            Log.d(TAG, "linkWithCredential: success; user=${user?.uid}")
                        } else {
                            Log.w(TAG, "linkWithCredential: failure", task.exception)
                            // Instead just log in as that user
                            auth.signInWithCredential(credential)
                                    .addOnCompleteListener { task ->
                                        if (task.isSuccessful) {
                                            Log.d(TAG, "signInWithCredential: success; user UID: ${auth.currentUser?.uid}")
                                        } else {
                                            // TODO Handle sign in failure more gracefully
                                            Log.w(TAG, "signInWithCredential: failure", task.exception)
                                        }
                                    }
                        }
                    }
                }
            }
        }

        // Initialize Firebase Auth
        auth = FirebaseAuth.getInstance()
        if (auth.currentUser == null) {
            auth.signInAnonymously()
                    .addOnCompleteListener(this) { task ->
                        if (task.isSuccessful) {
                            // Sign in success, hold on to signed-in user's information
                            Log.d(TAG, "signInAnonymously: success; user UID: ${auth.currentUser?.uid}")
                        } else {
                            // TODO Handle sign in failure more gracefully
                            Log.w(TAG, "signInAnonymously: failure", task.exception)
                        }
                    }
        } else {
            Log.d(TAG, "user UID: ${auth.currentUser?.uid}")
        }

        RatingsBooster.init(this)

        refreshPuzzles()

        vg_cryptogram.setCryptogramView(cryptogram)
        cryptogram.setOnHighlightListener(object : CryptogramView.OnHighlightListener {
            private val mHighlightShown = SparseBooleanArray()

            override fun onHighlight(type: Int, point: PointF) {
                if (mHighlightShown.get(type, false)) {
                    return
                }
                if (PrefsUtils.getHighlighted(type)) {
                    return
                }
                mHighlightShown.put(type, true)
                when (type) {
                    PrefsUtils.TYPE_HIGHLIGHT_HYPHENATION -> showHighlight(type, createTapTargetFromPoint(
                            point,
                            getString(R.string.highlight_hyphenation_title),
                            getString(R.string.highlight_hyphenation_description)),
                            1200
                    )
                }
            }
        })

        if (PrefsUtils.useSystemKeyboard) {
            keyboardView?.visibility = View.GONE
        } else {
            if (keyboardView == null) {
                vs_keyboard.layoutResource = when (PrefsUtils.keyboardLayout) {
                    KeyboardLayouts.LAYOUT_QWERTY -> R.layout.in_keyboard_qwerty
                    KeyboardLayouts.LAYOUT_QWERTZ -> R.layout.in_keyboard_qwertz
                    KeyboardLayouts.LAYOUT_AZERTY -> R.layout.in_keyboard_azerty
                    KeyboardLayouts.LAYOUT_ABC -> R.layout.in_keyboard_abc
                }
                keyboardView = vs_keyboard.inflate()
            }
            keyboardView?.visibility = View.VISIBLE
            cryptogram.setKeyboardView(keyboardView)
        }

        val intent = intent
        if (intent != null) {
            if (intent.getBooleanExtra(EXTRA_LAUNCH_SETTINGS, false)) {
                startActivity(SettingsActivity.create(this))
            }
        }

        if (UpdateManager.consumeEnabledShowUsedLetters()) {
            showHighlight(-1, TapTarget.forToolbarOverflow(
                    viewToolbar,
                    getString(R.string.highlight_used_letters_title),
                    getString(R.string.highlight_used_letters_description)),
                    1200
            )
        }

        vg_google_play_games.setOnClickListener {
            onClickGooglePlayGames()
        }

        bt_next.setOnClickListener {
            nextPuzzle()
        }
        bt_flag.setOnClickListener {
            FlaggingUtil.showDialog(this, PrefsUtils.theme.isDark, db, PuzzleProvider.getInstance(this).current, auth.currentUser)
        }
        bt_star.setOnClickListener {
            PuzzleProvider.getInstance(this).current?.apply {
                // Toggle puzzle state
                puzzleState.starred = !puzzleState.starred
                // Update record
                PuzzleStateDao.set(puzzleState)
            }
        }
        bt_covid.setOnClickListener {
            AlertDialog.Builder(this)
                    .setMessage(R.string.covid_19_prevention)
                    .setPositiveButton(R.string.covid_19_learn_more) { _, _ ->
                        try {
                            startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://g.co/kgs/i6Pdqv")).apply {
                                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                            })
                        } catch (e: Exception) {
                        }
                    }
                    .show()
        }

        RemoteConfig.refreshRemoteConfig()
    }

    override fun onStart() {
        super.onStart()
        activityVisible = true

        EventProvider.bus.register(this)

        val puzzleProvider = PuzzleProvider.getInstance(this)
        val puzzle = puzzleProvider.current
        puzzle?.onResume()
        showHintView(puzzle)

        if (hasOnBoardingPages()) {
            showOnboarding(0)
        } else {
            onGameplayReady()
            if (!PrefsUtils.notificationsPuzzlesSet) {
                // Ask to enable notifications
                AlertDialog.Builder(this)
                        .setTitle(R.string.enable_notifications_title)
                        .setMessage(R.string.enable_notifications_message)
                        .setPositiveButton(R.string.enable_notifications_yes) { _, _ ->
                            PrefsUtils.notificationsPuzzles = true
                        }
                        .setNegativeButton(R.string.enable_notifications_no) { _, _ ->
                            PrefsUtils.notificationsPuzzles = false
                        }
                        .show()
            }
        }

        updateGooglePlayGames()
    }

    override fun onResume() {
        super.onResume()

        val puzzleProvider = PuzzleProvider.getInstance(this)
        puzzleProvider.current?.let {
            onPuzzleChanged(it, true, layoutChange = true)
        }
    }

    override fun onStop() {
        super.onStop()
        activityVisible = false

        val puzzleProvider = PuzzleProvider.getInstance(this)
        val puzzle = puzzleProvider.current
        puzzle?.onPause()

        EventProvider.bus.unregister(this)
    }

    override fun onBackPressed() {
        if (viewDrawerLayout != null && viewDrawerLayout!!.isDrawerOpen(GravityCompat.START)) {
            viewDrawerLayout!!.closeDrawer(GravityCompat.START)
            return
        }
        if (keyboardView != null && keyboardView!!.isShown) {
            cryptogram.hideSoftInput()
            return
        }
        super.onBackPressed()
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
        super.onActivityResult(requestCode, resultCode, intent)
        when (requestCode) {
            RC_PLAY_GAMES -> {
                when (resultCode) {
                    RESULT_OK -> {
                        // Logged in
                        Log.d(TAG, "user UID: ${auth.currentUser?.uid}")
                        // Analytics
                        CryptogramApp.instance.firebaseAnalytics.logEvent(FirebaseAnalytics.Event.LOGIN, null)
                    }
                    GamesActivityResultCodes.RESULT_RECONNECT_REQUIRED -> {
                        // Logged out
                        googleSignInClient.signOut()
                    }
                    RESULT_CANCELED -> {
                        // Canceled; do nothing
                    }
                    else -> {
                        // Assume some error
                        showGmsError(resultCode)
                    }
                }
                updateGooglePlayGames()
                loadGame(intent)
            }
            RC_SAVED_GAMES -> {
                loadGame(intent)
                if (viewDrawerLayout != null) {
                    viewDrawerLayout!!.closeDrawers()
                }
            }
        }
    }

    private fun loadGame(intent: Intent?) {
        val account = GoogleSignIn.getLastSignedInAccount(this)
        if (intent != null && account != null) {
            val snapshotsClient = Games.getSnapshotsClient(this, account)
            if (intent.hasExtra(Snapshots.EXTRA_SNAPSHOT_METADATA)) {
                // TODO replace with a better UI
                // Load a snapshot
                val pd = ProgressDialog(this)
                pd.setMessage(getString(R.string.saved_game_loading))
                pd.setCancelable(false)
                pd.show()
                intent.getParcelableExtra<SnapshotMetadata>(Snapshots.EXTRA_SNAPSHOT_METADATA)?.let { snapshotMetadata ->
                    PuzzleProvider.getInstance(this).load(this, snapshotsClient, snapshotMetadata,
                            object : SavegameManager.OnLoadResult {
                                override fun onLoadSuccess() {
                                    PuzzleProvider.getInstance(this@CryptogramActivity).current?.let {
                                        onPuzzleChanged(it, false)
                                        showSnackbar(getString(R.string.saved_game_loaded))
                                    }
                                    pd.dismiss()
                                }

                                override fun onLoadFailure(reason: String) {
                                    showSnackbar(getString(R.string.saved_game_load_failed, reason))
                                    pd.dismiss()
                                }
                            })
                }
            } else if (intent.hasExtra(Snapshots.EXTRA_SNAPSHOT_NEW)) {
                PuzzleProvider.getInstance(this).save(this, snapshotsClient,
                        object : SavegameManager.OnSaveResult {
                            override fun onSaveSuccess() {
                                showSnackbar(getString(R.string.saved_game_saved))
                            }

                            override fun onSaveFailure(reason: String) {
                                showSnackbar(getString(R.string.saved_game_save_failed, reason))
                            }
                        })
            }
        }
    }

    private fun hasOnBoardingPages(): Boolean {
        return PrefsUtils.onboarding < ONBOARDING_PAGES - 1
    }

    private fun createTapTargetFromPoint(point: PointF, title: String,
                                         description: String): TapTarget {
        val viewRect = Rect()
        cryptogram.getGlobalVisibleRect(viewRect)
        val targetX = (point.x + viewRect.left).toInt()
        val targetY = (point.y + viewRect.top).toInt()
        val targetRadius = 48
        return TapTarget.forBounds(Rect(targetX - targetRadius, targetY - targetRadius, targetX + targetRadius, targetY + targetRadius),
                title, description)
    }

    private fun showHighlight(type: Int, tapTarget: TapTarget, delayMillis: Int) {
        Handler().postDelayed({
            val showTime = System.currentTimeMillis()
            TapTargetView.showFor(
                    this@CryptogramActivity,
                    tapTarget
                            .titleTextColor(R.color.white)
                            .descriptionTextColor(R.color.white)
                            .outerCircleColor(R.color.highlight_color)
                            .cancelable(true)
                            .tintTarget(false)
                            .transparentTarget(true),
                    object : TapTargetView.Listener() {
                        override fun onTargetClick(view: TapTargetView) {
                            dismiss(view)
                        }

                        override fun onOuterCircleClick(view: TapTargetView?) {
                            dismiss(view)
                        }

                        override fun onTargetCancel(view: TapTargetView) {
                            dismiss(view)
                        }

                        private fun dismiss(view: TapTargetView?) {
                            if (type >= 0 && System.currentTimeMillis() - showTime >= 1500) {
                                // Ensure that the user saw the message
                                PrefsUtils.setHighlighted(type, true)
                            }
                            view!!.dismiss(false)
                        }
                    })
        }, delayMillis.toLong())
    }

    private fun showOnboarding(page: Int) {
        if (!activityVisible) {
            // Do nothing if activity was paused
            return
        }
        val titleStringResId: Int
        val textStringResId: Int
        var actionStringResId = R.string.intro_next
        val video: VideoUtils.Video
        when (page) {
            0 -> {
                titleStringResId = R.string.intro1_title
                textStringResId = R.string.intro1_text
                video = VideoUtils.VIDEO_INSTRUCTION
            }
            1 -> {
                titleStringResId = R.string.intro2_title
                textStringResId = R.string.intro2_text
                actionStringResId = R.string.intro_done
                video = VideoUtils.VIDEO_HELP
            }
            ONBOARDING_PAGES -> {
                onGameplayReady()
                return
            }
            else -> {
                onGameplayReady()
                return
            }
        }

        val onboarding = PrefsUtils.onboarding
        if (onboarding == -1) {
            freshInstall = true
        }
        if (onboarding >= page) {
            showOnboarding(page + 1)
            return
        }

        val customView = LayoutInflater.from(this).inflate(R.layout.dialog_intro, null)

        val tvIntro = customView.findViewById<TextView>(R.id.tv_intro)
        tvIntro.setText(textStringResId)

        val vgRatio = customView.findViewById<RatioFrameLayout>(R.id.vg_ratio)
        val player = VideoUtils.setup(this, vgRatio, video)

        MaterialDialog.Builder(this)
                .title(titleStringResId)
                .customView(customView, false)
                .cancelable(false)
                .positiveText(actionStringResId)
                .showListener { _ ->
                    player?.start()
                }
                .onAny { _, _ ->
                    PrefsUtils.onboarding = page
                    showOnboarding(page + 1)
                }
                .show()
    }

    private fun onClickGooglePlayGames() {
        val account = GoogleSignIn.getLastSignedInAccount(this)
        if (account != null) {
            // Connected; show gameplay options
            val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_google_play_games, null)
            val dialog = AlertDialog.Builder(this)
                    .setView(dialogView)
                    .show()

            val btLeaderboards = dialogView.findViewById<Button>(R.id.bt_leaderboards)
            btLeaderboards.setOnClickListener { _ ->
                dialog.dismiss()
                // Analytics
                CryptogramApp.instance.firebaseAnalytics.logEvent(CryptogramApp.CONTENT_LEADERBOARDS, null)
                Games.getLeaderboardsClient(this, account).getLeaderboardIntent(getString(R.string.leaderboard_scoreboard)).addOnSuccessListener {
                    try {
                        startActivityForResult(
                                it,
                                RC_UNUSED)
                    } catch (e: SecurityException) {
                        // Not sure why we're still seeing errors about the connection state, but here we are
                        FirebaseCrashlytics.getInstance().recordException(e)
                    } catch (e: ActivityNotFoundException) {
                        Toast.makeText(this@CryptogramActivity, R.string.google_play_games_not_installed, Toast.LENGTH_LONG).show()
                    }
                }
            }

            val btAchievements = dialogView.findViewById<Button>(R.id.bt_achievements)
            btAchievements.setOnClickListener { _ ->
                dialog.dismiss()
                // Analytics
                CryptogramApp.instance.firebaseAnalytics.logEvent(CryptogramApp.CONTENT_ACHIEVEMENTS, null)
                Games.getAchievementsClient(this, account).achievementsIntent.addOnSuccessListener {
                    try {
                        startActivityForResult(it, RC_UNUSED)
                    } catch (e: SecurityException) {
                        // Not sure why we're still seeing errors about the connection state, but here we are
                        FirebaseCrashlytics.getInstance().recordException(e)
                    } catch (e: ActivityNotFoundException) {
                        Toast.makeText(this@CryptogramActivity, R.string.google_play_games_not_installed, Toast.LENGTH_LONG).show()
                    }
                }
            }

            val btRestoreSavedGames = dialogView.findViewById<Button>(R.id.bt_restore_saved_games)
            btRestoreSavedGames.setOnClickListener { _ ->
                dialog.dismiss()
                val maxNumberOfSavedGamesToShow = 5
                val task = Games.getSnapshotsClient(this, account).getSelectSnapshotIntent(
                        getString(R.string.google_play_games_saved_games_window_title), true, true, maxNumberOfSavedGamesToShow)
                task.addOnFailureListener { e ->
                    FirebaseCrashlytics.getInstance().recordException(e)
                }
                task.addOnSuccessListener {
                    try {
                        startActivityForResult(it, RC_SAVED_GAMES)
                    } catch (e: SecurityException) {
                        // Not sure why we're still seeing errors about the connection state, but here we are
                        FirebaseCrashlytics.getInstance().recordException(e)
                    } catch (e: ActivityNotFoundException) {
                        Toast.makeText(this@CryptogramActivity, R.string.google_play_games_not_installed, Toast.LENGTH_LONG).show()
                    }
                }
            }

            val btSignOut = dialogView.findViewById<Button>(R.id.bt_sign_out)
            btSignOut.setOnClickListener { _ ->
                dialog.dismiss()
                googleSignInClient.signOut()
                updateGooglePlayGames()
            }
        } else {
            // Start the sign-in flow
            startActivityForResult(googleSignInClient.signInIntent, RC_PLAY_GAMES)
        }
    }

    private fun refreshPuzzles() {
        rv_drawer.layoutManager = LinearLayoutManager(this)
        puzzleAdapter = PuzzleAdapter(this, object : PuzzleAdapter.OnItemClickListener {
            override fun onItemClick(position: Int) {
                if (viewDrawerLayout != null) {
                    viewDrawerLayout!!.closeDrawers()
                }
                PuzzleProvider.getInstance(this@CryptogramActivity)[position]?.let {
                    onPuzzleChanged(it, false)
                }
            }
        })
        rv_drawer.adapter = puzzleAdapter
    }

    private fun updateCryptogram(puzzle: Puzzle?, layoutChange: Boolean = false) {
        if (puzzle != null) {
            // Pause the previous puzzle
            cryptogram.puzzle?.onPause()

            // Resume the current puzzle
            puzzle.onResume()

            val provider = PuzzleProvider.getInstance(this)
            provider.setCurrentId(puzzle.id)

            // Update UI
            rv_drawer.scrollToPosition(provider.currentIndex)
            tv_error.visibility = View.GONE
            vg_cryptogram.visibility = View.VISIBLE
            // Apply the puzzle to the CryptogramView
            cryptogram.puzzle = puzzle
            // Show other puzzle details
            val author = puzzle.author
            if (!PrefsUtils.showAuthor && !puzzle.isCompleted || author == null) {
                tv_author.visibility = View.GONE
            } else {
                tv_author.visibility = View.VISIBLE
                tv_author.text = getString(R.string.quote, author)
            }
            val topic = puzzle.topic
            if (!PrefsUtils.showTopic && !puzzle.isCompleted || topic == null) {
                tv_topic.visibility = View.GONE
            } else {
                tv_topic.visibility = View.VISIBLE
                tv_topic.text = getString(R.string.topic, topic)
            }
            if (puzzle.isInstruction || puzzle.isNoScore) {
                setToolbarSubtitle(puzzle.getTitle(this))
            } else {
                setToolbarSubtitle(getString(
                        R.string.puzzle_number,
                        puzzle.number))
            }

            // Invoke various events
            showPuzzleState(puzzle, layoutChange = layoutChange)
        } else {
            tv_error.visibility = View.VISIBLE
            vg_cryptogram.visibility = View.GONE
            setToolbarSubtitle(null)
        }
    }

    private fun onGameplayReady() {
        cryptogram.requestFocus()
        if (UpdateManager.consumeScoreExcludesExcessInputs()) {
            MaterialDialog.Builder(this)
                    .title(R.string.scoring_changed_title)
                    .content(R.string.scoring_changed_message)
                    .cancelable(false)
                    .positiveText(R.string.ok)
                    .show()
        }
    }

    private fun showPuzzleState(puzzle: Puzzle, layoutChange: Boolean = false) {
        // Update the HintView as the puzzle updates
        puzzleAdapter.notifyDataSetChanged()
        if (puzzle.isCompleted) {
            if (PrefsUtils.randomize) {
                bt_next.setText(R.string.continue_to_puzzle_random)
            } else {
                bt_next.setText(R.string.continue_to_puzzle_sequential)
            }
            vg_stats.apply {
                // Show the stats
                visibility = View.VISIBLE
                animate()
                        .translationY(0f)
                        .alpha(1f)
                        .setListener(null)
                iv_check.visibility = View.VISIBLE
            }

            // C-19 information message
            bt_covid.visibility = if (puzzle.number == Puzzle.PUZZLE_UNIQUE_COVID) View.VISIBLE else View.GONE

            // Orient buttons to make space for stats if needed
            if (layoutChange) {
                ConstraintSet().apply {
                    clone(vg_stats)
                    if (PrefsUtils.showScore) {
                        connect(R.id.bt_star, ConstraintSet.TOP, R.id.bt_flag, ConstraintSet.BOTTOM, 8.px)
                        connect(R.id.bt_star, ConstraintSet.END, R.id.vg_stats, ConstraintSet.END, 0)
                    } else {
                        connect(R.id.bt_star, ConstraintSet.TOP, R.id.vg_stats, ConstraintSet.TOP, 0)
                        connect(R.id.bt_star, ConstraintSet.END, R.id.bt_flag, ConstraintSet.START, 8.px)
                    }
                    applyTo(vg_stats)
                }
            }

            vg_solved.visibility =
                    if (PrefsUtils.showScore || PrefsUtils.showSolveTime || puzzle.isNoScore) View.GONE else View.VISIBLE

            val durationMs = puzzle.durationMs
            if (!PrefsUtils.showSolveTime || durationMs <= 0) {
                vg_stats_time.visibility = View.GONE
            } else {
                vg_stats_time.visibility = View.VISIBLE
                tv_stats_time.text = StringUtils.getDurationString(durationMs)
            }
            var reveals = -1
            var score: Float? = null
            if (PrefsUtils.showScore) {
                reveals = puzzle.reveals
                score = puzzle.score
            }
            if (reveals < 0) {
                vg_stats_reveals.visibility = View.GONE
            } else {
                vg_stats_reveals.visibility = View.VISIBLE
                tv_stats_reveals.text = getString(R.string.stats_reveals_value, reveals, resources.getQuantityString(R.plurals.stats_reveals_chars, reveals))
            }
            if (score != null) {
                vg_stats_practice.visibility = View.GONE
                vg_stats_score.visibility = View.VISIBLE
                tv_stats_score.text = String.format(
                        Locale.ENGLISH,
                        "%.1f%%",
                        score * 100)
            } else {
                vg_stats_score.visibility = View.GONE
                vg_stats_practice.visibility = if (puzzle.isNoScore) View.VISIBLE else View.GONE
            }
            if (CryptogramApp.instance.remoteConfig.getBoolean(RemoteConfig.ALLOW_USER_FLAGGING) && !puzzle.isNoScore) {
                bt_flag.visibility = View.VISIBLE
            } else {
                bt_flag.visibility = View.GONE
            }
            bt_star.visibility = if (puzzle.isNoScore) View.GONE else View.VISIBLE
        } else {
            bt_covid.visibility = View.GONE
            if (vg_stats.isShown) {
                vg_stats.apply {
                    animate()
                            .translationY(height.toFloat())
                            .alpha(0f)
                            .setListener(object : AnimatorListenerAdapter() {
                                override fun onAnimationEnd(animation: Animator) {
                                    visibility = View.GONE
                                }
                            })
                }
            }
        }
        showHintView(puzzle)

        puzzle.apply {
            bt_star.isEnabled = false
            (PuzzleStateDao.get(id.toLong())).observe(this@CryptogramActivity, Observer {
                puzzleState = it.firstOrNull() ?: PuzzleState(id.toLong(), false)
                puzzleState.apply {
                    (bt_star as MaterialButton).setIconResource(if (starred) R.drawable.ic_star else R.drawable.ic_star_border)
                }
                bt_star.isEnabled = true
            })
        }
    }

    private fun showHintView(puzzle: Puzzle?) {
        hint.visibility = if (puzzle != null && !puzzle.isCompleted
                && PrefsUtils.showUsedChars && PrefsUtils.useSystemKeyboard)
            View.VISIBLE
        else
            View.GONE
    }

    fun onPuzzleChanged(puzzle: Puzzle, delayEvent: Boolean, layoutChange: Boolean = false) {
        updateCryptogram(puzzle, layoutChange = layoutChange)
        if (delayEvent) {
            EventProvider.postEventDelayed(PuzzleEvent.PuzzleProgressEvent(puzzle), 200)
        } else {
            EventProvider.postEvent(PuzzleEvent.PuzzleProgressEvent(puzzle))
        }
        cryptogram.onPuzzleResume()
    }

    @Subscribe
    fun onPuzzleProgress(event: PuzzleEvent.PuzzleProgressEvent) {
        event.puzzle?.let {
            showPuzzleState(it)
        }
    }

    @Subscribe
    fun onPuzzleStarted(event: PuzzleEvent.PuzzleStartedEvent) {
        // Submit any achievements
        AchievementProvider.onCryptogramStart()
        cryptogram.onPuzzleResume()
    }

    @Subscribe
    fun onPuzzleReset(event: PuzzleEvent.PuzzleResetEvent) {
        updateCryptogram(event.puzzle)
    }

    @Subscribe
    fun onPuzzleCompleted(event: PuzzleEvent.PuzzleCompletedEvent) {
        updateCryptogram(event.puzzle)
        cryptogram.onPuzzleCompleted()

        // TODO animate finish vector
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && iv_check.drawable is AnimatedVectorDrawable) {
            (iv_check.drawable as AnimatedVectorDrawable).apply {
                iv_check.visibility = View.INVISIBLE
                iv_check.postDelayed({
                    iv_check.visibility = View.VISIBLE
                    start()
                }, 1000)
            }
        }

        if (!FirebaseCrashlytics.getInstance().didCrashOnPreviousExecution()) {
            // Increment the trigger for displaying the rating dialog
            RatingsBooster.incrementAndPrompt()
        }

        // Conditional behavior after X triggers
        val provider = PuzzleProvider.getInstance(this)
        var count = 0L
        for (c in provider.all) {
            if (!c.isInstruction && c.isCompleted) {
                count++
            }
        }
        val suggestDonationCount = PrefsUtils.suggestDonationCount
        if (DONATIONS_ENABLED && count >= suggestDonationCount) {
            // Prompt for donations only if user hasn't already donated
            val purchases = PrefsUtils.purchases
            PrefsUtils.suggestDonationCount = count + BillingUtils.DONATION_SUGGESTION_FREQUENCY
            if (purchases == null) {
                // Query purchases
                BillingUtils.updatePurchases(this)
            } else if (purchases.isEmpty()) {
                BillingUtils.suggestDonation(this)
            } else {
                AchievementProvider.onDonated()
            }
        }

        // Show prompts for frustrated users
        if (bt_flag.isShown && !PrefsUtils.highlightedFrustrationFlag && CryptogramApp.instance.remoteConfig.getBoolean(RemoteConfig.FRUSTRATION_HINT)) {
            val createBalloon = createBalloon(this) {
                setArrowSize(12)
                setCornerRadius(4f)
                setPadding(8)
                setArrowPosition(0.88f)
                setArrowOrientation(ArrowOrientation.BOTTOM)
                setText(getString(R.string.highlight_frustration_flag))
                setTextColor(getColorFromAttr(R.attr.textColorOnPrimary))
                setBackgroundColor(getColorFromAttr(R.attr.colorPrimaryDark))
                setBalloonAnimation(BalloonAnimation.FADE)
                setLifecycleOwner(lifecycleOwner)
                dismissWhenTouchOutside = true
                dismissWhenClicked = true
            }
            bt_flag.postDelayed({
                createBalloon.showAlignTop(bt_flag)
            }, 500)
            bt_flag.postDelayed({
                // Only stop showing this if it was visible for a moment
                if (createBalloon.isShowing) {
                    PrefsUtils.highlightedFrustrationFlag = true
                }
            }, 1300)
        }

        // Using the Google sign in
        val account = GoogleSignIn.getLastSignedInAccount(this)
        if (account != null) {
            // Attach view for popups
            Games.getGamesClient(this, account).setViewForPopups(drawer_layout)

            // Submit score
            LeaderboardProvider.submit(Games.getLeaderboardsClient(this, account))

            // Submit any achievements
            AchievementProvider.onCryptogramCompleted(this, Games.getAchievementsClient(this, account))

            // Attempt to save the game to Google Play Saved Games
            provider.save(this, Games.getSnapshotsClient(this, account), null)
        } else if (BuildConfig.DEBUG) {
            // Check achievements for offline
            AchievementProvider.onCryptogramCompleted(this, null)
        }
    }

    @Subscribe
    fun onPuzzleKeyboardInput(event: PuzzleEvent.KeyboardInputEvent) {
        val keyCode = event.keyCode
        cryptogram.dispatchKeyEvent(KeyEvent(0, 0, KeyEvent.ACTION_DOWN,
                keyCode, 0))
        cryptogram.dispatchKeyEvent(KeyEvent(0, 0, KeyEvent.ACTION_UP,
                keyCode, 0))
    }

    @Subscribe
    fun onRemoteConfigPush(event: RemoteConfigEvent.Push) {
        if (BuildConfig.DEBUG) {
            Toast.makeText(this, "Remote configuration changing…", Toast.LENGTH_SHORT).show()
        }
    }

    @Subscribe
    fun onRemoteConfigChanged(event: RemoteConfigEvent.Change) {
        if (event.updated) {
            updateCryptogram(cryptogram.puzzle)
        }
    }

    @Subscribe
    fun onPuzzlesChanged(event: PuzzlesEvent.Change) {
        if (event.newPuzzles > 0) {
            showSnackbar(event.message, autoDismiss = false)
        }
        refreshPuzzles()
        updateCryptogram(cryptogram.puzzle)
    }

    override fun onDrawerOpened(drawerView: View?) {}

    override fun onDrawerClosed(drawerView: View?) {}

    override fun onDrawerMoving() {
        cryptogram.hideSoftInput()
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        // Inflate the menu; this adds items to the action bar if it is present.
        menuInflater.inflate(R.menu.menu_cryptogram, menu)
        menu.findItem(R.id.action_reveal_puzzle).apply {
            isVisible = BuildConfig.DEBUG
        }
        menu.findItem(R.id.action_donate).apply {
            isVisible = DONATIONS_ENABLED
        }
        menu.findItem(R.id.action_admin).apply {
            isVisible = BuildConfig.DEBUG
        }
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        if (super.onOptionsItemSelected(item)) {
            return true
        }

        val puzzle = cryptogram.puzzle
        when (item.itemId) {
            R.id.action_next -> {
                nextPuzzle()
                return true
            }
            R.id.action_reveal_letter -> {
                if (puzzle == null || !cryptogram.hasSelectedCharacter()) {
                    showSnackbar(getString(R.string.reveal_letter_instruction))
                } else {
                    if (PrefsUtils.neverAskRevealLetter) {
                        revealLetter()
                    } else {
                        MaterialDialog.Builder(this)
                                .content(R.string.reveal_letter_confirmation)
                                .checkBoxPromptRes(R.string.never_ask_again, false, null)
                                .positiveText(R.string.reveal)
                                .onPositive { dialog, _ ->
                                    PrefsUtils.neverAskRevealLetter = dialog.isPromptCheckBoxChecked
                                    revealLetter()
                                }
                                .negativeText(R.string.cancel)
                                .show()
                    }
                }
                return true
            }
            R.id.action_reveal_mistakes -> {
                puzzle?.let {
                    if (PrefsUtils.neverAskRevealMistakes) {
                        revealMistakes(it)
                    } else {
                        MaterialDialog.Builder(this)
                                .content(R.string.reveal_mistakes_confirmation)
                                .checkBoxPromptRes(R.string.never_ask_again, false, null)
                                .positiveText(R.string.reveal)
                                .onPositive { dialog, _ ->
                                    PrefsUtils.neverAskRevealMistakes = dialog.isPromptCheckBoxChecked
                                    revealMistakes(it)
                                }
                                .negativeText(R.string.cancel)
                                .show()
                    }
                }
                return true
            }
            R.id.action_reveal_puzzle -> {
                if (BuildConfig.DEBUG) {
                    puzzle?.revealPuzzle()
                    cryptogram.redraw()
                } else {
                    throw IllegalStateException("Only applicable to debug builds")
                }
                return true
            }
            R.id.action_reset -> {
                if (puzzle != null) {
                    AlertDialog.Builder(this)
                            .setMessage(R.string.reset_puzzle)
                            .setPositiveButton(R.string.reset) { _, _ ->
                                puzzle.reset(true)
                                cryptogram.reset()
                                showPuzzleState(puzzle)
                                onPuzzleChanged(puzzle, false)
                            }
                            .setNegativeButton(R.string.cancel) { _, _ -> }
                            .show()
                }
                return true
            }
            R.id.action_go_to -> {
                if (puzzle == null) {
                    if (viewDrawerLayout != null) {
                        viewDrawerLayout!!.openDrawer(GravityCompat.START)
                    }
                } else {
                    var prefilledText: String? = null
                    val currentId = puzzle.number
                    if (currentId > 0) {
                        prefilledText = currentId.toString()
                    }
                    MaterialDialog.Builder(this)
                            .content(R.string.go_to_puzzle_content)
                            .inputType(InputType.TYPE_CLASS_NUMBER)
                            .input(null, prefilledText) { dialog, input ->
                                val button = dialog.getActionButton(DialogAction.POSITIVE)
                                try {
                                    button.isEnabled = Integer.parseInt(input.toString()) > 0
                                } catch (ignored: NumberFormatException) {
                                    button.isEnabled = false
                                }
                            }
                            .alwaysCallInputCallback()
                            .showListener { dialogInterface ->
                                val dialog = dialogInterface as MaterialDialog

                                dialog.inputEditText!!.selectAll()
                            }
                            .onPositive { dialog, _ ->
                                val input = dialog.inputEditText!!.text
                                try {
                                    val puzzleNumber = Integer.parseInt(input.toString())
                                    val provider = PuzzleProvider
                                            .getInstance(this@CryptogramActivity)
                                    val newPuzzle = provider.getByNumber(puzzleNumber)
                                    if (newPuzzle == null) {
                                        showSnackbar(getString(R.string.puzzle_nonexistant, puzzleNumber))
                                    } else {
                                        onPuzzleChanged(newPuzzle, false)
                                    }
                                } catch (ignored: NumberFormatException) {
                                }
                            }.show()
                }
                return true
            }
            R.id.action_share -> {
                val intent = Intent(Intent.ACTION_SEND)
                intent.type = "text/plain"
                val text = if (puzzle != null && puzzle.isCompleted) {
                    getString(
                            R.string.share_full,
                            puzzle.text,
                            puzzle.author,
                            getString(R.string.share_url))
                } else {
                    getString(
                            R.string.share_partial,
                            if (puzzle == null)
                                getString(R.string.author_unknown)
                            else
                                puzzle.author,
                            getString(R.string.share_url))
                }
                intent.putExtra(Intent.EXTRA_TEXT, text)
                startActivity(Intent.createChooser(intent, getString(R.string.share)))
                // Analytics
                val puzzleId = puzzle?.id?.toString()
                val bundle = Bundle()
                bundle.putString(FirebaseAnalytics.Param.LEVEL, puzzleId)
                bundle.putString(FirebaseAnalytics.Param.CONTENT, text)
                CryptogramApp.instance.firebaseAnalytics.logEvent(FirebaseAnalytics.Event.SHARE, bundle)
                return true
            }
            R.id.action_stats -> {
                // Make sure to save the puzzle first
                puzzle?.save()
                // Now show the stats
                StatisticsUtils.showDialog(this);
                return true
            }
            R.id.action_settings -> {
                startActivity(SettingsActivity.create(this))
                return true
            }
            R.id.action_how_to_play -> {
                startActivity(HowToPlayActivity.create(this))
                return true
            }
            R.id.action_gameplay_tips -> {
                startActivity(TipsActivity.create(this))
                return true
            }
            R.id.action_about -> {
                startActivity(AboutActivity.create(this))
                return true
            }
            R.id.action_donate -> {
                startActivity(DonateActivity.create(this))
                return true
            }
            R.id.action_admin -> {
                startActivity(AdminActivity.create(this))
                return true
            }
        }
        return super.onOptionsItemSelected(item)
    }

    private fun revealLetter() {
        val c = cryptogram.caretChar
        cryptogram.revealCharacterMapping(c)
        if (CryptogramApp.instance.remoteConfig.getBoolean(RemoteConfig.SUBMIT_REVEALS)) {
            // Submit which character was revealed for analytics
            auth.currentUser?.let { user ->
                cryptogram.puzzle?.let { puzzle ->
                    val reveals = mutableMapOf<String, Any>(Pair(c.toString(), true))
                    reveals["timestamp"] = Timestamp(Date())
                    db.collection("users").document(user.uid)
                            .collection("reveals").document(puzzle.id.toString())
                            .set(reveals, SetOptions.merge())
                            .addOnSuccessListener {
                                Log.d(TAG, "DocumentSnapshot written")
                            }
                            .addOnFailureListener { e ->
                                Log.w(TAG, "DocumentSnapshot failed writing", e)
                            }
                }
            }
        }
    }

    private fun revealMistakes(puzzle: Puzzle) {
        if (puzzle.revealMistakes() == 0) {
            showSnackbar(getString(R.string.reveal_mistakes_none))
        } else {
            // Show those mistakes
            cryptogram.redraw()
        }
    }

    private fun nextPuzzle() {
        val puzzle = PuzzleProvider.getInstance(this).next
        puzzle.let {
            if (it == null) {
                val customView = LayoutInflater.from(this).inflate(R.layout.dialog_game_completed, null)
                MaterialDialog.Builder(this)
                        .customView(customView, false)
                        .cancelable(false)
                        .positiveText(android.R.string.ok)
                        .show()
            } else {
                onPuzzleChanged(it, false)
            }
        }
    }

    private fun updateGooglePlayGames() {
        val account = GoogleSignIn.getLastSignedInAccount(this)
        if (account != null) {
            val playerTask = Games.getPlayersClient(this, account).currentPlayer
            playerTask.addOnFailureListener {
                bt_google_play_games.visibility = View.VISIBLE
                iv_google_play_games_avatar.visibility = View.GONE
                tv_google_play_games.visibility = View.VISIBLE
                tv_google_play_games_name.visibility = View.GONE
                vg_google_play_games_actions.visibility = View.GONE
            }
            playerTask.addOnSuccessListener { p ->
                val displayName: String
                val imageUri: Uri?
                if (p != null) {
                    displayName = p.displayName
                    imageUri = if (p.hasHiResImage()) p.hiResImageUri else p.iconImageUri
                    //bannerUri = p.getBannerImageLandscapeUri();
                } else {
                    // Set the greeting appropriately on main menu
                    displayName = account.displayName
                            ?: getString(R.string.google_play_games_player_unknown)
                    imageUri = account.photoUrl
                }
                Log.w(TAG, "onConnected(): current player is $displayName")

                bt_google_play_games.visibility = View.GONE
                iv_google_play_games_avatar.visibility = View.VISIBLE
                ImageManager.create(this).loadImage(iv_google_play_games_avatar, imageUri, R.drawable.im_avatar)
                tv_google_play_games.visibility = View.GONE
                tv_google_play_games_name.visibility = View.VISIBLE
                tv_google_play_games_name.text = displayName
                vg_google_play_games_actions.visibility = View.VISIBLE
            }
        } else {
            bt_google_play_games.visibility = View.VISIBLE
            iv_google_play_games_avatar.visibility = View.GONE
            tv_google_play_games.visibility = View.VISIBLE
            tv_google_play_games_name.visibility = View.GONE
            vg_google_play_games_actions.visibility = View.GONE
        }
    }

    private fun showGmsError(errorCode: Int) {
        if (isFinishing) {
            return
        }
        AlertDialog.Builder(this)
                .setMessage(getString(R.string.google_play_games_connection_failure, lastConnectionError, errorCode))
                .setPositiveButton(android.R.string.ok) { dialog, _ -> dialog.dismiss() }
                .show()
    }

}
