package theorycrafter.ui.fiteditor

import androidx.compose.runtime.Stable
import eve.data.ModuleSlotType
import eve.data.ModuleType
import theorycrafter.fitting.Fit
import theorycrafter.fitting.Module

/**
 * A group of module (and their respective charge) slots. We use its implementing types to represent regular slots
 * ([SingleModuleSlot]), and grouped "slots" ([MultiModuleSlot]) for turrets and launchers.
 */
@Stable
sealed class ModuleSlotGroup(val fit: Fit) {


    /**
     * The type of the slots in this group.
     */
    abstract val slotType: ModuleSlotType


    /**
     * The indices of the slots in this group, sorted in increasing order.
     */
    abstract val slotIndices: List<Int>


    /**
     * The index of the first slot in this group.
     */
    abstract val firstSlotIndex: Int


    /**
     * Returns all the modules in this group.
     */
    val allModules: List<Module?>
        get() = slotIndices.map { fit.modules.inSlot(slotType, it) }


    /**
     * Returns the representative module.
     */
    val repModule: Module?
        get() = fit.modules.inSlot(slotType, firstSlotIndex)


    /**
     * Returns the non-null representative module.
     */
    fun requireRepModule() = repModule ?: throw IllegalStateException("No representative module in group")


    /**
     * The number of slots in this group.
     */
    abstract val slotCount: Int


    /**
     * Returns whether this slot group also requires a charge slot to be displayed.
     */
    fun hasChargeSlot() = repModule.let {
        (it != null) && it.canLoadCharges
    }


    override fun toString() = buildString {
        if (this@ModuleSlotGroup is MultiModuleSlot){
            append(slotCount)
            append("x")
        }

        append(repModule ?: "Empty")
        append(" $slotIndices")
    }


    /**
     * Subclasses must implement [equals] and [hashCode] because the undo/redo functionality will be using old
     * instances of module slot groups, and the group-modifying functions ([addSlot], [removeSlot], [disbandGroup])
     * must be able to find them.
     */
    abstract override fun equals(other: Any?): Boolean


    abstract override fun hashCode(): Int


}


/**
 * Represents a single, ungrouped slot.
 */
@Stable
class SingleModuleSlot(
    fit: Fit,
    override val slotType: ModuleSlotType,
    val slotIndex: Int
): ModuleSlotGroup(fit) {

    override val slotIndices = listOf(slotIndex)

    override val firstSlotIndex: Int
        get() = slotIndex

    override val slotCount: Int
        get() = 1

    override fun equals(other: Any?): Boolean {
        if (this === other)
            return true
        if (javaClass != other?.javaClass)
            return false

        other as SingleModuleSlot

        return (fit == other.fit) && (slotIndex == other.slotIndex) && (slotType == other.slotType)
    }

    override fun hashCode(): Int {
        var result = fit.hashCode()
        result = 31 * result + slotType.hashCode()
        result = 31 * result + slotIndex
        return result
    }

}


/**
 * A group of slots (although it's possible for it to only have one slot).
 * Within a single group, all modules have to be
 * - actual, non-`null` modules,
 * - of the same type, and
 * - fit the same type of charge (or `null`).
 */
@Stable
class MultiModuleSlot(
    fit: Fit,
    override val slotType: ModuleSlotType,
    slotIndices: Collection<Int>
): ModuleSlotGroup(fit) {

    override val slotIndices = slotIndices.sorted()

    override val firstSlotIndex: Int
        get() = slotIndices.first()

    override val slotCount by slotIndices::size

    override fun equals(other: Any?): Boolean {
        if (this === other)
            return true
        if (javaClass != other?.javaClass)
            return false

        other as MultiModuleSlot

        return (fit == other.fit) && (slotIndices == other.slotIndices) && (slotType == other.slotType)
    }

    override fun hashCode(): Int {
        var result = fit.hashCode()
        result = 31 * result + slotType.hashCode()
        result = 31 * result + slotIndices.hashCode()
        return result
    }


}


/**
 * Returns the initial grouping of modules in the given rack (e.g. when opening a new fit, or turning on grouping).
 */
fun moduleGrouping(fit: Fit, slotType: ModuleSlotType, groupWeapons: Boolean): List<ModuleSlotGroup> {
    val slotCount = fit.fitting.slots[slotType]
    if ((slotType != ModuleSlotType.HIGH) || !groupWeapons)
        return (0 until slotCount).map { SingleModuleSlot(fit, slotType, it) }

    val slots = fit.modules.slotsLimitedByRackSize(slotType)

    return slots
        .mapIndexed { index, module ->
            index to module
        }
        .groupBy { (index, module) ->
            if ((module != null) && (module.type.usesTurretHardpoint || module.type.usesLauncherHardpoint))
                Triple(null, module.type, module.loadedCharge?.type)
            else
                Triple(index, null, null)
        }
        .entries
        .map { (key, group) ->
            val indices = group.map { it.first }.sorted()
            if (key.first == null){
                MultiModuleSlot(
                    fit = fit,
                    slotType = slotType,
                    slotIndices = indices
                )
            }
            else{
                SingleModuleSlot(
                    fit = fit,
                    slotType = slotType,
                    slotIndex = indices.first()
                )
            }

        }
        .sortedBy {
            it.slotIndices.first()
        }
}


/**
 * Returns the module group to create when the user asks to fit the given module type into the given single-module slot.
 */
fun updatedSlotGroupWhenFittingModule(
    fit: Fit,
    moduleType: ModuleType,
    currentGroup: ModuleSlotGroup,
    groupWeapons: Boolean
): ModuleSlotGroup {
    if (!groupWeapons || (!moduleType.usesLauncherHardpoint && !moduleType.usesTurretHardpoint)) {
        @Suppress("IfThenToElvis")
        return if (currentGroup is SingleModuleSlot)
            currentGroup
        else
            SingleModuleSlot(fit, moduleType.slotType, currentGroup.firstSlotIndex)
    }

    if (currentGroup !is SingleModuleSlot)  // For multi-slot groups, keep the existing one
        return currentGroup

    val targetSlotIndex = currentGroup.slotIndex
    val currentModule = fit.modules.inSlot(moduleType.slotType, targetSlotIndex)
    val slots = fit.modules.slotsLimitedByRackSize(moduleType.slotType).toList()

    /**
     * Returns the indices of the slots into which to fit, given the limit definition.
     */
    fun fittableSlotIndices(limit: Int, countsTowardsLimit: (ModuleType) -> Boolean): Collection<Int> {

        // When replacing an existing weapon, replace all the already-fitted weapons of that type
        if ((currentModule != null) && countsTowardsLimit(currentModule.type)) {
            return slots.mapIndexedNotNull { slotIndex, module ->
                if (module?.type == currentModule.type) slotIndex else null
            }
        }

        // Otherwise, fit into empty slots up to the limit
        val usedCount = slots.count { module ->
            (module != null) && countsTowardsLimit(module.type)
        }
        val addLimit = limit - usedCount  // The maximum number of modules we can add
        if (addLimit == 0)
            return emptyList()

        return buildList(addLimit) {
            add(targetSlotIndex)  // Always add the target slot index

            if (size == addLimit)
                return@buildList

            // First use slots below the target
            for (slotIndex in targetSlotIndex + 1 .. slots.lastIndex) {
                if (slots[slotIndex] == null) {
                    add(slotIndex)
                    if (size == addLimit)
                        return@buildList
                }
            }

            // Then use slots above the target
            // This algorithm is duplicated by [findEmptySlotForAddingToGroup] function
            for (slotIndex in targetSlotIndex - 1 downTo 0) {
                if (slots[slotIndex] == null) {
                    add(slotIndex)
                    if (size == addLimit)
                        return@buildList
                }
            }
        }
    }

    fun groupModules(limit: Int, countsTowardsLimit: (ModuleType) -> Boolean): ModuleSlotGroup {
        val indices = fittableSlotIndices(
            limit = limit,
            countsTowardsLimit = countsTowardsLimit,
        )
        return if (indices.isEmpty())
            currentGroup
        else
            MultiModuleSlot(
                fit = fit,
                slotType = currentGroup.slotType,
                slotIndices = indices
            )
    }

    return when {
        moduleType.usesTurretHardpoint -> groupModules(
            limit = fit.ship.turretHardpoints.value,
            countsTowardsLimit = { it.usesTurretHardpoint },
        )
        moduleType.usesLauncherHardpoint -> groupModules(
            limit = fit.ship.launcherHardpoints.value,
            countsTowardsLimit = { it.usesLauncherHardpoint },
        )
        else -> currentGroup
    }
}


/**
 * Returns the index of the slot to add to an existing slot group, or when duplicating an item, when the user asks to
 * increase the number of modules in it.
 */
fun findEmptySlotForAddingToGroup(fit: Fit, slotType: ModuleSlotType, groupSlotIndex: Int): Int? {
    return addAnotherTargetIndex(
        slots = fit.modules.slotsLimitedByRackSize(slotType).toList(),
        selectedIndex = groupSlotIndex
    )
}


/**
 * Returns the index of the slot to remove from the group when the user asks to decrease the number of modules in it.
 */
fun slotToClearWhenRemovingFromGroup(slotGroup: MultiModuleSlot): Int {
    return slotGroup.slotIndices.last()
}


/**
 * Returns the index of the slot to clear when removing an item of the type at the given slot.
 * This is used when pressing "minus" on a single module slot.
 */
fun slotToClearWhenRemovingSameItem(fit: Fit, slotType: ModuleSlotType, slotIndex: Int): Int? {
    return removeSameItemTargetIndex(
        slots = fit.modules.slotsLimitedByRackSize(slotType).toList(),
        selectedIndex = slotIndex,
        isSameItem = { m1, m2 -> m1.type == m2.type }
    )
}


/**
 * Returns the index of a slot into which an "add another" action should add the new item.
 */
fun <T: Any> addAnotherTargetIndex(slots: List<T?>, selectedIndex: Int): Int? {
    // If there's an empty index below the slot group's first index, select that; if not, look above.
    // This algorithm is duplicated by [newSlotGroupWhenFittingModule]
    val emptySlotIndices = slots.indices.filter { slots[it] == null }
    return emptySlotIndices.firstOrNull { it > selectedIndex }
        ?: emptySlotIndices.lastOrNull { it < selectedIndex }
}


/**
 * Returns the index of the slot to clear by "remove one" action.
 */
fun <T: Any> removeSameItemTargetIndex(slots: List<T?>, selectedIndex: Int, isSameItem: (T, T) -> Boolean): Int? {
    // Do the reverse of addAnotherTargetIndex
    val selectedItem = slots[selectedIndex]!!
    val sameItemIndices = slots.indices.filter {
        val item = slots[it]
        (item != null) && isSameItem(item, selectedItem)
    }
    return sameItemIndices.firstOrNull { it < selectedIndex }
        ?: sameItemIndices.lastOrNull { it > selectedIndex }
}


/**
 * Returns the new list of groups in the rack after assembling the given group.
 */
fun assembleGroup(
    currentGrouping: List<ModuleSlotGroup>,
    newGroup: MultiModuleSlot
) = buildList(currentGrouping.size - newGroup.slotCount + 1) {
    for (existingGroup in currentGrouping) {
        if ((existingGroup !is SingleModuleSlot) || !newGroup.slotIndices.contains(existingGroup.slotIndex)) {
            add(existingGroup)
        }
    }
    add(newGroup)
    sortBy { it.slotIndices.first() }
}


/**
 * Returns the new list of groups in the rack after disbanding the given group.
 */
fun disbandGroup(
    fit: Fit,
    slotType: ModuleSlotType,
    currentGrouping: List<ModuleSlotGroup>,
    removedGroup: MultiModuleSlot
) = buildList(currentGrouping.size - 1 + removedGroup.slotCount) {
    if (removedGroup !in currentGrouping)
        throw IllegalArgumentException("Group to remove is not in current groups")

    for (existingGroup in currentGrouping) {
        if (existingGroup != removedGroup)
            add(existingGroup)
    }
    for (slotIndex in removedGroup.slotIndices) {
        add(
            SingleModuleSlot(
                fit = fit,
                slotType = slotType,
                slotIndex = slotIndex
            )
        )
    }
    sortBy { it.slotIndices.first() }
}


/**
 * Returns the new list of groups in the rack after adding the given slot to the given group, and the new (updated)
 * group itself.
 */
fun addSlot(
    fit: Fit,
    slotType: ModuleSlotType,
    currentGrouping: List<ModuleSlotGroup>,
    group: MultiModuleSlot,
    slotIndex: Int
): Pair<List<ModuleSlotGroup>, MultiModuleSlot> {
    if (group !in currentGrouping)
        throw IllegalArgumentException("Group to add to is not in current groups")

    val updatedGroup = MultiModuleSlot(fit, slotType, group.slotIndices.plus(slotIndex))
    val updatedGroups = buildList(currentGrouping.size-1) {
        for (existingGroup in currentGrouping) {
            if (existingGroup == group)
                add(updatedGroup)
            else if ((existingGroup !is SingleModuleSlot) || (existingGroup.slotIndex != slotIndex))
                add(existingGroup)
        }
        sortBy { it.slotIndices.first() }
    }
    return updatedGroups to updatedGroup
}


/**
 * Returns the new list of groups in the rack after removing the given slot from the given group, and the new (updated)
 * group itself.
 */
fun removeSlot(
    fit: Fit,
    slotType: ModuleSlotType,
    currentGrouping: List<ModuleSlotGroup>,
    group: MultiModuleSlot,
    slotIndex: Int
): Pair<List<ModuleSlotGroup>, MultiModuleSlot> {
    if (group !in currentGrouping)
        throw IllegalArgumentException("Group to remove from is not in current groups")

    val updatedGroup = MultiModuleSlot(fit, slotType, group.slotIndices.minus(slotIndex))
    val updatedGroups = buildList(currentGrouping.size+1) {
        for (existingGroup in currentGrouping) {
            if (existingGroup == group){
                add(updatedGroup)
                add(SingleModuleSlot(fit, slotType, slotIndex))
            }
            else
                add(existingGroup)
        }
        sortBy { it.slotIndices.first() }
    }
    return updatedGroups to updatedGroup
}


/**
 * The actions that can be performed on the [ModuleSlotGroup]s within a rack.
 */
abstract class RackSlotGroupingActions {


    /**
     * Returns an [FitEditorUndoRedoAction] that assembles the given group.
     */
    abstract fun assembleGroupAction(group: MultiModuleSlot): FitEditorUndoRedoAction


    /**
     * Returns an [FitEditorUndoRedoAction] that disbands the given group.
     */
    abstract fun disbandGroupAction(group: MultiModuleSlot): FitEditorUndoRedoAction


    /**
     * Returns an [FitEditorUndoRedoAction] that adds a slot to the given group.
     */
    abstract fun addSlotAction(group: MultiModuleSlot, slotIndex: Int): FitEditorUndoRedoAction


    /**
     * Returns an [FitEditorUndoRedoAction] that removes a slot from the given group.
     */
    abstract fun removeSlotAction(group: MultiModuleSlot, slotIndex: Int): FitEditorUndoRedoAction


}
