package theorycrafter.formats

import eve.data.EveData
import eve.data.EveItemType
import eve.data.ModuleSlotType
import eve.data.SubsystemType
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.fits.FitsContext
import theorycrafter.fits.defaultTacticalMode
import theorycrafter.fitting.utils.associateWithIndex
import theorycrafter.storage.StoredFit
import theorycrafter.storage.StoredFit.*
import theorycrafter.tournaments.Composition
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(::moduleTypeOrNull) ?: 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(::droneTypeOrNull) ?: 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 = shipType.defaultTacticalMode()?.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?,
): 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,
                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 fits of the ships in this composition, mapped by their local (to the composition XML) id.
     *
     * This matches only [CompositionShipData.localFitId], not Theorycrafter database fit ids.
     */
    val fitsByLocalId: Map<String, StoredFit>


)


/**
 * 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 local (to the composition XML) id of the fit associated with this ship, if any.
     *
     * This matches only [XmlCompositionsResult.fitsByLocalId], not Theorycrafter database fit ids.
     */
    val localFitId: String?


)


/**
 * 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 = buildMap {
            fitsAndIds.forEach { (fit, id) ->
                if (id != null)
                    put(id, fit)
            }
        }
        val ships = compositionShips.map { ship ->
            CompositionShipData(
                shipTypeId = ship.shipTypeId,
                amount = ship.amount,
                active = ship.active,
                localFitId = ship.fitId,
            )
        }
        XmlCompositionsResult(
            name = compositionName,
            note = compositionNote?.toString(),
            ships = ships,
            fitsByLocalId = fitsById
        )
    }
}


/**
 * 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()
        .distinct()
        .associateWithIndex()
        .mapValues { (_, index) -> (index + 1).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) }
        )
    }
}
