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.HorizontalScrollView
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,
    val id: String = "",
    val deathDate: String? = null,
)

data class FilmographyCredit(
    val title: String,
    val year: Int? = null,
    val imageUrl: String? = null,
)

data class CastPersonPanel(
    val id: String,
    val name: String,
    val role: String? = null,
    val overview: String? = null,
    val birthDate: String? = null,
    val deathDate: String? = null,
    val filmography: List<FilmographyCredit> = emptyList(),
    val loaded: Boolean = false,
)

/**
 * 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,
    val selectedPerson: CastPersonPanel? = null,
)

/**
 * 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 = { _, _ -> },
    onMemberClick: (CastMember) -> Unit = {},
) {
    val context = overlay.context
    val selected = state.selectedPerson
    overlay.findViewById<TextView>(R.id.player_cast_eyebrow).text = context.getString(
        if (selected == null) R.string.player_cast_eyebrow else R.string.player_cast_profile_eyebrow,
    )
    overlay.findViewById<TextView>(R.id.player_cast_title).apply {
        text = selected?.name ?: state.title
        isVisible = !text.isNullOrBlank()
    }
    overlay.findViewById<TextView>(R.id.player_cast_status).apply {
        text = when {
            selected != null && !selected.loaded -> context.getString(R.string.player_cast_profile_loading)
            !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, onMemberClick))
    }
    overlay.findViewById<View>(R.id.player_cast_scroller).isVisible =
        selected == null && state.members.isNotEmpty()
    overlay.findViewById<LinearLayout>(R.id.player_cast_profile).apply {
        isVisible = selected?.loaded == true
        removeAllViews()
        selected?.takeIf { it.loaded }?.let { bindPersonProfile(this, it, loadImage) }
    }
    overlay.findViewById<TextView>(R.id.player_cast_back_hint).setText(
        if (selected == null) R.string.player_back_to_close else R.string.player_cast_back_to_cast,
    )
}

private fun castCard(
    context: Context,
    member: CastMember,
    loadImage: (ImageView, String) -> Unit,
    onClick: (CastMember) -> 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
        setOnClickListener { onClick(member) }
        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(LinearLayout(context).apply {
            orientation = LinearLayout.HORIZONTAL
            gravity = Gravity.CENTER_VERTICAL
            layoutParams = LinearLayout.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT,
            )
            setPadding(0, dp(9), 0, 0)
            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
            }, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
            if (!member.deathDate.isNullOrBlank()) {
                addView(ImageView(context).apply {
                    setImageResource(R.drawable.ic_deceased)
                    contentDescription = context.getString(R.string.player_cast_deceased)
                    setPadding(dp(4), 0, 0, 0)
                }, LinearLayout.LayoutParams(dp(18), dp(16)))
            }
        })
        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)
                },
            )
        }
    }
}

private fun bindPersonProfile(
    container: LinearLayout,
    person: CastPersonPanel,
    loadImage: (ImageView, String) -> Unit,
) {
    val context = container.context
    val density = context.resources.displayMetrics.density
    fun dp(value: Int) = (value * density).toInt()
    val life = personLifeDates(person.birthDate, person.deathDate)
    if (life.isNotBlank() || !person.role.isNullOrBlank()) {
        container.addView(TextView(context).apply {
            text = listOfNotNull(
                person.role?.takeIf(String::isNotBlank),
                life.takeIf(String::isNotBlank),
            ).joinToString("  ·  ")
            setTextColor(Color.rgb(185, 193, 200))
            textSize = 13f
            maxLines = 1
            ellipsize = TextUtils.TruncateAt.END
        })
    }
    container.addView(TextView(context).apply {
        text = person.overview?.takeIf(String::isNotBlank)
            ?: context.getString(R.string.player_cast_biography_empty)
        setTextColor(Color.WHITE)
        textSize = 14f
        maxLines = 3
        ellipsize = TextUtils.TruncateAt.END
        setLineSpacing(0f, 1.08f)
        setPadding(0, dp(7), 0, 0)
    })
    container.addView(TextView(context).apply {
        text = context.getString(R.string.player_cast_filmography)
        setTextColor(Color.rgb(82, 181, 75))
        textSize = 11f
        typeface = Typeface.create("sans-serif", Typeface.BOLD)
        letterSpacing = 0.12f
        setPadding(0, dp(13), 0, dp(5))
    })
    if (person.filmography.isEmpty()) {
        container.addView(TextView(context).apply {
            text = context.getString(R.string.player_cast_filmography_empty)
            setTextColor(Color.rgb(185, 193, 200))
            textSize = 13f
        })
        return
    }
    container.addView(HorizontalScrollView(context).apply {
        isHorizontalScrollBarEnabled = false
        overScrollMode = View.OVER_SCROLL_NEVER
        clipChildren = false
        addView(LinearLayout(context).apply {
            orientation = LinearLayout.HORIZONTAL
            clipChildren = false
            person.filmography.forEach { credit ->
                addView(filmographyCard(context, credit, loadImage, ::dp))
            }
        })
    })
}

private fun filmographyCard(
    context: Context,
    credit: FilmographyCredit,
    loadImage: (ImageView, String) -> Unit,
    dp: (Int) -> Int,
): View = LinearLayout(context).apply {
    orientation = LinearLayout.HORIZONTAL
    gravity = Gravity.CENTER_VERTICAL
    isFocusable = true
    isClickable = true
    setOnClickListener { }
    background = context.getDrawable(R.drawable.player_cast_portrait_background)
    foreground = context.getDrawable(R.drawable.player_cast_portrait_frame)
    setOnFocusChangeListener { view, focused ->
        view.animate()
            .scaleX(if (focused) 1.03f else 1f)
            .scaleY(if (focused) 1.03f else 1f)
            .setDuration(120L)
            .start()
    }
    setPadding(dp(5), dp(5), dp(9), dp(5))
    layoutParams = LinearLayout.LayoutParams(dp(194), dp(68)).apply { marginEnd = dp(10) }
    addView(FrameLayout(context).apply {
        layoutParams = LinearLayout.LayoutParams(dp(42), dp(58)).apply { marginEnd = dp(9) }
        background = context.getDrawable(R.drawable.player_cast_portrait_background)
        clipToOutline = true
        addView(ImageView(context).apply {
            layoutParams = FrameLayout.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT,
            )
            scaleType = ImageView.ScaleType.CENTER_CROP
            contentDescription = credit.title
            credit.imageUrl?.takeIf(String::isNotBlank)?.let { loadImage(this, it) }
        })
    })
    addView(LinearLayout(context).apply {
        orientation = LinearLayout.VERTICAL
        addView(TextView(context).apply {
            text = credit.title
            setTextColor(Color.WHITE)
            textSize = 12f
            typeface = Typeface.create("sans-serif", Typeface.BOLD)
            maxLines = 2
            ellipsize = TextUtils.TruncateAt.END
        })
        credit.year?.let { year ->
            addView(TextView(context).apply {
                text = year.toString()
                setTextColor(Color.rgb(158, 168, 178))
                textSize = 11f
            })
        }
    }, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
}

internal fun personLifeDates(birthDate: String?, deathDate: String?): String {
    val birth = formatPersonDate(birthDate)
    val death = formatPersonDate(deathDate)
    return when {
        birth.isNotBlank() && death.isNotBlank() -> "$birth – $death"
        birth.isNotBlank() -> "Born $birth"
        death.isNotBlank() -> "Died $death"
        else -> ""
    }
}

private fun formatPersonDate(value: String?): String {
    val match = Regex("""^(\d{4})-(\d{2})-(\d{2})""").find(value.orEmpty()) ?: return ""
    val (year, month, day) = match.destructured
    val monthName = listOf(
        "January", "February", "March", "April", "May", "June",
        "July", "August", "September", "October", "November", "December",
    ).getOrNull(month.toIntOrNull()?.minus(1) ?: -1) ?: return year
    return "${day.toIntOrNull() ?: day} $monthName $year"
}

/**
 * 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()
    }
}
