package theorycrafter.formats

import eve.data.*
import eve.data.utils.valueByEnum
import org.w3c.dom.Document
import org.w3c.dom.Element
import org.w3c.dom.Node
import org.xml.sax.Attributes
import org.xml.sax.ContentHandler
import org.xml.sax.InputSource
import org.xml.sax.XMLReader
import org.xml.sax.helpers.DefaultHandler
import theorycrafter.FitsContext
import theorycrafter.fitting.utils.associateWithIndex
import theorycrafter.storage.StoredFit
import theorycrafter.storage.StoredFit.*
import theorycrafter.tournaments.Composition
import theorycrafter.ui.fiteditor.defaultMutatedName
import theorycrafter.ui.fiteditor.initialTacticalMode
import theorycrafter.utils.addNotNull
import theorycrafter.utils.timeAction
import java.io.Reader
import java.io.Writer
import javax.xml.parsers.DocumentBuilder
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.parsers.SAXParserFactory
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult


/**
 * The contents of fit item tag.
 */
data class FitItem(
    val kind: Kind,
    val type: String,
    val slot: String?,
    val quantity: Int?,
    val baseType: String?,
    val mutaplasmid: String?,
    val mutatedAttrs: String?
) {

    /**
     * The kinds of fit items in the XML.
     */
    enum class Kind { Hardware, Implant, Booster }

}


/**
 * Converts a fit item tag into a [FitItem] instance.
 */
private fun Attributes.asFitItem(kind: FitItem.Kind): FitItem? {
    val type = getValue("type") ?: return null
    val slot = getValue("slot")
    val quantity = getValue("qty")?.let {
        it.toIntOrNull() ?: return null
    }
    val baseType = getValue("base_type")
    val mutaplasmid = getValue("mutaplasmid")
    val mutatedAttrs = getValue("mutated_attrs")

    return FitItem(
        kind = kind,
        type = type,
        slot = slot,
        quantity = quantity,
        baseType = baseType,
        mutaplasmid = mutaplasmid,
        mutatedAttrs = mutatedAttrs
    )
}


/**
 * Parses a single fit; returns `null` if unsuccessful.
 */
context(EveData)
private fun parseFit(
    name: String,
    shipTypeName: String,
    fitItemList: List<FitItem>
): StoredFit? {
    val shipType = shipTypeOrNull(shipTypeName) ?: return null

    val subsystemByKind = mutableMapOf<SubsystemType.Kind, StoredSubsystem>()
    val highSlotRack = arrayOfNulls<StoredModule>(ModuleSlotType.HIGH.maxSlotCount)
    val medSlotRack = arrayOfNulls<StoredModule>(ModuleSlotType.MEDIUM.maxSlotCount)
    val lowSlotRack = arrayOfNulls<StoredModule>(ModuleSlotType.LOW.maxSlotCount)
    val rigs = arrayOfNulls<StoredModule>(ModuleSlotType.RIG.maxSlotCount)
    val drones = mutableListOf<StoredDroneGroup>()
    val cargoItems = mutableListOf<StoredCargoItem>()
    val implants = mutableListOf<StoredImplant>()
    val boosters = mutableListOf<StoredBooster>()

    // The list of charges in the cargo; we'll be loading them into modules when we parse those
    val charges = fitItemList.mapNotNull {
        if (it.slot == "cargo")
            chargeTypeOrNull(it.type)
        else
            null
    }

    // Parses, creates and inserts a StoredModule into the array; returns whether successful
    fun Array<StoredModule?>.add(slotType: ModuleSlotType, fitItem: FitItem): Boolean {
        val moduleTypeAndMutation = fitItem.decodeTypeAndMutation(
            typeByName = ::moduleTypeOrNull,
            mutatedName = ModuleType::defaultMutatedName
        ) ?: return false
        val (moduleType, mutation) = moduleTypeAndMutation

        if (moduleType.slotType != slotType)
            return false
        val slotIndex = fitItem.slot!!.substringAfterLast(" ").toIntOrNull() ?: return false
        this[slotIndex] = StoredModule(
            itemId = moduleType.itemId,
            enabled = true,
            state = moduleType.importedState(),
            chargeId = charges.firstOrNull { moduleType.canLoadCharge(it) }?.itemId,
            mutation = mutation,
            extraAttributes = moduleType.importedExtraAttributes()
        )
        return true
    }

    // Parses, creates and inserts a StoredSubsystem into the map; returns whether successful
    fun MutableMap<SubsystemType.Kind, StoredSubsystem>.add(subsystemName: String): Boolean {
        val subsystemType = subsystemTypeOrNull(subsystemName) ?: return false
        this[subsystemType.kind] = StoredSubsystem(subsystemType)
        return true
    }

    // Parses, creates and inserts a StoredDroneGroup into the list; returns whether successful
    fun MutableList<StoredDroneGroup>.add(fitItem: FitItem): Boolean {
        if (fitItem.quantity == null)
            return false

        val droneTypeAndMutation = fitItem.decodeTypeAndMutation(
            typeByName = ::droneTypeOrNull,
            mutatedName = DroneType::defaultMutatedName
        ) ?: return false
        val (droneType, mutation) = droneTypeAndMutation

        add(
            StoredDroneGroup(
                itemId = droneType.itemId,
                size = fitItem.quantity,
                active = this.isEmpty(),  // Activate the first drone group
                mutation = mutation
            )
        )
        return true
    }

    // Parses, creates and inserts a StoredCargoItem into the list; returns whether successful
    fun MutableList<StoredCargoItem>.add(itemName: String, quantity: Int?): Boolean {
        if (quantity == null)
            return false
        val itemType = cargoItemTypeOrNull(itemName) ?: return false
        add(
            StoredCargoItem(
                itemId = itemType.itemId,
                amount = quantity
            )
        )
        return true
    }

    // Parses, creates and inserts a StoredImplant into the list; returns whether successful
    fun MutableList<StoredImplant>.add(implantName: String): Boolean {
        val implantType = implantTypes.getOrNull(implantName) ?: return false
        add(
            StoredImplant(
                itemId = implantType.itemId,
                enabled = true,
            )
        )
        return true
    }

    // Parses, creates and inserts a StoredBooster into the list; returns whether successful
    fun MutableList<StoredBooster>.add(boosterName: String): Boolean {
        val implantType = boosterTypeOrNull(boosterName) ?: return false
        add(
            StoredBooster(
                itemId = implantType.itemId,
                enabled = true,
                activeSideEffectPenalizedAttributeIds = emptyList()
            )
        )
        return true
    }

    // Parse all hardware
    for (fitItem in fitItemList) {
        val success = when (fitItem.kind) {
            FitItem.Kind.Hardware -> {
                val slotName = fitItem.slot
                when {
                    slotName == null -> false
                    slotName.startsWith("hi slot") ->
                        highSlotRack.add(ModuleSlotType.HIGH, fitItem)
                    slotName.startsWith("med slot") ->
                        medSlotRack.add(ModuleSlotType.MEDIUM, fitItem)
                    slotName.startsWith("low slot") ->
                        lowSlotRack.add(ModuleSlotType.LOW, fitItem)
                    slotName.startsWith("rig slot") ->
                        rigs.add(ModuleSlotType.RIG, fitItem)
                    slotName.startsWith("subsystem slot") ->
                        subsystemByKind.add(fitItem.type)
                    slotName == "drone bay" ->
                        drones.add(fitItem)
                    slotName == "cargo" ->
                        cargoItems.add(fitItem.type, fitItem.quantity)
                    else -> false
                }
            }
            FitItem.Kind.Implant -> implants.add(fitItem.type)
            FitItem.Kind.Booster -> boosters.add(fitItem.type)
        }

        if (!success)
            return null
    }

    // Should have subsystems iff it uses subsystems
    if (subsystemByKind.isNotEmpty() != shipType.usesSubsystems)
        return null
    val subsystems = if (!shipType.usesSubsystems) null else
        valueByEnum<SubsystemType.Kind, StoredSubsystem> { kind ->
            subsystemByKind[kind] ?: return null
        }

    // Trims null modules from the end and convers the array into a list
    fun Array<StoredModule?>.toModuleList(): List<StoredModule?> {
        val lastNonNullIndex = indexOfLast { it != null }
        return asList().subList(0, lastNonNullIndex + 1)
    }

    return StoredFit.new(
        name = name,
        shipType = shipType,
        tacticalMode = initialTacticalMode(shipType)?.let(StoredFit::StoredTacticalMode),
        subsystems = subsystems,
        highSlotRack = highSlotRack.toModuleList(),
        medSlotRack = medSlotRack.toModuleList(),
        lowSlotRack = lowSlotRack.toModuleList(),
        rigs = rigs.toModuleList(),
        droneGroups = drones,
        cargoItems = cargoItems,
        implants = implants,
        boosters = boosters,
    )
}


/**
 * Decodes a `mutated_attrs` value into a list of attribute ids and their mutated values.
 */
context(EveData)
private fun String.decodeMutatedAttrs(): List<Pair<Int, Double>> = this
    .split(",")
    .mapNotNull { attrNameAndValue ->
        val parts = attrNameAndValue.trim().split(" ")
        if (parts.size != 2)
            return@mapNotNull null
        val attrName = parts[0]
        val attribute = attributes[attrName] ?: return@mapNotNull null
        val value = parts[1].toDoubleOrNull() ?: return@mapNotNull null

        Pair(attribute.id, value)
    }


/**
 * Returns a module or drone type and a mutation decoded from a [FitItem].
 */
context(EveData)
private inline fun <reified T: Any> FitItem.decodeTypeAndMutation(
    crossinline typeByName: (String) -> T?,
    crossinline mutatedName: T.(Mutaplasmid) -> String
): Pair<T, StoredMutation?>? {
    if ((baseType != null) && (mutaplasmid != null) && (mutatedAttrs != null)) {
        val itemType = typeByName(baseType) ?: return null
        val mutaplasmid = mutaplasmidOrNull(mutaplasmid)
        val mutatedAttrs = mutatedAttrs.decodeMutatedAttrs()
        val mutation = if (mutaplasmid == null) null else {
            StoredMutation(
                mutaplasmidId = mutaplasmid.id,
                name = itemType.mutatedName(mutaplasmid),
                attributeIdsAndValues = mutatedAttrs
            )
        }
        return Pair(itemType, mutation)
    }
    else {
        val itemType = typeByName(type)
            ?: abyssalItemReplacement(type) as? T
            ?: return null
        return Pair(itemType, null)
    }
}


/**
 * The parsed composition ship element.
 */
private data class ParsedCompositionShip(
    val shipTypeId: Int,
    val amount: Int?,
    val active: Boolean,
    val fitId: String?
)


/**
 * The [ContentHandler] passed to an [XMLReader] in order to parse the contents of the XML.
 */
private class XmlHandler(private val eveData: EveData) : DefaultHandler() {


    /**
     * The number of fits read (both successfully and unsuccessfully) so far.
     */
    var readFitsCount = 0
        private set


    /**
     * The list of fits and their ids read so far.
     */
    val fitsAndIds = mutableListOf<Pair<StoredFit, String?>>()


    /**
     * The list of fits we could not read. Note that this only includes fits where the fit name
     * and ship type were read successfully.
     */
    val unparsed = mutableListOf<UnloadedFit>()


    /**
     * The name of the composition defined by the XML file, if it does define a composition.
     */
    var compositionName: String? = null


    /**
     * The composition note defined by the XML file, if it does define a composition.
     */
    var compositionNote: StringBuilder? = null


    /**
     * The ships in the composition.
     */
    val compositionShips = mutableListOf<ParsedCompositionShip>()


    /**
     * The name of the fit being currently read.
     */
    private var fitName: String? = null


    /**
     * The name of the ship type in the fit being currently read.
     */
    private var fitShipTypeName: String? = null


    /**
     * The id of the current fit in the composition.
     */
    private var fitId: String? = null


    /**
     * Whether the handler is in the process of reading a composition note.
     */
    private var readingCompositionNote = false


    /**
     * The list of items in the fit being currently read.
     */
    private val fitItemList = mutableListOf<FitItem>()


    override fun startElement(uri: String, localName: String, qName: String, attributes: Attributes) {
        when (qName) {
            "composition" -> compositionName = attributes.getValue("name")
            "note" -> {
                compositionNote = StringBuilder()
                readingCompositionNote = true
            }
            "ship" -> {
                val shipTypeName = attributes.getValue("shipType") ?: return
                val shipType = eveData.shipTypeOrNull(shipTypeName) ?: return
                val amount = attributes.getValue("amount")?.toIntOrNull()
                val active = attributes.getValue("active").toBooleanStrictOrNull() ?: true
                val fitId = attributes.getValue("fitId")
                compositionShips.add(
                    ParsedCompositionShip(
                        shipTypeId = shipType.itemId,
                        amount = amount,
                        active = active,
                        fitId = fitId
                    )
                )
            }
            "fitting" -> {
                fitName = attributes.getValue("name")
                fitId = attributes.getValue("fitId")
            }
            "shipType" -> fitShipTypeName = attributes.getValue("value")
            else -> {
                val kind = when (qName) {
                    "hardware" -> FitItem.Kind.Hardware
                    "implant" -> FitItem.Kind.Implant
                    "booster" -> FitItem.Kind.Booster
                    else -> return
                }
                fitItemList.addNotNull(attributes.asFitItem(kind))
            }
        }
    }


    override fun characters(ch: CharArray, start: Int, length: Int) {
        if (readingCompositionNote)
            compositionNote!!.appendRange(ch, start, start + length)
    }


    override fun endElement(uri: String, localName: String, qName: String) {
        if (qName == "fitting") {
            readFitsCount += 1

            val name = this.fitName ?: return
            val shipTypeName = this.fitShipTypeName ?: return

            val fit = with(eveData) {
                parseFit(name = name, shipTypeName = shipTypeName, fitItemList)
            }
            if (fit == null)
                unparsed.add(UnloadedFit(name = name, shipTypeName = shipTypeName))
            else {
                fitsAndIds.add(Pair(fit, fitId))
            }

            this.fitName = null
            this.fitShipTypeName = null
            this.fitId = null
            this.fitItemList.clear()
        } else if (qName == "note") {
            readingCompositionNote = false
        }
    }


}


/**
 * Loads an XML file with a list of fits or a composition, using the given block for creating the result.
 */
private fun <T> loadFromXml(
    actionName: String,
    eveData: EveData,
    reader: Reader,
    result: XmlHandler.() -> T
): T {
    val xmlReader = SAXParserFactory.newInstance().newSAXParser().xmlReader
    val handler = XmlHandler(eveData)
    xmlReader.contentHandler = handler

    return timeAction(actionName) {
        xmlReader.parse(InputSource(reader))
        result(handler)
    }
}


/**
 * Loads fits from an XML file.
 */
fun loadFitsFromXml(eveData: EveData, reader: Reader): FitsImportResult {
    return loadFromXml(
        actionName = "Reading fits from XML",
        eveData = eveData,
        reader = reader,
    ) {
        FitsImportResult(
            totalFitsCount = readFitsCount,
            fits = fitsAndIds.map { it.first },
            unsuccessful = unparsed,
        )
    }
}



/**
 * The result of parsing an XML with a composition.
 */
data class XmlCompositionsResult(


    /**
     * The name of the composition.
     */
    val name: String?,


    /**
     * The composition note.
     */
    val note: String?,


    /**
     * The composition ships.
     */
    val ships: List<CompositionShipData>,


)


/**
 * The data about a single ship in a composition provided in the XML.
 */
data class CompositionShipData(


    /**
     * The id of the ship type.
     */
    val shipTypeId: Int,


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


    /**
     * Whether this ship is "active" in the composition.
     */
    val active: Boolean,


    /**
     * The fit associated with this ship, if any.
     */
    val fit: StoredFit?


)


/**
 * Loads a composition from an XML file.
 */
fun loadCompositionFromXml(eveData: EveData, reader: Reader): XmlCompositionsResult {
    return loadFromXml(
        actionName = "Reading composition from XML",
        eveData = eveData,
        reader = reader
    ) {
        val fitsById = fitsAndIds.associate { (fit, id) -> id to fit }
        val ships = compositionShips.map { ship ->
            val fit = ship.fitId?.let { fitsById[it] }
            CompositionShipData(
                shipTypeId = ship.shipTypeId,
                amount = ship.amount,
                active = ship.active,
                fit = fit
            )
        }
        XmlCompositionsResult(
            name = compositionName,
            note = compositionNote?.toString(),
            ships = ships
        )
    }
}


/**
 * The [DocumentBuilder] we use to create XML document builders.
 */
private val XmlDocumentBuilder = DocumentBuilderFactory.newDefaultInstance().newDocumentBuilder()


/**
 * The [TransformerFactory] we use to serialize XML.
 */
private val XmlTransformerFactory = TransformerFactory.newDefaultInstance()


/**
 * Encodes the given list of fits to XML and writes it into the given output stream.
 */
fun writeFitsToXml(eveData: EveData, fits: Collection<StoredFit>, writer: Writer) {
    val document = XmlDocumentBuilder.newDocument()
    encodeFittings(
        eveData = eveData,
        parent = document,
        fitsAndIds = fits.map { Pair(it, null) },
    )
    writeDocument(document, writer)
}


/**
 * Encodes the given (tournament) composition to XML and writes it into the given output stream.
 */
fun writeCompositionToXml(
    eveData: EveData,
    composition: Composition,
    fitsContext: FitsContext,
    writer: Writer
) {
    val document = XmlDocumentBuilder.newDocument()
    val compositionElem = document.appendElement(
        tagName = "composition",
        "name" to composition.name,
    )

    // Write the composition ships
    val compShips = composition.ships.filterNotNull()
    val fitByCompShip = compShips.associateWith {
        val fitId = it.fitId ?: return@associateWith null
        fitsContext.handleById(fitId)?.storedFit ?: return@associateWith null
    }
    val localIdByFit = fitByCompShip.values
        .filterNotNull()
        .associateWithIndex()
        .mapValues { it.value.toString() }
    for (ship in compShips) {
        val shipElem = compositionElem.appendElement(
            tagName = "ship",
            "shipType" to ship.shipType.name,
            "amount" to ship.amount?.toString(),
            "active" to ship.active.toString()
        )
        val localFitId = fitByCompShip[ship]?.let { localIdByFit[it] }
        if (localFitId != null)
            shipElem.setAttribute("fitId", localFitId)
    }

    compositionElem.appendElement("note").appendChild(document.createTextNode(composition.note))

    // Add the fittings
    encodeFittings(
        eveData = eveData,
        parent = compositionElem,
        fitsAndIds = localIdByFit.map { Pair(it.key, it.value) }
    )
    writeDocument(document, writer)
}


private fun Node.ownerDocumentOrSelf(): Document {
    return (this as? Document) ?: this.ownerDocument
}


/**
 * Encodes the given list of fits to XML.
 */
private fun encodeFittings(
    eveData: EveData,
    parent: Node,
    fitsAndIds: Collection<Pair<StoredFit, String?>>,
) {
    val document = parent.ownerDocumentOrSelf()
    val fittings = parent.appendElement(
        tagName = "fittings",
        "count" to fitsAndIds.size.toString()
    )
    with(eveData) {
        for ((fit, id) in fitsAndIds) {
            val elem = document.encodeFit(fit)
            if (id != null) {
                elem.setAttribute("fitId", id)
            }
            fittings.appendChild(elem)
        }
    }
}


private fun writeDocument(document: Document, writer: Writer) {
    val transformer = XmlTransformerFactory.newTransformer().also {
        it.setOutputProperty(OutputKeys.INDENT, "yes")
        it.setOutputProperty(OutputKeys.STANDALONE, "yes")
    }
    transformer.transform(DOMSource(document), StreamResult(writer))
}


/**
 * Appends a new element to the given element.
 */
private fun Node.appendElement(
    tagName: String,
    vararg attributes: Pair<String, String?>
): Element {
    return ownerDocumentOrSelf()
        .createElement(tagName)
        .also { appendChild(it) }
        .also {
            for ((attrName, attrValue) in attributes) {
                it.setAttribute(attrName, attrValue)
            }
        }
}


/**
 * Encodes a fit into an XML node.
 */
context(EveData)
private fun Document.encodeFit(fit: StoredFit): Element {

    fun <T: Any> Element.appendItemList(
        tagName: String = "hardware",
        items: List<T?>,
        type: (T) -> EveItemType?,
        slotName: ((Int) -> String)? = null,
        quantity: ((T) -> Int)? = null
    ) {
        for ((slotIndex, item) in items.withIndex()) {
            if (item == null)
                continue
            val itemTypeName = type(item)?.name ?: continue
            appendElement(
                tagName = tagName,
                attributes = listOfNotNull(
                    if (slotName != null) "slot" to slotName(slotIndex) else null,
                    "type" to itemTypeName,
                    if (quantity != null) "qty" to quantity(item).toString() else null,
                ).toTypedArray()
            )
        }
    }

    fun <T: Any> Element.appendIndexedList(
        tagName: String = "hardware",
        slotName: String,
        items: List<T?>,
        type: (T) -> EveItemType?,
    ) = appendItemList(
        tagName = tagName,
        items = items,
        slotName = { slotIndex -> "$slotName $slotIndex" },
        type = type,
    )

    fun <T: Any> Element.appendListWithQuantity(
        tagName: String = "hardware",
        slotName: String,
        items: List<T?>,
        type: (T) -> EveItemType?,
        quantity: (T) -> Int
    ) = appendItemList(
        tagName = tagName,
        items = items,
        slotName = { slotName },
        type = type,
        quantity = quantity
    )

    fun Element.appendModuleRack(slotName: String, modules: List<StoredModule?>) {
        appendIndexedList(
            slotName = slotName,
            items = modules,
            type = { moduleTypeOrNull(it.itemId) }
        )
    }

    return createElement("fitting").apply {
        setAttribute("name", fit.name)
        appendElement("description", "value" to "")
        appendElement("shipType", "value" to shipType(fit.shipTypeId).name)
        appendModuleRack("low slot", fit.lowSlotRack)
        appendModuleRack("med slot", fit.medSlotRack)
        appendModuleRack("hi slot", fit.highSlotRack)
        appendModuleRack("rig slot", fit.rigs)
        fit.subsystems?.let { subsystems ->
            appendIndexedList(
                slotName = "subsystem slot",
                items = subsystems.values,
                type = { subsystemTypeOrNull(it.itemId) }
            )
        }
        appendListWithQuantity(
            slotName = "drone bay",
            items = fit.droneGroups,
            type = { droneTypeOrNull(it.itemId) },
            quantity = { it.size }
        )
        appendListWithQuantity(
            slotName = "cargo",
            items = fit.cargoItems,
            type = { cargoItemTypeOrNull(it.itemId) },
            quantity = { it.amount }
        )

        // Extensions; the EVE client can't import them
        appendItemList(
            tagName = "implant",
            items = fit.implants,
            type = { implantTypes.getOrNull(it.itemId) },
        )
        appendItemList(
            tagName = "booster",
            items = fit.boosters,
            type = { boosterTypeOrNull(it.itemId) }
        )
    }
}
