/**
 * Teapod
 *
 * Copyright 2020-2022  <seil0@mosad.xyz>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 *
 */

package org.mosad.teapod.ui.activity.player

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.annotation.SuppressLint
import android.app.PictureInPictureParams
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Rect
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.util.Rational
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GestureDetectorCompat
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ui.StyledPlayerControlView
import com.google.android.exoplayer2.ui.StyledPlayerView
import com.google.android.exoplayer2.util.Util
import kotlinx.coroutines.launch
import org.mosad.teapod.R
import org.mosad.teapod.databinding.ActivityPlayerBinding
import org.mosad.teapod.databinding.PlayerControlsBinding
import org.mosad.teapod.parser.crunchyroll.NoneEpisode
import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.ui.activity.player.fragment.EpisodeListDialogFragment
import org.mosad.teapod.ui.activity.player.fragment.LanguageSettingsDialogFragment
import org.mosad.teapod.util.hideBars
import org.mosad.teapod.util.isInPiPMode
import org.mosad.teapod.util.navToLauncherTask
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.concurrent.scheduleAtFixedRate

class PlayerActivity : AppCompatActivity() {

    private val model: PlayerViewModel by viewModels()
    private lateinit var playerBinding: ActivityPlayerBinding
    private lateinit var controlsBinding: PlayerControlsBinding

    private lateinit var controller: StyledPlayerControlView
    private lateinit var gestureDetector: GestureDetectorCompat
    private lateinit var controlsUpdates: TimerTask

    private var wasInPiP = false
    private var remainingTime: Long = 0

    private val rwdTime: Long = 10000.unaryMinus()
    private val fwdTime: Long = 10000
    private val defaultShowTimeoutMs = 5000

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_player)
        hideBars() // Initial hide the bars

        playerBinding = ActivityPlayerBinding.bind(findViewById(R.id.player_root))
        controlsBinding = PlayerControlsBinding.bind(findViewById(R.id.player_controls_root))

        model.loadMediaAsync(
            intent.getStringExtra(getString(R.string.intent_season_id)) ?: "",
            intent.getStringExtra(getString(R.string.intent_episode_id)) ?: ""
        )
        model.currentEpisodeChangedListener.add { onMediaChanged() }
        gestureDetector = GestureDetectorCompat(this, PlayerGestureListener())

        controller = playerBinding.videoView.findViewById(R.id.exo_controller)
        controller.isAnimationEnabled = false // disable controls (time-bar) animation

        initExoPlayer() // call in onCreate, exoplayer lives in view model
        initGUI()
        initActions()
    }

    /**
     * once minimum is android 7.0 this can be simplified
     * only onStart and onStop should be needed then
     * see: https://developer.android.com/guide/topics/ui/picture-in-picture#continuing_playback
     */
    override fun onStart() {
        super.onStart()
        if (Util.SDK_INT > 23) {
            initPlayer()
            playerBinding.videoView.onResume()
        }
    }

    override fun onResume() {
        super.onResume()
        if (isInPiPMode()) { return }

        if (Util.SDK_INT <= 23) {
            initPlayer()
            playerBinding.videoView.onResume()
        }
    }

    override fun onPause() {
        super.onPause()

        if (isInPiPMode()) { return }
        if (Util.SDK_INT <= 23) { onPauseOnStop() }
    }

    override fun onStop() {
        super.onStop()

        if (Util.SDK_INT > 23) { onPauseOnStop() }
        // if the player was in pip, it's on a different task
        if (wasInPiP) { navToLauncherTask() }
        // if the player is in pip, remove the task, else we'll get a zombie
        if (isInPiPMode()) { finishAndRemoveTask() }
    }

    /**
     * used, when the player is in pip and the user selects a new media
     */
    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)

        // when the intent changed, load the new media and play it
        intent?.let {
            model.loadMediaAsync(
                it.getStringExtra(getString(R.string.intent_season_id)) ?: "",
                it.getStringExtra(getString(R.string.intent_episode_id)) ?: ""
            )
            model.playCurrentMedia()
        }
    }

    /**
     * previous to android n, don't override
     */
    @RequiresApi(Build.VERSION_CODES.N)
    override fun onUserLeaveHint() {
        super.onUserLeaveHint()

        // start pip mode, if supported
        if (packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
                @Suppress("deprecation")
                enterPictureInPictureMode()
            } else {
                val width = model.player.videoFormat?.width ?: 0
                val height = model.player.videoFormat?.height ?: 0
                val contentFrame: View = playerBinding.videoView.findViewById(R.id.exo_content_frame)
                val contentRect = with(contentFrame) {
                    val (x, y) = intArrayOf(0, 0).also(::getLocationInWindow)
                    Rect(x, y, x + width, y + height)
                }

                val params = PictureInPictureParams.Builder()
                    .setAspectRatio(Rational(width, height))
                    .setSourceRectHint(contentRect)
                    .build()
                enterPictureInPictureMode(params)
            }

            wasInPiP = isInPiPMode()
        }
    }

    override fun onPictureInPictureModeChanged(
        isInPictureInPictureMode: Boolean,
        newConfig: Configuration
    ) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
        }

        // Hide the full-screen UI (controls, etc.) while in picture-in-picture mode.
        playerBinding.videoView.useController = !isInPictureInPictureMode

        // TODO also hide language settings/episodes list
    }

    private fun initPlayer() {
        initVideoView()
        initTimeUpdates()

        // if the player is ready or buffering we can simply play the file again, else do nothing
        val playbackState = model.player.playbackState
        if ((playbackState == ExoPlayer.STATE_READY || playbackState == ExoPlayer.STATE_BUFFERING)) {
            model.player.play()
        }
    }

    /**
     * set play when ready and listeners
     */
    private fun initExoPlayer() {
        model.player.addListener(object : Player.Listener {
            override fun onPlaybackStateChanged(state: Int) {
                super.onPlaybackStateChanged(state)

                playerBinding.loading.visibility = when (state) {
                    ExoPlayer.STATE_READY -> View.GONE
                    ExoPlayer.STATE_BUFFERING -> View.VISIBLE
                    else -> View.GONE
                }

                // don't use isVisible to hide exoPlayPause, as it will set the visibility to GONE
                controlsBinding.exoPlayPause.visibility = when(playerBinding.loading.isVisible) {
                    true -> View.INVISIBLE
                    false -> View.VISIBLE
                }

                if (state == ExoPlayer.STATE_ENDED && hasNextEpisode() && Preferences.autoplay) {
                    playNextEpisode()
                }
            }
        })

        // revert back to the old behaviour (blocking init) in case there are any issues with async init
        // start playing the current episode, after all needed player components have been initialized
        //model.playCurrentMedia(model.currentPlayhead)
    }

    @SuppressLint("ClickableViewAccessibility")
    private fun initVideoView() {
        playerBinding.videoView.player = model.player

        // when the player controls get hidden, hide the bars too
        playerBinding.videoView.setControllerVisibilityListener(StyledPlayerView.ControllerVisibilityListener {
            when (it) {
                View.GONE -> {
                    hideBars()
                    // TODO also hide the skip op button
                }
                View.VISIBLE -> updateControls()
            }
        })

        playerBinding.videoView.setOnTouchListener { _, event ->
            gestureDetector.onTouchEvent(event)
            true
        }
    }

    private fun initActions() {
        controlsBinding.exoClosePlayer.setOnClickListener {
            this.finish()
        }
        controlsBinding.rwd10.setOnButtonClickListener { rewind() }
        controlsBinding.ffwd10.setOnButtonClickListener { fastForward() }
        playerBinding.buttonNextEp.setOnClickListener { playNextEpisode() }
        playerBinding.buttonSkipOp.setOnClickListener { skipOpening() }
        controlsBinding.buttonLanguage.setOnClickListener { showLanguageSettings() }
        controlsBinding.buttonEpisodes.setOnClickListener { showEpisodesList() }
        controlsBinding.buttonNextEpC.setOnClickListener { playNextEpisode() }
    }

    private fun initGUI() {
        // TODO reimplement for cr
//        if (model.media.type == DataTypes.MediaType.MOVIE) {
//            button_episodes.visibility = View.GONE
//        }
    }

    private fun initTimeUpdates() {
        if (this::controlsUpdates.isInitialized) {
            controlsUpdates.cancel()
        }

        controlsUpdates = Timer().scheduleAtFixedRate(0, 500) {
            lifecycleScope.launch {
                val currentPosition = model.player.currentPosition
                val btnNextEpIsVisible = playerBinding.buttonNextEp.isVisible
                val controlsVisible = controller.isVisible

                // make sure remaining time is > 0
                if (model.player.duration > 0) {
                    remainingTime = model.player.duration - currentPosition
                    remainingTime = if (remainingTime < 0) 0 else remainingTime
                } else {
                    remainingTime = 0
                }

                // TODO add metaDB ending_start support
                // if remaining time > 1 and < 20 sec, a next ep is set, autoplay is enabled
                // and not in pip: show next ep button
                if (remainingTime in 1000..20000) {
                    if (!btnNextEpIsVisible && hasNextEpisode() && Preferences.autoplay && !isInPiPMode()) {
                        showButtonNextEp()
                    }
                } else if (btnNextEpIsVisible) {
                    hideButtonNextEp()
                }

                // into metadata is present and we can show the skip button
                if (model.currentIntroMetadata.duration >= 10) {
                    val startTime = model.currentIntroMetadata.startTime.toInt() * 1000
                    if (currentPosition in startTime..(startTime + 10000) && !playerBinding.buttonSkipOp.isVisible) {
                        showButtonSkipOp()
                    } else if (playerBinding.buttonSkipOp.isVisible &&
                        currentPosition !in startTime..(startTime + 10000)
                    ) {
                        // the button should only be visible if currentEpisodeMeta != null
                        hideButtonSkipOp()
                    }

                }

                // if controls are visible, update them
                if (controlsVisible) {
                    updateControls()
                }
            }
        }
    }

    private fun onPauseOnStop() {
        playerBinding.videoView.onPause()
        model.player.pause()
        controlsUpdates.cancel()
    }

    /**
     * update the custom controls
     */
    private fun updateControls() {
        // update remaining time label
        val hours =  TimeUnit.MILLISECONDS.toHours(remainingTime) % 24
        val minutes = TimeUnit.MILLISECONDS.toMinutes(remainingTime) % 60
        val seconds = TimeUnit.MILLISECONDS.toSeconds(remainingTime) % 60

        // if remaining time is below 60 minutes, don't show hours
        controlsBinding.exoRemaining.text = if (TimeUnit.MILLISECONDS.toMinutes(remainingTime) < 60) {
            getString(R.string.time_min_sec, minutes, seconds)
        } else {
            getString(R.string.time_hour_min_sec, hours, minutes, seconds)
        }
    }

    /**
     * This methode is called, if the current episode has changed.
     * Update title text and next ep button visibility.
     * If the currentEpisode changed to NoneEpisode, exit the activity.
     */
    private fun onMediaChanged() {
        if (model.currentEpisode == NoneEpisode) {
            Log.e(javaClass.name, "No media was set.")
            this.finish()
        }

        controlsBinding.exoTextTitle.text = model.getMediaTitle()

        // hide the next episode button, if there is none
        controlsBinding.buttonNextEpC.isVisible = hasNextEpisode()
    }

    /**
     * Check if the current episode has a next episode.
     *
     * @return Boolean: true if there is a next episode, else false.
     */
    private fun hasNextEpisode(): Boolean {
        return (model.currentEpisode.nextEpisodeId != null && !model.currentEpisodeIsLastEpisode())
    }

    /**
     * TODO set position of rewind/fast forward indicators programmatically
     */

    private fun rewind() {
        model.seekToOffset(rwdTime)

        // hide/show needed components
        playerBinding.exoDoubleTapIndicator.visibility = View.VISIBLE
        playerBinding.ffwd10Indicator.visibility = View.INVISIBLE
        controlsBinding.rwd10.visibility = View.INVISIBLE

        playerBinding.rwd10Indicator.onAnimationEndCallback = {
            playerBinding.exoDoubleTapIndicator.visibility = View.GONE
            playerBinding.ffwd10Indicator.visibility = View.VISIBLE
            controlsBinding.rwd10.visibility = View.VISIBLE
        }

        // run animation
        playerBinding.rwd10Indicator.runOnClickAnimation()
    }

    private fun fastForward() {
        model.seekToOffset(fwdTime)

        // hide/show needed components
        playerBinding.exoDoubleTapIndicator.visibility = View.VISIBLE
        playerBinding.rwd10Indicator.visibility = View.INVISIBLE
        controlsBinding.ffwd10.visibility = View.INVISIBLE

        playerBinding.ffwd10Indicator.onAnimationEndCallback = {
            playerBinding.exoDoubleTapIndicator.visibility = View.GONE
            playerBinding.rwd10Indicator.visibility = View.VISIBLE
            controlsBinding.ffwd10.visibility = View.VISIBLE
        }

        // run animation
        playerBinding.ffwd10Indicator.runOnClickAnimation()
    }

    private fun playNextEpisode() {
        // disable the next episode buttons, so a user can't double click it
        playerBinding.buttonNextEp.isClickable = false
        controlsBinding.buttonNextEpC.isClickable = false

        hideButtonNextEp()
        model.playNextEpisode()

        // enable the next episode buttons when playNextEpisode() has returned
        playerBinding.buttonNextEp.isClickable = true
        controlsBinding.buttonNextEpC.isClickable = true
    }

    private fun skipOpening() {
        // calculate the seek time
        if (model.currentIntroMetadata.duration > 10) {
            val endTime = model.currentIntroMetadata.endTime.toInt() * 1000
            val seekTime = endTime - model.player.currentPosition
            model.seekToOffset(seekTime)
        }
    }

    /**
     * show the next episode button
     * TODO improve the show animation
     */
    private fun showButtonNextEp() {
        playerBinding.buttonNextEp.isVisible = true
        playerBinding.buttonNextEp.alpha = 0.0f

        playerBinding.buttonNextEp.animate()
            .alpha(1.0f)
            .setListener(null)
    }

    /**
     * hide the next episode button
     * TODO improve the hide animation
     */
    private fun hideButtonNextEp() {
        playerBinding.buttonNextEp.animate()
            .alpha(0.0f)
            .setListener(object : AnimatorListenerAdapter() {
                override fun onAnimationEnd(animation: Animator) {
                    super.onAnimationEnd(animation)
                    playerBinding.buttonNextEp.isVisible = false
                }
            })
    }

    private fun showButtonSkipOp() {
        playerBinding.buttonSkipOp.isVisible = true
        playerBinding.buttonSkipOp.alpha = 0.0f

        playerBinding.buttonSkipOp.animate()
            .alpha(1.0f)
            .setListener(null)
    }

    private fun hideButtonSkipOp() {
        playerBinding.buttonSkipOp.animate()
            .alpha(0.0f)
            .setListener(object : AnimatorListenerAdapter() {
                override fun onAnimationEnd(animation: Animator) {
                    super.onAnimationEnd(animation)
                    playerBinding.buttonSkipOp.isVisible = false
                }
            })

    }

    private fun showEpisodesList() {
        pauseAndHideControls()
        EpisodeListDialogFragment().show(supportFragmentManager, EpisodeListDialogFragment.TAG)
    }

    private fun showLanguageSettings() {
        pauseAndHideControls()
        LanguageSettingsDialogFragment().show(supportFragmentManager, LanguageSettingsDialogFragment.TAG)
    }

    /**
     * pause playback and hide controls
     */
    private fun pauseAndHideControls() {
        model.player.pause() // showTimeoutMs is set to 0 when calling pause, but why
        controller.showTimeoutMs = defaultShowTimeoutMs // fix showTimeoutMs set to 0
        controller.hide()
    }

    inner class PlayerGestureListener : GestureDetector.SimpleOnGestureListener() {

        /**
         * on single tap hide or show the controls
         */
        override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
            if (!isInPiPMode()) {
                if (controller.isVisible) controller.hide() else controller.show()
            }

            return true
        }

        /**
         * on double tap rewind or forward
         */
        override fun onDoubleTap(e: MotionEvent): Boolean {
            val eventPosX = e.x.toInt()
            val viewCenterX = playerBinding.videoView.measuredWidth / 2

            // if the event position is on the left side rewind, if it's on the right forward
            if (eventPosX < viewCenterX) rewind() else fastForward()

            return true
        }

        /**
         * not used
         */
        override fun onDoubleTapEvent(e: MotionEvent): Boolean {
            return true
        }

        /**
         * on long press toggle pause/play
         */
        override fun onLongPress(e: MotionEvent) {
            model.togglePausePlay()
        }

    }

}
