/**
 * Exporting and import fits in EFT format.
 */

package theorycrafter.formats

import eve.data.*
import eve.data.utils.ValueByEnum
import eve.data.utils.forEach
import eve.data.utils.valueByEnum
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import theorycrafter.fits.defaultSubsystems
import theorycrafter.fits.defaultTacticalMode
import theorycrafter.fitting.Module
import theorycrafter.storage.StoredFit
import theorycrafter.storage.StoredFit.*


/**
 * Returns the EFT format of the given fit.
 */
context(EveData)
fun StoredFit.toEft(options: EftExportOptions = EftExportOptions.IncludeAll): String {
    val mutations = mutableListOf<EftMutationDescriptor>()

    // Appends a module section
    fun StringBuilder.appendModuleRack(rack: ModuleSlotType, slots: List<StoredModule?>) {
        for (module in slots) {
            if (module == null) {
                appendLine(rack.emptySlot())
                continue
            }

            val baseType = moduleType(module.itemId)
            append(baseType.name)

            if (options.loadedCharges) {
                val charge = module.chargeId?.let { chargeType(it) }
                if (charge != null) {
                    append(", ")
                    append(charge.name)
                }
            }

            if (module.state == Module.State.OFFLINE)
                append(OfflineMarker)

            if (options.mutatedAttributes && (module.mutation != null)) {
                mutations.add(EftMutationDescriptor(module))
                append(" [${mutations.size}]")
            }
            appendLine()
        }
        appendLine()
    }

    // Appends a subsystem section
    fun StringBuilder.appendSubsystems(subsystems: ValueByEnum<SubsystemType.Kind, StoredSubsystem>?) {
        if (subsystems == null)
            return

        subsystems.forEach { _, storedSubsystem ->
            val subsystemType = subsystemType(storedSubsystem.itemId)
            appendLine(subsystemType.name)
        }
        appendLine()
    }

    // Appends a drone section
    fun StringBuilder.appendDrones(drones: Collection<StoredDroneGroup>) {
        for (droneGroup in drones) {
            val baseType = droneType(droneGroup.itemId)
            append("${baseType.name} x${droneGroup.size}")

            if (options.mutatedAttributes && (droneGroup.mutation != null)) {
                mutations.add(EftMutationDescriptor(droneGroup))
                append(" [${mutations.size}]")
            }
            appendLine()
        }
        if (drones.isNotEmpty())
            appendLine()
    }

    // Appends an implant section
    fun StringBuilder.appendImplants(implants: Collection<StoredImplant>) {
        for (implant in implants) {
            val implantType = implantTypes[implant.itemId]
            appendLine(implantType.name)
        }
        if (implants.isNotEmpty())
            appendLine()
    }

    // Appends a booster section
    fun StringBuilder.appendBoosters(boosters: Collection<StoredBooster>) {
        for (booster in boosters) {
            val boosterType = boosterType(booster.itemId)
            appendLine(boosterType.name)
        }
        if (boosters.isNotEmpty())
            appendLine()
    }

    // Appends a cargo section
    fun StringBuilder.appendCargo(cargoItems: Collection<StoredCargoItem>) {
        for (cargoItem in cargoItems) {
            val itemType = cargoItemType(cargoItem.itemId)
            appendLine("${itemType.name} x${cargoItem.amount}")
        }
        if (cargoItems.isNotEmpty())
            appendLine()
    }

    // Appends mutated item descriptions
    fun StringBuilder.appendMutatedItemDescriptions() {
        if (mutations.isEmpty())
            return

        appendLine()
        for ((index, mutationDesc) in mutations.withIndex()) {
            append("[${index+1}] ")
            append(mutationDesc.toString())
            appendLine()
        }
    }

    return buildString {
        val shipType = shipType(shipTypeId)
        appendLine("[${shipType.name}, $name]")
        appendLine()
        appendModuleRack(ModuleSlotType.LOW, lowSlotRack)
        appendModuleRack(ModuleSlotType.MEDIUM, medSlotRack)
        appendModuleRack(ModuleSlotType.HIGH, highSlotRack)
        appendModuleRack(ModuleSlotType.RIG, rigs)
        appendSubsystems(subsystems)
        appendLine()
        appendDrones(droneGroups)
        if (options.implants)
            appendImplants(implants)
        if (options.boosters)
            appendBoosters(boosters)
        if (options.cargo)
            appendCargo(cargoItems)
        if (options.mutatedAttributes)
            appendMutatedItemDescriptions()
    }.trimEnd()
}


/**
 * EFT export options.
 */
@Serializable
data class EftExportOptions(
    @SerialName("charges") val loadedCharges: Boolean = true,
    @SerialName("implants") val implants: Boolean = true,
    @SerialName("boosters") val boosters: Boolean = true,
    @SerialName("cargo") val cargo: Boolean = true,
    @SerialName("mutation") val mutatedAttributes: Boolean = true
) {


    companion object {

        /**
         * The EFT export options for including all the parts of the fit.
         */
        val IncludeAll = EftExportOptions(
            loadedCharges = true,
            implants = true,
            boosters = true,
            cargo = true,
            mutatedAttributes = true
        )

    }

}


/**
 * Returns the name of an empty in EFT format.
 */
fun ModuleSlotType.emptySlot() = when (this) {
    ModuleSlotType.HIGH -> "[Empty High slot]"
    ModuleSlotType.MEDIUM -> "[Empty Med slot]"
    ModuleSlotType.LOW -> "[Empty Low slot]"
    ModuleSlotType.RIG -> "[Empty Rig slot]"
}


/**
 * Holds an item and its possible index in the mutation addendum.
 */
private data class PossiblyMutated<T>(
    val item: T,
    val mutationIndex: Int?
)


/**
 * Parses the given EFT-formatted string into a [StoredFit].
 * Returns `null` if the given string doesn't correspond to a valid fit.
 */
context(EveData)
fun fitFromEft(text: String): StoredFit? {
    var mutationCount = 0

    // Reads the ship type and fit name.
    fun ArrayDeque<String>.readShipTypeAndFitName(): Pair<ShipType, String> {
        while (isNotEmpty()) {
            val line = removeFirst()
            if (line.isBlank())
                continue
            val (shipTypeName, fitName) = line.substring(1, line.length - 1).split(", ")
            val shipType = shipTypeOrNull(shipTypeName)
                ?: throw IllegalArgumentException("Unknown ship type: $shipTypeName")
            return Pair(shipType, fitName)
        }

        throw IllegalArgumentException("Missing ship type and fit name")
    }

    // Reads a module/rig rack, including the blank line at the end
    // expectedSize is the size or the rack; `null` for strategic cruisers
    fun ArrayDeque<String>.readModuleRack(slotType: ModuleSlotType, expectedSize: Int?): List<PossiblyMutated<StoredModule>?> {
        val modules = ArrayList<PossiblyMutated<StoredModule>?>(expectedSize ?: slotType.maxSlotCount)
        fun appendModule(module: StoredModule?, mutationIndex: Int?) {
            modules.add(module?.let { PossiblyMutated(it, mutationIndex) })
            // Only when the module is successfully processed, increase the mutation count.
            // Otherwise we could end up increasing the count for a module in the next rack
            if (mutationIndex != null)
                mutationCount += 1
        }

        while (isNotEmpty()) {
            val originalLine = removeFirst()
            var line = originalLine
            if (line.isBlank())
                continue

            // [Empty X slot]
            if (line.startsWith("[Empty")) {
                if (line == slotType.emptySlot()) {
                    modules.add(null)
                    continue
                }
                else {
                    addFirst(originalLine)
                    break
                }
            }

            var mutationIndex: Int? = null
            val mutationIndexString = " [${mutationCount+1}]"
            if (line.endsWith(mutationIndexString)) {
                mutationIndex = mutationCount
                // mutationIndex will be bumped only if the module line is not pushed back into the deque
                line = line.take(line.length - mutationIndexString.length)
            }

            val isOffline = line.endsWith(OfflineMarker, ignoreCase = true)
            if (isOffline)
                line = line.take(line.length - OfflineMarker.length)

            // [Module Name, Charge Name] or just [Module Name]
            val (moduleName, chargeName) = "$line,".split(",").map { it.trim() }
            val moduleType = moduleTypeOrNull(moduleName)
            if (moduleType == null) {
                if (modules.size < (expectedSize ?: slotType.maxSlotCount)) {
                    appendModule(null, mutationIndex)
                    continue
                }
                else {
                    addFirst(originalLine)
                    break
                }
            }
            if (moduleType.slotType != slotType) {
                addFirst(originalLine)
                break
            }
            val chargeType = if (chargeName.isNotEmpty())
                chargeTypeOrNull(chargeName)
            else
                null

            val module = StoredModule(
                itemId = moduleType.itemId,
                enabled = if (moduleType.isRig) !isOffline else true,
                state = moduleType.importedState(isOffline),
                mutation = null,
                chargeId = chargeType?.itemId,
                extraAttributes = moduleType.importedExtraAttributes()
            )

            appendModule(module, mutationIndex)
        }

        return modules
    }

    // Reads the subsystems section, including a blank line at the end
    fun ArrayDeque<String>.readSubsystems(shipType: ShipType): ValueByEnum<SubsystemType.Kind, StoredSubsystem> {
        val subsystemTypeByKind = mutableMapOf<SubsystemType.Kind, SubsystemType>()
        while (isNotEmpty()) {
            val line = removeFirst()
            if (line.isBlank())
                continue
            if (line == "[Empty Subsystem slot]")
                continue

            val subsystemType = subsystemTypeOrNull(line)
            if (subsystemType == null) {
                addFirst(line)
                break
            }
            subsystemTypeByKind[subsystemType.kind] = subsystemType

            if (subsystemTypeByKind.size == SubsystemType.Kind.entries.size) {
                removeFirstOrNull()  // Read the following blank line
                break
            }
        }

        val defaultSubsystemTypeByKind by lazy { shipType.defaultSubsystems() }
        return valueByEnum {
            val subsystemType = subsystemTypeByKind[it] ?: defaultSubsystemTypeByKind[it]
            StoredSubsystem(subsystemType.itemId)
        }
    }

    // Reads the drones section
    fun ArrayDeque<String>.readDrones(): List<PossiblyMutated<StoredDroneGroup>> {
        val drones = mutableListOf<PossiblyMutated<StoredDroneGroup>>()
        fun appendDrone(drone: StoredDroneGroup, mutationIndex: Int?) {
            drones.add(PossiblyMutated(drone, mutationIndex))
            // Only when the drone is successfully processed, increase the mutation count.
            // Otherwise we could end up increasing the count for a something other than a drone
            if (mutationIndex != null)
                mutationCount += 1
        }

        var skippedEmptyLines = 0
        while (isNotEmpty()) {
            val line = removeFirst()
            if (line.isBlank()) {
                // Because the cargo section could be next and it can contain drones, and in the same format as a drone,
                // we need to rely on two empty lines to tell the two sections apart.
                skippedEmptyLines += 1
                if (skippedEmptyLines == 2)
                    break
                else
                    continue
            }
            skippedEmptyLines = 0

            // Parse and remove the mutation index
            var mutationIndex: Int? = null
            val mutationIndexString = " [${mutationCount+1}]"
            val droneTypeAndAmountString: String
            if (line.endsWith(mutationIndexString)) {
                mutationIndex = mutationCount
                // mutationIndex will be bumped only if the drone line is not pushed back into the deque
                droneTypeAndAmountString = line.dropLast(mutationIndexString.length)
            }
            else
                droneTypeAndAmountString = line

            // Drone Name xAmount
            val indexOfX = droneTypeAndAmountString.lastIndexOf('x')
            if (indexOfX == -1) {
                addFirst(line)
                break
            }

            val droneName = droneTypeAndAmountString.take(indexOfX - 1)
            val amount = droneTypeAndAmountString.substring(indexOfX + 1).toIntOrNull()
            val droneType = droneTypeOrNull(droneName)
            if ((amount == null) || (droneType == null)) {
                addFirst(line)
                break
            }

            val droneGroup = StoredDroneGroup(
                itemId = droneType.itemId,
                size = amount,
                active = drones.isEmpty(),  // Make the first group active
                mutation = null
            )

            appendDrone(droneGroup, mutationIndex)
        }

        return drones
    }

    // Reads the implants section
    fun ArrayDeque<String>.readImplants(): List<StoredImplant> {
        val implants = mutableListOf<StoredImplant>()
        while (isNotEmpty()) {
            val line = removeFirst()
            if (line.isBlank())
                continue
            val implantType = implantTypes.getOrNull(line)
            if (implantType == null) {
                addFirst(line)
                break
            }
            implants.add(StoredImplant(implantType.itemId, enabled = true))
        }

        return implants
    }

    // Reads the boosters section
    fun ArrayDeque<String>.readBoosters(): List<StoredBooster> {
        val boosters = mutableListOf<StoredBooster>()
        while (isNotEmpty()) {
            val line = removeFirst()
            if (line.isBlank())
                continue
            val boosterType = boosterTypeOrNull(line)
            if (boosterType == null) {
                addFirst(line)
                break
            }
            boosters.add(
                StoredBooster(
                    itemId = boosterType.itemId,
                    enabled = true,
                    activeSideEffectPenalizedAttributeIds = emptyList()
                )
            )
        }

        return boosters
    }

    // Reads the cargo section
    fun ArrayDeque<String>.readCargoItems(): List<StoredCargoItem> {
        val cargoItems by lazy { mutableListOf<StoredCargoItem>() }
        while (isNotEmpty()) {
            val line = removeFirst()
            if (line.isBlank())
                continue

            // Item Name xAmount
            val indexOfX = line.lastIndexOf('x')
            if (indexOfX == -1) {
                addFirst(line)
                break
            }

            val itemName = line.take(indexOfX - 1)
            val amount = line.substring(indexOfX + 1).toIntOrNull()
            val cargoItemType = cargoItemTypeOrNull(itemName)
            if ((amount == null) || (cargoItemType == null)) {
                addFirst(line)
                break
            }

            cargoItems.add(
                StoredCargoItem(
                    itemId = cargoItemType.itemId,
                    amount = amount
                )
            )
        }

        return cargoItems
    }

    // Reads the mutations section; returns null if the section is not the mutations section
    fun ArrayDeque<String>.readMutations(): List<EftMutationDescriptor>? {
        val mutations = mutableListOf<EftMutationDescriptor>()
        while (isNotEmpty()) {
            val mutationDescLines = buildList(3) {
                val input = this@readMutations
                while (input.isNotEmpty() && (size < 3)) {
                    val line = input.removeFirst()
                    if (line.isBlank())
                        continue
                    else
                        add(line)
                }
            }
            if (mutationDescLines.size != 3)
                break

            val indexText = "[${mutations.size+1}] "
            if (!mutationDescLines[0].startsWith(indexText))
                break

            val mutationDesc = mutationDescLines.toEftMutationDescriptor()
            if (mutationDesc == null)
                break

            mutations.add(mutationDesc)
        }

        return mutations.ifEmpty { null }
    }

    fun StoredModule.mutatedWith(mutationDesc: EftMutationDescriptor): StoredModule {
        val mutaplasmid = mutaplasmidOrNull(mutationDesc.mutaplasmidName) ?: return this
        return StoredModule(
            itemId = itemId,
            enabled = enabled,
            state = state,
            chargeId = chargeId,
            extraAttributes = extraAttributes,
            mutation = StoredMutation(
                mutaplasmidId = mutaplasmid.id,
                attributeIdsAndValues = mutationDesc.attributeNamesAndValues.mapNotNull { (attrName, value) ->
                    val attribute = attributes[attrName] ?: return@mapNotNull null
                    Pair(attribute.id, value)
                }
            )
        )
    }

    fun StoredDroneGroup.mutatedWith(mutationDesc: EftMutationDescriptor): StoredDroneGroup {
        val mutaplasmid = mutaplasmidOrNull(mutationDesc.mutaplasmidName) ?: return this
        return StoredDroneGroup(
            itemId = itemId,
            size = size,
            active = active,
            mutation = StoredMutation(
                mutaplasmidId = mutaplasmid.id,
                attributeIdsAndValues = mutationDesc.attributeNamesAndValues.mapNotNull { (attrName, value) ->
                    val attribute = attributes[attrName] ?: return@mapNotNull null
                    Pair(attribute.id, value)
                }
            )
        )
    }

    fun List<PossiblyMutated<StoredModule>?>.toStoredModules(mutations: List<EftMutationDescriptor>?): List<StoredModule?> {
        return map {
            if (it == null)
                return@map null

            val (module, mutationIndex) = it
            if ((mutationIndex == null) || (mutations == null))
                return@map module

            val mutationDesc = mutations.getOrNull(mutationIndex) ?: return@map module
            module.mutatedWith(mutationDesc)
        }
    }

    fun List<PossiblyMutated<StoredDroneGroup>>.toStoredDrones(mutations: List<EftMutationDescriptor>?): List<StoredDroneGroup> {
        return map {
            val (droneGroup, mutationIndex) = it
            if ((mutationIndex == null) || (mutations == null))
                return@map droneGroup

            val mutationDesc = mutations.getOrNull(mutationIndex) ?: return@map droneGroup
            droneGroup.mutatedWith(mutationDesc)
        }
    }

    return text.reader().useLines { lines ->
        ArrayDeque(lines.toList()).runCatching {
            val (shipType, fitName) = readShipTypeAndFitName()

            val usesSubsystems = shipType.usesSubsystems
            val lowSlotRack = readModuleRack(ModuleSlotType.LOW, shipType.fitting.slots.low.takeUnless { usesSubsystems })
            val medSlotRack = readModuleRack(ModuleSlotType.MEDIUM, shipType.fitting.slots.med.takeUnless { usesSubsystems })
            val highSlotRack = readModuleRack(ModuleSlotType.HIGH, shipType.fitting.slots.high.takeUnless { usesSubsystems })
            val rigs = readModuleRack(ModuleSlotType.RIG, shipType.fitting.slots.rig)

            val subsystems = if (shipType.usesSubsystems) readSubsystems(shipType) else null
            val drones = readDrones()
            val implants = readImplants()
            val boosters = readBoosters()
            val cargoItems = readCargoItems()
            val mutations = readMutations()

            StoredFit.new(
                name = fitName,
                shipType = shipType,
                tacticalMode = shipType.defaultTacticalMode()?.let(::StoredTacticalMode),
                subsystems = subsystems,
                highSlotRack = highSlotRack.toStoredModules(mutations),
                medSlotRack = medSlotRack.toStoredModules(mutations),
                lowSlotRack = lowSlotRack.toStoredModules(mutations),
                rigs = rigs.toStoredModules(null),
                droneGroups = drones.toStoredDrones(mutations),
                cargoItems = cargoItems,
                implants = implants,
                boosters = boosters,
            )
        }.getOrNull()
    }
}


/**
 * The suffix marking an offline module.
 */
private const val OfflineMarker = " /offline"



/**
 * An item [Mutation] descriptor.
 */
class EftMutationDescriptor(
    val baseTypeName: String,
    val mutaplasmidName: String,
    val attributeNamesAndValues: List<Pair<String, Double>>
) {

    override fun toString() = buildString {
        appendLine(baseTypeName)
        append("  ")
        appendLine(mutaplasmidName)
        append("  ")
        append(
            attributeNamesAndValues.joinToString(separator = ", ") { (attrName, value) ->
                "$attrName ${value.toDecimalWithSignificantDigitsAtMost(5)}"
            }
        )
    }

}


/**
 * Convenience method to create an [EftMutationDescriptor] from stored module/drone.
 */
context(EveData)
private fun EftMutationDescriptor(
    baseType: EveItemType,
    mutation: StoredMutation
): EftMutationDescriptor {
    val mutaplasmid = mutaplasmid(mutation.mutaplasmidId)
    return EftMutationDescriptor(
        baseTypeName = baseType.name,
        mutaplasmidName = mutaplasmid.name,
        attributeNamesAndValues = mutation.attributeIdsAndValues.map { (attrId, value) ->
            val attribute = attributes[attrId]
            Pair(attribute.name, value)
        }
    )
}


/**
 * Returns an [EftMutationDescriptor] for the given [StoredModule].
 */
context(EveData)
private fun EftMutationDescriptor(storedModule: StoredModule): EftMutationDescriptor {
    val mutation = storedModule.mutation ?: throw IllegalArgumentException("$storedModule is not mutated")
    return EftMutationDescriptor(
        baseType = moduleType(storedModule.itemId),
        mutation = mutation
    )
}


/**
 * Returns an [EftMutationDescriptor] for the given [StoredDroneGroup].
 */
context(EveData)
private fun EftMutationDescriptor(storedDroneGroup: StoredDroneGroup): EftMutationDescriptor {
    val mutation = storedDroneGroup.mutation ?: throw IllegalArgumentException("$storedDroneGroup is not mutated")
    return EftMutationDescriptor(
        baseType = droneType(storedDroneGroup.itemId),
        mutation = mutation
    )
}


/**
 * Decodes an EFT-format mutated type description. Returns `null` if the string is not in the correct format.
 */
fun String.toEftMutationDescriptor(): EftMutationDescriptor? {
    return split("\n", "\r").toEftMutationDescriptor()
}


/**
 * Decodes the given lines as an EFT-format mutated type description.
 *
 * Returns `null` if the string is not in the correct format.
 */
private fun List<String>.toEftMutationDescriptor(): EftMutationDescriptor? {
    if (size != 3)
        return null

    val baseTypeName = this[0]
    val mutaplasmidName = this[1].trimStart()
    val attributeNamesAndValues = this[2].trimStart().split(",").map { attrNameAndValue ->
        val parts = attrNameAndValue.trim().split(" ")
        if (parts.size != 2)
            return null
        val attrName = parts[0]
        val value = parts[1].toDoubleOrNull() ?: return null
        Pair(attrName, value)
    }
    return EftMutationDescriptor(
        baseTypeName = baseTypeName,
        mutaplasmidName = mutaplasmidName,
        attributeNamesAndValues = attributeNamesAndValues
    )
}