package com.ponzischeme89.memby.ui.player

import android.content.Context
import android.graphics.Color
import android.graphics.Typeface
import android.text.TextUtils
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.view.isVisible
import com.ponzischeme89.memby.R

/** One face in the cast panel. [role] is the character, which is what a viewer is asking. */
data class CastMember(
    val name: String,
    val role: String? = null,
    val imageUrl: String? = null,
)

/**
 * What the panel is showing.
 *
 * [loaded] is separate from an empty [members] list because "still fetching" and "this
 * title has no cast recorded" are different things to be told, and a spinner that never
 * resolves is the worse of the two to leave on screen.
 */
data class CastPanelState(
    val title: String = "",
    val members: List<CastMember> = emptyList(),
    val loaded: Boolean = false,
)

/**
 * Fills the cast overlay from a plain state.
 *
 * Like [bindSubtitleMenu], it lives apart from [PlayerActivity] so `CastPanelScreenshotTest`
 * can render the real panel — these cards, these colours, this layout — with no player, no
 * Emby server and no network. [loadImage] is injected for the same reason: artwork is the
 * one thing a screenshot test cannot fetch, and passing it in means the test renders the
 * initials fallback rather than a blank where every portrait should be.
 */
fun bindCastPanel(
    overlay: View,
    state: CastPanelState,
    loadImage: (ImageView, String) -> Unit = { _, _ -> },
) {
    val context = overlay.context
    overlay.findViewById<TextView>(R.id.player_cast_title).apply {
        text = state.title
        isVisible = state.title.isNotBlank()
    }
    overlay.findViewById<TextView>(R.id.player_cast_status).apply {
        text = when {
            !state.loaded -> context.getString(R.string.player_cast_loading)
            state.members.isEmpty() -> context.getString(R.string.player_cast_empty)
            else -> ""
        }
        isVisible = !text.isNullOrEmpty()
    }
    val people = overlay.findViewById<LinearLayout>(R.id.player_cast_people)
    people.removeAllViews()
    state.members.forEach { member -> people.addView(castCard(context, member, loadImage)) }
    overlay.findViewById<View>(R.id.player_cast_scroller).isVisible = state.members.isNotEmpty()
}

private fun castCard(
    context: Context,
    member: CastMember,
    loadImage: (ImageView, String) -> Unit,
): View {
    val density = context.resources.displayMetrics.density
    fun dp(value: Int) = (value * density).toInt()

    return LinearLayout(context).apply {
        orientation = LinearLayout.VERTICAL
        isFocusable = true
        isClickable = true
        // Nothing happens on a press. The panel is a reference, not a destination — there
        // is no person page to open — but the card still has to be focusable, or a D-pad
        // cannot scroll the row at all.
        setOnClickListener { }
        clipChildren = false
        layoutParams = LinearLayout.LayoutParams(dp(132), ViewGroup.LayoutParams.WRAP_CONTENT).apply {
            marginEnd = dp(18)
        }
        setOnFocusChangeListener { view, focused ->
            view.animate()
                .scaleX(if (focused) 1.06f else 1f)
                .scaleY(if (focused) 1.06f else 1f)
                .setDuration(120L)
                .start()
        }

        addView(castPortrait(context, member, loadImage, ::dp))
        addView(
            TextView(context).apply {
                text = member.name
                setTextColor(Color.WHITE)
                textSize = 14f
                typeface = Typeface.create("sans-serif", Typeface.BOLD)
                maxLines = 1
                ellipsize = TextUtils.TruncateAt.END
                setPadding(0, dp(9), 0, 0)
            },
        )
        member.role?.takeIf(String::isNotBlank)?.let { role ->
            addView(
                TextView(context).apply {
                    text = role
                    setTextColor(Color.rgb(158, 168, 178))
                    textSize = 11f
                    maxLines = 1
                    ellipsize = TextUtils.TruncateAt.END
                    setPadding(0, dp(2), 0, 0)
                },
            )
        }
    }
}

/**
 * The portrait, with the person's initials underneath it.
 *
 * Emby has no photo for a good part of a typical cast, and a row of identical grey
 * rectangles tells a viewer nothing about which name is which. The initials sit *behind*
 * the image rather than replacing it, so nothing has to decide in advance whether the
 * artwork will arrive — when it does, it simply covers them.
 */
private fun castPortrait(
    context: Context,
    member: CastMember,
    loadImage: (ImageView, String) -> Unit,
    dp: (Int) -> Int,
): View = FrameLayout(context).apply {
    layoutParams = LinearLayout.LayoutParams(dp(132), dp(176))
    background = context.getDrawable(R.drawable.player_cast_portrait_background)
    // The ring is drawn over the artwork, and the frame follows the card's focus rather
    // than its own — the card is what takes focus, the portrait is never focusable itself.
    foreground = context.getDrawable(R.drawable.player_cast_portrait_frame)
    isDuplicateParentStateEnabled = true
    clipToOutline = true

    addView(
        TextView(context).apply {
            layoutParams = FrameLayout.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT,
            )
            text = castInitials(member.name)
            setTextColor(Color.rgb(126, 138, 148))
            textSize = 34f
            typeface = Typeface.create("sans-serif-light", Typeface.NORMAL)
            gravity = Gravity.CENTER
        },
    )
    addView(
        ImageView(context).apply {
            layoutParams = FrameLayout.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT,
            )
            scaleType = ImageView.ScaleType.CENTER_CROP
            contentDescription = member.name
            member.imageUrl?.takeIf(String::isNotBlank)?.let { loadImage(this, it) }
        },
    )
}

/**
 * Up to two initials from a name. Pure, so it can be tested without a view: the cases that
 * matter are a mononym, a name with a middle name (the *last* initial is the useful one,
 * not the second) and a blank, which yields nothing rather than a stray character.
 */
internal fun castInitials(name: String): String {
    val parts = name.trim().split(Regex("\\s+")).filter(String::isNotBlank)
    return when (parts.size) {
        0 -> ""
        1 -> parts[0].take(1).uppercase()
        else -> (parts.first().take(1) + parts.last().take(1)).uppercase()
    }
}
