package com.fabledsword.minstrel.shared.widgets

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.layout.ContentScale
import coil3.compose.AsyncImage
import coil3.compose.AsyncImagePainter
import com.fabledsword.minstrel.shared.resolveServerUrl

// The fallback fades out as the artwork crossfades in (Coil's crossfade is
// configured globally on the ImageLoader in MinstrelApplication). Matching
// durations makes the swap read as one cross-dissolve; without the fade the
// placeholder icon vanished a frame before the cover appeared, which is the
// "art popping in" the Home veil exists to hide (issue #2327).
private const val FALLBACK_FADE_MS = 220

/**
 * Renders a server-hosted image, resolving relative URLs centrally so
 * every cover surface loads consistently. Shows [fallback] when the URL
 * is blank/unresolvable, while the image is still loading, and when the
 * load fails — so a tile is never left blank (e.g. art not yet backfilled,
 * which the "You might like" row hits often).
 *
 * In-flight loads are reported to [LocalArtSettleTracker] when a screen
 * provides one, so a screen-level overlay can wait for artwork to land
 * instead of guessing with a fixed delay.
 */
@Composable
fun ServerImage(
    url: String?,
    contentDescription: String?,
    modifier: Modifier = Modifier,
    contentScale: ContentScale = ContentScale.Crop,
    fallback: @Composable () -> Unit = {},
) {
    val resolved = resolveServerUrl(url)
    if (resolved == null) {
        fallback()
        return
    }
    // Track Coil's load state so the fallback doubles as a placeholder
    // (loading) and an error state (404 / unreachable) — not just a
    // null-URL guard, which left present-but-failing URLs blank.
    var state by remember(resolved) {
        mutableStateOf<AsyncImagePainter.State>(AsyncImagePainter.State.Empty)
    }
    // Empty counts as loading: it's the pre-request state, so treating it
    // as settled would let a screen overlay lower before Coil even starts.
    val loading = state is AsyncImagePainter.State.Empty ||
        state is AsyncImagePainter.State.Loading
    val tracker = LocalArtSettleTracker.current
    DisposableEffect(tracker, loading) {
        if (loading) tracker?.begin()
        // Balanced by construction: the effect re-runs when `loading` flips
        // (decrement, then no re-increment) and disposes when a tile leaves
        // the composition mid-load (scrolled away).
        onDispose { if (loading) tracker?.end() }
    }
    val fallbackAlpha by animateFloatAsState(
        targetValue = if (loading || state is AsyncImagePainter.State.Error) 1f else 0f,
        animationSpec = tween(FALLBACK_FADE_MS),
        label = "art-fallback",
    )
    Box(modifier = modifier, contentAlignment = Alignment.Center) {
        AsyncImage(
            model = resolved,
            contentDescription = contentDescription,
            modifier = Modifier.fillMaxSize(),
            contentScale = contentScale,
            onState = { state = it },
        )
        if (fallbackAlpha > 0f) {
            Box(Modifier.alpha(fallbackAlpha), contentAlignment = Alignment.Center) {
                fallback()
            }
        }
    }
}
