package ru.durakscore.app

import android.graphics.Bitmap
import android.graphics.Paint
import android.view.HapticFeedbackConstants
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collect
import java.util.Date
import java.util.UUID
import kotlin.random.Random

val GAME_REACTION_EMOJIS =
    listOf(
        "😂",
        "💩",
        "🔥",
        "👏"
    )

private fun reactionIconRes(emoji: String): Int =
    when (emoji) {
        "😂" -> R.drawable.reaction_laugh_jester
        "💩" -> R.drawable.reaction_poop_jester
        "🔥" -> R.drawable.reaction_fire_jester
        "👏" -> R.drawable.reaction_clap_jester
        else -> R.drawable.reaction_laugh_jester
    }

private const val REACTION_RECENT_WINDOW_MS =
    10_000L

private const val REACTION_SEND_COOLDOWN_MS =
    650L

private const val REACTION_PARTICLE_COUNT =
    10

/*
 * Максимальное падение сейчас до 7.5 сек + случайная задержка старта.
 * Через 9 сек конкретный дождь уже можно безопасно убрать из Compose.
 */
private const val REACTION_RAIN_LIFETIME_MS =
    9_000L

data class GameReaction(
    val id: String,
    val emoji: String,
    val senderUid: String,
    val senderName: String? = null,
    val createdAt: Timestamp?
)

private data class LocalReactionEvent(
    val gameId: String,
    val reaction: GameReaction
)

private object GameReactionLocalBus {

    val events =
        MutableSharedFlow<LocalReactionEvent>(
            extraBufferCapacity = 64
        )

    fun emit(
        gameId: String,
        reaction: GameReaction
    ) {
        events.tryEmit(
            LocalReactionEvent(
                gameId = gameId,
                reaction = reaction
            )
        )
    }
}

object GameReactionRepository {

    fun sendReaction(
        gameId: String,
        emoji: String,
        senderName: String,
        onSuccess: () -> Unit = {},
        onError: (String) -> Unit = {}
    ) {

        if (
            emoji !in
            GAME_REACTION_EMOJIS
        ) {
            onError(
                "Неизвестная реакция"
            )
            return
        }

        val user =
            DurakAuthSession.current()

        if (
            user ==
            null
        ) {
            onError(
                "Пользователь не авторизован"
            )
            return
        }

        val cleanSenderName =
            senderName
                .trim()
                .ifBlank {
                    "Игрок"
                }
                .take(
                    40
                )

        /*
         * UUID создаём на Android заранее.
         * Тот же id уходит на VPS, поэтому optimistic-дождь
         * и подтверждённая серверная реакция не дублируются.
         */
        val requestId =
            UUID.randomUUID()
                .toString()

        val localReaction =
            GameReaction(
                id =
                    requestId,
                emoji =
                    emoji,
                senderUid =
                    user.uid,
                senderName =
                    cleanSenderName,
                createdAt =
                    null
            )

        GameReactionLocalBus.emit(
            gameId =
                gameId,
            reaction =
                localReaction
        )

        DurakServerApi
            .sendReaction(
                user =
                    user,
                gameId =
                    gameId,
                requestId =
                    requestId,
                emoji =
                    emoji,
                onSuccess = {
                    onSuccess()
                },
                onError =
                    onError
            )
    }

    fun listenReactions(
        gameId: String,
        onReaction: (GameReaction) -> Unit,
        onError: (String) -> Unit = {},
        skipInitialSnapshot: Boolean = false
    ): ServerPollingHandle {

        val user =
            DurakAuthSession.current()

        if (
            user ==
            null
        ) {
            onError(
                "Пользователь не авторизован"
            )

            return ServerPollingHandle {
            }
        }

        val seenIds =
            mutableSetOf<String>()

        var firstSuccessfulSnapshot =
            true

        return DurakServerApi
            .startReactionPolling(
                user =
                    user,
                gameId =
                    gameId,
                onSnapshot = {
                        serverReactions ->

                    if (
                        skipInitialSnapshot &&
                        firstSuccessfulSnapshot
                    ) {
                        serverReactions
                            .forEach {
                                seenIds.add(
                                    it.id
                                )
                            }

                        firstSuccessfulSnapshot =
                            false

                        return@startReactionPolling
                    }

                    firstSuccessfulSnapshot =
                        false

                    val now =
                        System.currentTimeMillis()

                    serverReactions
                        .asReversed()
                        .forEach {
                                reaction ->

                            if (
                                reaction.id in
                                seenIds
                            ) {
                                return@forEach
                            }

                            seenIds.add(
                                reaction.id
                            )

                            val createdAtMillis =
                                reaction
                                    .createdAtEpochMillis

                            if (
                                createdAtMillis !=
                                null &&
                                now -
                                createdAtMillis >
                                REACTION_RECENT_WINDOW_MS
                            ) {
                                return@forEach
                            }

                            onReaction(
                                GameReaction(
                                    id =
                                        reaction.id,
                                    emoji =
                                        reaction.emoji,
                                    senderUid =
                                        reaction.senderUid,
                                    senderName =
                                        reaction.senderName,
                                    createdAt =
                                        createdAtMillis
                                            ?.let {
                                                Timestamp(
                                                    Date(
                                                        it
                                                    )
                                                )
                                            }
                                )
                            )
                        }
                },
                onError =
                    onError
            )
    }
}

private data class ReactionParticle(
    val startX: Float,
    val endX: Float,
    val delayMs: Long,
    val durationMs: Int,
    val sizeSp: Int,
    val startRotation: Float,
    val rotations: Float
)

private data class ActiveReactionRain(
    val reaction: GameReaction,
    val startedAtNanos: Long,
    val particles: List<ReactionParticle>
)

private fun createReactionParticles(
    reactionId: String
): List<ReactionParticle> {

    val random =
        Random(
            reactionId.hashCode()
        )

    return List(
        REACTION_PARTICLE_COUNT
    ) {

        val startX =
            random.nextFloat()

        val drift =
            (
                    random.nextFloat() -
                            0.5f
                    ) * 0.38f

        ReactionParticle(
            startX =
                startX,
            endX =
                (startX + drift)
                    .coerceIn(
                        0.02f,
                        0.94f
                    ),
            delayMs =
                random.nextLong(
                    from = 0L,
                    until = 650L
                ),
            durationMs =
                random.nextInt(
                    from = 4_350,
                    until = 7_500
                ),
            sizeSp =
                random.nextInt(
                    from = 34,
                    until = 55
                ),
            startRotation =
                random.nextFloat() *
                        80f -
                        40f,
            rotations =
                if (
                    random.nextBoolean()
                ) {
                    random.nextFloat() *
                            1.4f +
                            0.35f
                } else {
                    -(
                            random.nextFloat() *
                                    1.4f +
                                    0.35f
                            )
                }
        )
    }
}

private fun createActiveReactionRain(
    reaction: GameReaction
): ActiveReactionRain =
    ActiveReactionRain(
        reaction =
            reaction,
        startedAtNanos =
            System.nanoTime(),
        particles =
            createReactionParticles(
                reaction.id
            )
    )

private data class EmojiSprite(
    val bitmap: Bitmap,
    val paddingPx: Int
)

private class EmojiSpriteCache {

    private val textPaint =
        Paint(
            Paint.ANTI_ALIAS_FLAG
        ).apply {
            textAlign =
                Paint.Align.LEFT
            color =
                android.graphics.Color.WHITE
            isDither =
                true
            isSubpixelText =
                true
        }

    private val sprites =
        HashMap<String, EmojiSprite>()

    fun get(
        emoji: String,
        textSizePx: Float
    ): EmojiSprite {

        val sizePx =
            textSizePx
                .toInt()
                .coerceAtLeast(
                    1
                )

        val key =
            "$emoji@$sizePx"

        return sprites.getOrPut(
            key
        ) {

            textPaint.textSize =
                sizePx.toFloat()

            val metrics =
                textPaint.fontMetrics

            val paddingPx =
                (
                        sizePx *
                                0.20f
                        )
                    .toInt()
                    .coerceAtLeast(
                        4
                    )

            val contentWidth =
                textPaint
                    .measureText(
                        emoji
                    )
                    .toInt()
                    .coerceAtLeast(
                        1
                    )

            val contentHeight =
                (
                        metrics.descent -
                                metrics.ascent
                        )
                    .toInt()
                    .coerceAtLeast(
                        1
                    )

            val bitmap =
                Bitmap.createBitmap(
                    contentWidth +
                            paddingPx *
                            2,
                    contentHeight +
                            paddingPx *
                            2,
                    Bitmap.Config.ARGB_8888
                )

            val bitmapCanvas =
                android.graphics.Canvas(
                    bitmap
                )

            bitmapCanvas.drawText(
                emoji,
                paddingPx.toFloat(),
                paddingPx -
                        metrics.ascent,
                textPaint
            )

            EmojiSprite(
                bitmap =
                    bitmap,
                paddingPx =
                    paddingPx
            )
        }
    }

    fun clear() {

        sprites
            .values
            .forEach { sprite ->

                if (
                    !sprite.bitmap.isRecycled
                ) {
                    sprite.bitmap.recycle()
                }
            }

        sprites.clear()
    }
}

private fun DrawScope.drawReactionRains(
    rains: List<ActiveReactionRain>,
    frameTimeNanos: Long,
    spriteCache: EmojiSpriteCache,
    bitmapPaint: Paint
) {

    if (
        rains.isEmpty()
    ) {
        return
    }

    val widthPx =
        size.width

    val heightPx =
        size.height

    val canvas =
        drawContext
            .canvas
            .nativeCanvas

    rains.forEach rainLoop@ { rain ->

        val rainElapsedMs =
            (
                    frameTimeNanos -
                            rain.startedAtNanos
                    ) /
                    1_000_000f

        if (
            rainElapsedMs < 0f ||
            rainElapsedMs >
            REACTION_RAIN_LIFETIME_MS
        ) {
            return@rainLoop
        }

        rain.particles.forEach particleLoop@ { particle ->

            val particleElapsedMs =
                rainElapsedMs -
                        particle.delayMs

            if (
                particleElapsedMs < 0f
            ) {
                return@particleLoop
            }

            val progress =
                (
                        particleElapsedMs /
                                particle.durationMs
                        )
                    .coerceIn(
                        0f,
                        1f
                    )

            if (
                progress >= 1f
            ) {
                return@particleLoop
            }

            val x =
                (
                        particle.startX +
                                (
                                        particle.endX -
                                                particle.startX
                                        ) *
                                progress
                        ) *
                        widthPx

            val y =
                -100f +
                        (
                                heightPx +
                                        220f
                                ) *
                        progress

            val alpha =
                when {
                    progress < 0.05f ->
                        progress /
                                0.05f

                    progress > 0.90f ->
                        (
                                1f -
                                        progress
                                ) /
                                0.10f

                    else ->
                        1f
                }
                    .coerceIn(
                        0f,
                        1f
                    )

            val textSizePx =
                particle.sizeSp *
                        density *
                        fontScale

            val sprite =
                spriteCache.get(
                    emoji =
                        rain.reaction.emoji,
                    textSizePx =
                        textSizePx
                )

            bitmapPaint.alpha =
                (
                        alpha *
                                255f
                        )
                    .toInt()
                    .coerceIn(
                        0,
                        255
                    )

            val left =
                x -
                        sprite.paddingPx

            val top =
                y -
                        sprite.paddingPx

            val pivotX =
                left +
                        sprite.bitmap.width /
                        2f

            val pivotY =
                top +
                        sprite.bitmap.height /
                        2f

            val rotation =
                particle.startRotation +
                        360f *
                        particle.rotations *
                        progress

            canvas.save()

            canvas.rotate(
                rotation,
                pivotX,
                pivotY
            )

            canvas.drawBitmap(
                sprite.bitmap,
                left,
                top,
                bitmapPaint
            )

            canvas.restore()
        }
    }
}

@Composable
fun ReactionRainHost(
    gameId: String,
    modifier: Modifier = Modifier,
    skipExistingOnStart: Boolean = false
) {

    val activeRains =
        remember(gameId) {
            mutableStateListOf<ActiveReactionRain>()
        }

    val seenReactionIds =
        remember(gameId) {
            HashSet<String>()
        }

    var frameTimeNanos by
    remember(gameId) {
        mutableLongStateOf(
            System.nanoTime()
        )
    }

    val spriteCache =
        remember(gameId) {
            EmojiSpriteCache()
        }

    val bitmapPaint =
        remember(gameId) {
            Paint(
                Paint.ANTI_ALIAS_FLAG or
                        Paint.FILTER_BITMAP_FLAG
            ).apply {
                isDither =
                    true
            }
        }

    LaunchedEffect(gameId) {

        GameReactionLocalBus
            .events
            .collect { event ->

                if (
                    event.gameId ==
                    gameId &&
                    seenReactionIds.add(
                        event.reaction.id
                    )
                ) {
                    activeRains.add(
                        createActiveReactionRain(
                            event.reaction
                        )
                    )
                }
            }
    }

    DisposableEffect(
        gameId,
        skipExistingOnStart
    ) {

        val registration =
            GameReactionRepository
                .listenReactions(
                    gameId =
                        gameId,
                    skipInitialSnapshot =
                        skipExistingOnStart,
                    onReaction = { reaction ->

                        if (
                            seenReactionIds.add(
                                reaction.id
                            )
                        ) {
                            activeRains.add(
                                createActiveReactionRain(
                                    reaction
                                )
                            )
                        }
                    }
                )

        onDispose {
            registration.remove()
            activeRains.clear()
            seenReactionIds.clear()
            spriteCache.clear()
        }
    }

    val hasActiveRains =
        activeRains.isNotEmpty()

    LaunchedEffect(
        gameId,
        hasActiveRains
    ) {

        if (
            !hasActiveRains
        ) {
            return@LaunchedEffect
        }

        var lastCleanupNanos =
            0L

        var lastRenderNanos =
            0L

        while (
            activeRains.isNotEmpty()
        ) {

            withFrameNanos { frameNanos ->

                if (
                    lastRenderNanos ==
                    0L ||
                    frameNanos -
                    lastRenderNanos >=
                    22_000_000L
                ) {
                    lastRenderNanos =
                        frameNanos
                    frameTimeNanos =
                        frameNanos
                }
            }

            val currentFrame =
                frameTimeNanos

            if (
                currentFrame -
                lastCleanupNanos >=
                250_000_000L
            ) {
                lastCleanupNanos =
                    currentFrame

                activeRains.removeAll { rain ->
                    (
                            currentFrame -
                                    rain.startedAtNanos
                            ) /
                            1_000_000L >=
                            REACTION_RAIN_LIFETIME_MS
                }
            }
        }
    }

    Canvas(
        modifier =
            modifier.fillMaxSize()
    ) {
        drawReactionRains(
            rains =
                activeRains,
            frameTimeNanos =
                frameTimeNanos,
            spriteCache =
                spriteCache,
            bitmapPaint =
                bitmapPaint
        )
    }
}

@Composable
fun ReactionRain(
    reaction: GameReaction?,
    modifier: Modifier = Modifier
) {

    val activeReaction =
        reaction
            ?: return

    val rain =
        remember(
            activeReaction.id
        ) {
            createActiveReactionRain(
                activeReaction
            )
        }

    var frameTimeNanos by
    remember(
        activeReaction.id
    ) {
        mutableLongStateOf(
            System.nanoTime()
        )
    }

    val spriteCache =
        remember(
            activeReaction.id
        ) {
            EmojiSpriteCache()
        }

    val bitmapPaint =
        remember(
            activeReaction.id
        ) {
            Paint(
                Paint.ANTI_ALIAS_FLAG or
                        Paint.FILTER_BITMAP_FLAG
            ).apply {
                isDither =
                    true
            }
        }

    LaunchedEffect(
        activeReaction.id
    ) {

        var lastRenderNanos =
            0L

        while (
            (
                    frameTimeNanos -
                            rain.startedAtNanos
                    ) /
            1_000_000L <
            REACTION_RAIN_LIFETIME_MS
        ) {

            withFrameNanos { frameNanos ->

                if (
                    lastRenderNanos ==
                    0L ||
                    frameNanos -
                    lastRenderNanos >=
                    22_000_000L
                ) {
                    lastRenderNanos =
                        frameNanos
                    frameTimeNanos =
                        frameNanos
                }
            }
        }
    }

    Canvas(
        modifier =
            modifier.fillMaxSize()
    ) {
        drawReactionRains(
            rains =
                listOf(
                    rain
                ),
            frameTimeNanos =
                frameTimeNanos,
            spriteCache =
                spriteCache,
            bitmapPaint =
                bitmapPaint
        )
    }
}

@Composable
fun ReactionBar(
    gameId: String,
    senderName: String
) {

    val view =
        LocalView.current

    var lastSendAt by remember {
        mutableLongStateOf(0L)
    }

    var errorText by remember {
        mutableStateOf<String?>(
            null
        )
    }

    LaunchedEffect(
        errorText
    ) {
        if (
            errorText != null
        ) {
            delay(2_500L)
            errorText = null
        }
    }

    PokerPanel {

        Text(
            text =
                "Кинуть реакцию",
            color =
                PokerPalette.Gold,
            fontSize =
                15.sp,
            fontWeight =
                FontWeight.Bold
        )

        Spacer(
            modifier =
                Modifier.height(
                    10.dp
                )
        )

        Row(
            modifier =
                Modifier
                    .fillMaxWidth(),
            horizontalArrangement =
                Arrangement.SpaceEvenly,
            verticalAlignment =
                Alignment.CenterVertically
        ) {

            GAME_REACTION_EMOJIS
                .forEach { emoji ->

                    Box(
                        modifier =
                            Modifier
                                .size(
                                    58.dp
                                )
                                .border(
                                    width = 1.dp,
                                    color =
                                        PokerPalette
                                            .GoldDark,
                                    shape =
                                        CircleShape
                                )
                                .clickable {

                                    val now =
                                        System
                                            .currentTimeMillis()

                                    if (
                                        now -
                                        lastSendAt <
                                        REACTION_SEND_COOLDOWN_MS
                                    ) {
                                        return@clickable
                                    }

                                    lastSendAt =
                                        now

                                    view.performHapticFeedback(
                                        HapticFeedbackConstants
                                            .CLOCK_TICK
                                    )

                                    GameReactionRepository
                                        .sendReaction(
                                            gameId =
                                                gameId,
                                            emoji =
                                                emoji,
                                            senderName =
                                                senderName,
                                            onError = { message ->
                                                /*
                                                 * Пока тестируем реакции,
                                                 * показываем реальную причину сервер,
                                                 * а не безликое "не удалось".
                                                 */
                                                errorText =
                                                    message
                                            }
                                        )
                                },
                        contentAlignment =
                            Alignment.Center
                    ) {

                        Image(
                            painter =
                                painterResource(
                                    id =
                                        reactionIconRes(emoji)
                                ),
                            contentDescription =
                                null,
                            contentScale =
                                ContentScale.Fit,
                            modifier =
                                Modifier.size(
                                    44.dp
                                )
                        )
                    }
                }
        }

        if (
            errorText != null
        ) {

            Spacer(
                modifier =
                    Modifier.height(
                        8.dp
                    )
            )

            Text(
                text =
                    errorText!!,
                color =
                    PokerPalette.Danger,
                fontSize =
                    13.sp
            )
        }
    }
}