package theorycrafter.tournaments

import androidx.compose.runtime.*
import eve.data.EveData
import eve.data.ShipType
import theorycrafter.TheorycrafterContext
import theorycrafter.tournaments.StoredComposition.StoredCompositionShip
import theorycrafter.utils.onSet
import theorycrafter.utils.swap
import kotlin.math.max


/**
 * A composition of ships for a tournament.
 */
@Stable
class Composition(


    /**
     * The tournament this composition is part of.
     */
    val tournament: Tournament,


    /**
     * The id of this composition.
     */
    val id: Int,


    /**
     * The name of this composition.
     */
    name: String,


    /**
     * The note of this composition.
     */
    note: String


) {


    /**
     * Whether this composition has been deleted; a Compose state.
     */
    var deleted: Boolean by mutableStateOf(false)


    /**
     * The name of the composition.
     *
     * This value is Compose State, and setting it causes it to be written to disk.
     */
    var name by mutableStateOf(name).onSet(::updateInRepo)


    /**
     * The (mutable) list of ships in the composition, for internal use.
     */
    private val mutableShips = mutableStateListOf<Ship?>()


    /**
     * The list of ships in the composition.
     *
     * Note that this is a Compose State.
     */
    val ships: List<Ship?>
        get() = mutableShips


    /**
     * The composition notes.
     */
    var note by mutableStateOf(note).onSet(::updateInRepo)


    /**
     * Updates the composition stored in the repository.
     */
    private fun updateInRepo(@Suppress("UNUSED_PARAMETER") arg: Any? = Unit) {
        tournament.repository.updateComposition(id, this.toStoredComposition())
    }


    /**
     * Returns the ship at the given index.
     */
    operator fun get(index: Int): Ship? {
        return mutableShips.getOrNull(index)
    }


    /**
     * Trims `null` ships the end.
     */
    private fun trimNullsFromEnd() {
        while ((mutableShips.isNotEmpty()) && (mutableShips.last() == null))
            mutableShips.removeLast()
    }


    /**
     * Pads `null` ships at the end until the composition size is the given one.
     */
    private fun padEnd(targetSize: Int) {
        while (mutableShips.size < targetSize)
            mutableShips.add(null)
    }


    /**
     * Runs the given block, then calls [trimNullsFromEnd] and [updateInRepo].
     */
    private inline fun modifyShipsAndSave(block: () -> Unit) {
        try {
            block()
        } finally {
            trimNullsFromEnd()
            updateInRepo()
        }
    }


    /**
     * Sets the ship at the given index.
     */
    operator fun set(index: Int, ship: Ship?) {
        modifyShipsAndSave {
            if (ship != null) {
                if (ship.composition != this)
                    throw IllegalArgumentException("$ship belongs to a different composition")
                while (mutableShips.lastIndex < index)
                    mutableShips.add(null)
            }
            mutableShips[index] = ship
        }
    }


    /**
     * Returns the number of slots in the composition, including empty ones.
     */
    val size: Int
        get() = mutableShips.size


    /**
     * Inserts a ship at the given index.
     */
    fun insert(index: Int, ship: Ship?) {
        modifyShipsAndSave {
            mutableShips.add(index, ship)
        }
    }


    /**
     * Removes the slot at the given index.
     */
    fun removeAt(index: Int) {
        modifyShipsAndSave {
            if (index < mutableShips.size)
                mutableShips.removeAt(index)
        }
    }


    /**
     * Swaps the ships at the two indices.
     */
    fun swap(index1: Int, index2: Int) {
        modifyShipsAndSave {
            padEnd(max(index1, index2) + 1)
            mutableShips.swap(index1, index2)
        }
    }


    /**
     * Moves the ship at the given index to the given index.
     */
    fun move(fromIndex: Int, toIndex: Int) {
        if (fromIndex == toIndex)
            return

        modifyShipsAndSave {
            val item = mutableShips.removeAt(fromIndex)
            padEnd(toIndex)
            mutableShips.add(index = toIndex, element = item)
        }
    }


    /**
     * Returns a [StoredComposition] for this composition.
     */
    private fun toStoredComposition(id: Int? = this.id, name: String = this.name): StoredComposition {
        return StoredComposition(
            id = id,
            name = name,
            ships = mutableShips.map { ship ->
                ship?.let { StoredCompositionShip(it) }
            },
            note = note
        )
    }


    /**
     * Returns a [StoredComposition] for this composition, but with a `null` id.
     */
    fun toStoredCompositionDuplicate(name: String): StoredComposition {
        return toStoredComposition(id = null, name = name)
    }


    /**
     * A ship in a composition.
     */
    @Stable
    class Ship(


        /**
         * The composition this ship is part of.
         */
        val composition: Composition,


        /**
         * The ship type.
         */
        val shipType: ShipType,


        /**
         * The amount of this ship in the composition.
         */
        amount: Int?,


        /**
         * The id of the fit associated with this composition ship.
         */
        fitId: Int?,


        /**
         * Whether the ship is active in the composition.
         */
        active: Boolean


    ) {


        /**
         * The amount of this ship in the composition; `null` if the corresponding tournament implies one ship per slot.
         */
        var amount: Int? by mutableStateOf(amount).onSet(composition::updateInRepo)


        /**
         * The amount of this ship in the composition, or 1 if the corresponding tournament implies one ship per slot.
         */
        val amountOrOne: Int
            get() = amount ?: 1


        /**
        * The id of the fit associated with this composition ship.
         */
        var fitId: Int? by mutableStateOf(fitId).onSet(composition::updateInRepo)


        /**
         * Whether this ship is "active" in the composition.
         */
        var active: Boolean by mutableStateOf(active).onSet(composition::updateInRepo)


        /**
         * Returns a copy of this composition ship.
         */
        fun copy(): Ship = Ship(
            composition = composition,
            shipType = shipType,
            amount = amount,
            fitId = fitId,
            active = active
        )


    }


    companion object {


        /**
         * Creates a [Composition] for the given tournament, from the given [StoredComposition].
         */
        fun fromStored(tournament: Tournament, storedComposition: StoredComposition): Composition {
            return Composition(
                tournament = tournament,
                id = storedComposition.id ?: error("Stored composition must have an id"),
                name = storedComposition.name,
                note = storedComposition.note,
            ).apply {
                for (storedShip in storedComposition.ships) {
                    mutableShips.add(
                        shipFromStored(
                            tournament.eveData,
                            composition = this,
                            storedShip = storedShip
                        )
                    )
                }
            }
        }


        /**
         * Creates a [Composition.Ship] from the given [StoredCompositionShip], to be part of the given composition.
         */
        private fun shipFromStored(
            eveData: EveData,
            composition: Composition,
            storedShip: StoredCompositionShip?
        ): Ship? {
            if (storedShip == null)
                return null

            return Ship(
                composition = composition,
                shipType = eveData.shipType(storedShip.shipTypeId),
                amount = storedShip.amount,
                fitId = storedShip.fitId?.takeIf {
                    TheorycrafterContext.fits.handleById(it) != null // Ignore the fit id if it has been deleted
                },
                active = storedShip.active
            )
        }


    }


}
