package theorycrafter.ui.tournaments

import androidx.compose.foundation.layout.Column
import androidx.compose.material.SnackbarHostState
import androidx.compose.runtime.*
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager
import eve.data.EveData
import eve.data.ShipType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import theorycrafter.CompositionExportOptionSettings
import theorycrafter.CompositionExportTemplatesSettings
import theorycrafter.TheorycrafterContext
import theorycrafter.formats.EftExportOptions
import theorycrafter.formats.toEft
import theorycrafter.formats.writeCompositionToXml
import theorycrafter.tournaments.*
import theorycrafter.tournaments.Composition
import theorycrafter.ui.FitCopier
import theorycrafter.ui.LocalSnackbarHost
import theorycrafter.ui.shortName
import theorycrafter.ui.widgets.CheckboxedText
import theorycrafter.ui.widgets.DialogInitiallyFocusedButton
import theorycrafter.ui.widgets.InnerDialog
import theorycrafter.ui.widgets.LocalStandardDialogs
import theorycrafter.utils.*
import kotlin.math.max


/**
 * The export options for only exporting the composition itself.
 */
private val ExportCompositionOnly = CompositionExportOptionSettings(
    includeInactiveShips = false,
    includeReplacementShips = false,
    includeUtilities = false,
    includeNote = false,
    useTemplates = false
)


/**
 * An object that manages exporting compositions in various forms.
 */
class CompositionExporter(
    private val clipboardManager: ClipboardManager,
    private val snackbarHost: SnackbarHostState,
    private val coroutineScope: CoroutineScope
) {


    /**
     * The types of export actions we can perform on a composition.
     */
    private enum class ExportAction {
        COPY_TO_CLIPBOARD_WITH_OPTIONS,
        EXPORT_TO_XML
    }


    /**
     * The action we're about to perform on a composition after querying the user for extra information.
     */
    private var pendingAction: ExportAction? by mutableStateOf(null)


    /**
     * The composition to export.
     */
    private var pendingComposition: Composition? by mutableStateOf(null)


    /**
     * Sets the pending action and composition on which to perform the action.
     */
    private fun setPendingAction(action: ExportAction, composition: Composition) {
        pendingAction = action
        pendingComposition = composition
    }


    /**
     * Clears the pending action and composition.
     */
    private fun clearPendingAction() {
        pendingAction = null
        pendingComposition = null
    }



    /**
     * Copies the given composition to the clipboard.
     */
    fun copyToClipboard(
        composition: Composition,
        compositionExportOptionSettings: CompositionExportOptionSettings = ExportCompositionOnly,
    ) {
        coroutineScope.launch {
            clipboardManager.setText(composition.clipboardText(compositionExportOptionSettings))
            snackbarHost.showSnackbar("Copied \"${composition.name}\" to clipboard")
        }
    }

    /**
     * Copies the given composition to the clipboard, after letting the user specify export options.
     */
    fun copyToClipboardWithOptions(composition: Composition) {
        setPendingAction(
            action = ExportAction.COPY_TO_CLIPBOARD_WITH_OPTIONS,
            composition = composition
        )
    }


    /**
     * Exports the given composition to XML.
     */
    fun exportToXml(composition: Composition) {
        setPendingAction(
            action = ExportAction.EXPORT_TO_XML,
            composition = composition
        )
    }


    /**
     * A composable to put somewhere in order to let the [FitCopier] show the options dialog.
     */
    @Composable
    fun content() {
        val composition = pendingComposition ?: return
        when (pendingAction) {
            ExportAction.COPY_TO_CLIPBOARD_WITH_OPTIONS -> {
                val settings = TheorycrafterContext.settings
                CompositionExportOptionsDialog(
                    composition = composition,
                    exportOptionSettings = settings.compositionExportOptions,
                    onExportOptionsChanged = {
                        settings.compositionExportOptions = it
                    },
                    onDismiss = {
                        clearPendingAction()
                    },
                    confirmText = "Copy to Clipboard",
                    onConfirm = { copyToClipboard(composition, it) }
                )
            }
            ExportAction.EXPORT_TO_XML -> {
                val fitsContext = TheorycrafterContext.fits
                val hasAnyFits = composition.ships.any {
                    val fitId = it?.fitId ?: return@any false
                    fitsContext.handleById(fitId)?.storedFit != null
                }
                if (!hasAnyFits) {
                    LocalStandardDialogs.current.showConfirmDialog(
                        text = "No fits specified in composition \"${composition.name}\"",
                        confirmText = "Close",
                        onConfirm = { clearPendingAction() }
                    )
                } else {
                    NativeWriteFilePickerDialog(
                        title = "Specify XML file…",
                        suggestedFilename = "${composition.name}.xml",
                        filenameFilter = extensionFilenameFilter(ignoreCase = true, "xml"),
                        onCompletedSelecting = { file ->
                            clearPendingAction()
                            if (file == null)
                                return@NativeWriteFilePickerDialog
                            val eveData = TheorycrafterContext.eveData
                            coroutineScope.launch(Dispatchers.IO) {
                                file.writer().use {
                                    writeCompositionToXml(eveData, composition, fitsContext, it)
                                }
                            }
                        }
                    )
                }
            }
            else -> {}
        }
    }
    
    
}


/**
 * Returns a remembered [CompositionExporter].
 */
@Composable
fun rememberCompositionExporter(): CompositionExporter {
    val clipboardManager = LocalClipboardManager.current
    val snackbarHost = LocalSnackbarHost.current
    val coroutineScope = rememberCoroutineScope()

    val compositionExporter = remember(clipboardManager, snackbarHost, coroutineScope) {
        CompositionExporter(clipboardManager, snackbarHost, coroutineScope)
    }
    compositionExporter.content()

    return compositionExporter
}


/**
 * UI for editing [CompositionExportOptionSettings].
 */
@Composable
fun CompositionExportOptionsEditor(
    composition: Composition,
    exportOptionSettings: CompositionExportOptionSettings,
    onExportOptionsChanged: (CompositionExportOptionSettings) -> Unit,
) {
    Column {
        val compRules = composition.tournament.rules.compositionRules
        if (composition.isDoctrine || (compRules is PointsCompositionRules)) {
            CheckboxedText(
                text = "Include Inactive Ships",
                checked = exportOptionSettings.includeInactiveShips,
                onCheckedChange = {
                    onExportOptionsChanged(exportOptionSettings.copy(includeInactiveShips = it))
                }
            )
        } else if (compRules is DraftCompositionRules) {
            CheckboxedText(
                text = "Include Replacement Ships",
                checked = exportOptionSettings.includeReplacementShips,
                onCheckedChange = {
                    onExportOptionsChanged(exportOptionSettings.copy(includeReplacementShips = it))
                }
            )
        }
        CheckboxedText(
            text = "Include Utilities",
            checked = exportOptionSettings.includeUtilities,
            onCheckedChange = {
                onExportOptionsChanged(exportOptionSettings.copy(includeUtilities = it))
            }
        )
        CheckboxedText(
            text = "Include Notes",
            checked = exportOptionSettings.includeNote,
            onCheckedChange = {
                onExportOptionsChanged(exportOptionSettings.copy(includeNote = it))
            }
        )
        CheckboxedText(
            text = "Use Templates",
            checked = exportOptionSettings.useTemplates,
            onCheckedChange = {
                onExportOptionsChanged(exportOptionSettings.copy(useTemplates = it))
            }
        )
    }
}


/**
 * The dialog to let the user select composition export options.
 */
@Composable
private fun CompositionExportOptionsDialog(
    composition: Composition,
    exportOptionSettings: CompositionExportOptionSettings,
    onExportOptionsChanged: (CompositionExportOptionSettings) -> Unit,
    onDismiss: () -> Unit,
    confirmText: String,
    onConfirm: (CompositionExportOptionSettings) -> Unit,
) {

    InnerDialog(
        title = "Options:",
        onDismiss = onDismiss,
        confirmText = confirmText,
        initiallyFocusedButton = DialogInitiallyFocusedButton.Confirm,
        onConfirm = {
            onConfirm(exportOptionSettings)
        }
    ) {
        CompositionExportOptionsEditor(
            composition = composition,
            exportOptionSettings = exportOptionSettings,
            onExportOptionsChanged = onExportOptionsChanged
        )
    }
}


/**
 * The text we put into the clipboard for an empty slot.
 */
private const val EmptySlotClipboardText = "Empty Slot"


/**
 * The text we put into the clipboard for an empty slot in a composition of a draft tournament.
 */
private fun emptySlotInPositionalTournamentClipboardText(position: DraftCompositionRules.ShipPosition) =
    "[${position.name}]"


/**
 * Returns the text to copy to clipboard for the given composition.
 */
private suspend fun Composition.clipboardText(
    options: CompositionExportOptionSettings
): String {
    return if (options.useTemplates)
        clipboardTextWithTemplates(options, TheorycrafterContext.settings.compositionExportTemplates)
    else
        clipboardTextStandard(options)
}


/**
 * Returns the text to copy to clipboard for the given composition using the standard format.
 */
private suspend fun Composition.clipboardTextStandard(
    options: CompositionExportOptionSettings
): String {
    return when (val rules = tournament.rules.compositionRules) {
        is PointsCompositionRules -> clipboardTextStandardForPointsBasedTournament(options, rules)
        is DraftCompositionRules -> clipboardTextStandardForPositionBasedTournament(options, rules)
        is DoctrineCompositionRules -> clipboardTextStandardForDoctrine(options)
    }
}


/**
 * Returns the text to copy to clipboard for the given composition using the standard format for points-based
 * tournaments.
 */
private suspend fun Composition.clipboardTextStandardForPointsBasedTournament(
    options: CompositionExportOptionSettings,
    rules: PointsCompositionRules
): String {
    if (ships.isEmpty())
        return "Empty Composition ($name)"

    val shipsCosts = rules.shipsCost(this)

    val shipCount = ships.count { it?.active == true }
    val shipCountText = "$shipCount ships"
    val totalShipCostsText = "${shipsCosts.sum()}"

    val showActiveColumn = options.includeInactiveShips && ships.any { it?.active == false }
    val shipNameColLength = 1 +
            ships.maxOf { it?.shipType?.name?.length ?: EmptySlotClipboardText.length }
                .coerceAtLeast(shipCountText.length)
    val shipCostColLength = 1 +
            shipsCosts.maxOf { it.toString().length }
                .coerceAtLeast(totalShipCostsText.length)

    return buildString {
        // The composition itself
        appendLine("# $name")
        for ((index, ship) in ships.withIndex()) {
            if (ship == null) {
                if (showActiveColumn)
                    append("  ")
                appendLine(EmptySlotClipboardText)
                continue
            } else if (!ship.active && !options.includeInactiveShips) {
                continue
            }

            val shipName = ship.shipType.name
            val shipCostText = shipsCosts[index].toString()

            if (showActiveColumn) {
                append(if (ship.active) "+ " else "- ")
            }
            append(shipName.padEnd(shipNameColLength))
            if (ship.active) {
                append(shipCostText.padStart(shipCostColLength))
            }
            appendLine()
        }
        appendLine("".padEnd(
            length = (if (showActiveColumn) 2 else 0) + shipNameColLength + shipCostColLength,
            padChar = '-'
        ))
        if (showActiveColumn)
            append("  ")
        append(shipCountText.padEnd(shipNameColLength))
        append(totalShipCostsText.padStart(shipCostColLength))

        // The composition utilities
        if (options.includeUtilities) {
            append("\n\n")
            append("## Utilities")
            val lines = utilitiesSummaryLines(this@clipboardTextStandardForPointsBasedTournament)
            if (lines.isNotEmpty()) {
                for (line in lines) {
                    appendLine()
                    append("${line.count}x ${line.name}: ${line.plainTextSummary}")
                }
            } else {
                appendLine()
                append("None")
            }
        }

        // The composition note
        if (options.includeNote && note.isNotEmpty()) {
            append("\n\n")
            appendLine("## Notes")
            append(note)
        }
    }
}


/**
 * Returns the text to copy to clipboard for the given composition using the standard format for draft tournaments.
 */
private suspend fun Composition.clipboardTextStandardForPositionBasedTournament(
    options: CompositionExportOptionSettings,
    rules: DraftCompositionRules
): String {
    if (ships.isEmpty())
        return "Empty Composition ($name)"

    val prefixColLength = 1 + rules.positions.maxOf { it.prefix.length }
    return buildString {
        // The composition itself
        append("# $name")
        for (index in 0..max(ships.lastIndex, rules.positions.lastIndex)) {
            val position = rules.positions.getOrNull(index)
            if ((position == null) && !options.includeReplacementShips)
                break
            val ship = ships.getOrNull(index)
            if ((ship == null) && (position == null))
                continue

            appendLine()
            append((position?.prefix ?: "*").padEnd(prefixColLength))
            append(ship?.shipType?.name ?: emptySlotInPositionalTournamentClipboardText(position!!))
        }

        // The composition utilities
        if (options.includeUtilities) {
            append("\n\n")
            append("## Utilities")
            val lines = utilitiesSummaryLines(this@clipboardTextStandardForPositionBasedTournament)
            if (lines.isNotEmpty()) {
                for (line in lines) {
                    appendLine()
                    append("${line.count}x ${line.name}: ${line.plainTextSummary}")
                }
            } else {
                appendLine()
                append("None")
            }
        }

        // The composition note
        if (options.includeNote && note.isNotEmpty()) {
            append("\n\n")
            appendLine("## Notes")
            append(note)
        }
    }
}


/**
 * Returns the text to copy to clipboard for the given doctrine using the standard format for doctrines.
 */
private suspend fun Composition.clipboardTextStandardForDoctrine(
    options: CompositionExportOptionSettings,
): String {
    if (ships.isEmpty())
        return "Empty Doctrine ($name)"

    fun Composition.Ship.shipText() = buildString {
        append("- ")
        if (!active)
            append("(")
        append(shipType.name.withAmount(amountOrOne))
        if (!active)
            append(")")
    }

    return buildString {
        // The composition itself
        appendLine("# $name")
        for (ship in ships) {
            if ((ship == null) || (!ship.active && !options.includeInactiveShips)) {
                continue
            }

            appendLine(ship.shipText())
        }

        val shipCount = ships.sumOf {
            if (it?.active == true)
                it.amountOrOne
            else
                0
        }
        append("$shipCount ships")

        // The composition utilities
        if (options.includeUtilities) {
            append("\n\n")
            append("## Utilities")
            val lines = utilitiesSummaryLines(this@clipboardTextStandardForDoctrine)
            if (lines.isNotEmpty()) {
                for (line in lines) {
                    appendLine()
                    append("- ${line.count}x ${line.name}: ${line.plainTextSummary}")
                }
            } else {
                appendLine()
                append("None")
            }
        }

        // The composition note
        if (options.includeNote && note.isNotEmpty()) {
            append("\n\n")
            appendLine("## Notes")
            append(note)
        }
    }
}


/**
 * The regular expression for the EFT fit replacement.
 */
@Suppress("CanUnescapeDollarLiteral")
private val EftFitTokenRegex = "\\\$eftFit\\((.*)\\)\\\$".toRegex()


/**
 * Replaces a template token with the given name, with the given value.
 */
@Suppress("CanUnescapeDollarLiteral")
private fun String.replaceTemplateToken(token: String, replacement: String): String {
    return replace("\$$token\$", replacement)
}


/**
 * The composition template replace tokens and the value they map to.
 */
private class CompositionTemplateReplacements(
    val composition: Composition,
    val templateSettings: CompositionExportTemplatesSettings
) {


    /**
     * Returns the result of the title template.
     */
    fun title(): String {
        var result = templateSettings.titleTemplate
        result = result.replaceTemplateToken("compName", composition.name)
        return result
    }


    /**
     * Returns the result of the ship template.
     */
    fun ship(ship: Composition.Ship, count: Int): String {
        val storedFit = ship.fitId?.let { TheorycrafterContext.fits.handleById(it)?.storedFit }

        var result = templateSettings.shipTemplate
        result = result.replaceTemplateToken("shipCount", count.toString())
        result = result.replaceTemplateToken("shipName", ship.shipType.name)
        result = result.replaceTemplateToken("shipShortName", ship.shipType.shortName())
        result = result.replaceTemplateToken("fitName", storedFit?.name ?: "Unspecified")
        val eveData = TheorycrafterContext.eveData
        result = result.replace(EftFitTokenRegex) { matchResult ->
            if (storedFit != null) {
                val options = matchResult.groupValues[1].split(",").map { it.trim() }.toSet()
                with(eveData) {
                    storedFit.toEft(
                        EftExportOptions(
                            loadedCharges = "charges" in options,
                            implants = "implants" in options,
                            boosters = "boosters" in options,
                            cargo = "cargo" in options,
                            mutatedAttributes = "mutations" in options,
                        )
                    )
                }
            } else {
                "Unspecified fit\n"
            }
        }
        return result
    }


    /**
     * Returns the result of the utility summary line template.
     */
    fun utilityLine(line: UtilitySummaryLine): String {
        var result = templateSettings.utilityTemplate
        result = result.replaceTemplateToken("utilityCount", line.count.toString())
        result = result.replaceTemplateToken("utilityName", line.name)
        result = result.replaceTemplateToken("utilitySummary", line.summary)
        return result
    }


    /**
     * Returns the result of the note template.
     */
    fun note(): String {
        var result = templateSettings.noteTemplate
        result = result.replaceTemplateToken("note", composition.note)
        result = result.replaceTemplateToken(
            "plainShipList",
            composition.ships.filterNotNull().joinToString("\n"){ it.shipType.name }
        )
        return result
    }


}


/**
 * Returns the text to copy to clipboard for the given composition using templates.
 */
private suspend fun Composition.clipboardTextWithTemplates(
    options: CompositionExportOptionSettings,
    templatesSettings: CompositionExportTemplatesSettings
): String {
    val replacements = CompositionTemplateReplacements(this, templatesSettings)
    return buildString {
        // The composition title
        append(replacements.title())

        // The composition itself
        val remainingShips = ships.filterNotNull().toMutableList()
        while (remainingShips.isNotEmpty()) {
            val ship = remainingShips.first()
            if (!ship.active && !options.includeInactiveShips) {
                remainingShips.removeFirst()
                continue
            }

            val sameShips = remainingShips.filter {
                (it.shipType == ship.shipType) && (it.fitId == ship.fitId) && (it.active == ship.active)
            }

            append(replacements.ship(ship, sameShips.count()))

            remainingShips.removeAll(sameShips)
        }

        // The composition utilities
        val utilityLines = utilitiesSummaryLines(this@clipboardTextWithTemplates)
        if (options.includeUtilities && utilityLines.isNotEmpty()) {
            append("\n")
            for (line in utilityLines) {
                append(replacements.utilityLine(line))
            }
        }

        // The composition note
        if (options.includeNote && note.isNotEmpty()) {
            append("\n")
            append(replacements.note())
        }
    }
}


/**
 * Parses the given text as a composition.
 */
context(EveData)
fun compositionFromClipboardText(
    text: String,
    tournament: Tournament,
): StoredComposition? {
    val lines = text.lines().dropWhile { it.isBlank() }
    if (lines.isEmpty())
        return null

    return when (val rules = tournament.rules.compositionRules) {
        is PointsCompositionRules -> compositionFromClipboardTextForPointsBasedTournament(lines)
        is DraftCompositionRules -> compositionFromClipboardTextForPositionBasedTournament(lines, rules)
        is DoctrineCompositionRules -> compositionFromClipboardTextForDoctrine(lines)
    }
}


/**
 * Parses a composition in the points-based tournament format.
 */
context(EveData)
private fun compositionFromClipboardTextForPointsBasedTournament(lines: List<String>): StoredComposition? {
    val isTheorycrafterFormat = run {
        val shipsEndLineIndex = lines.indexOfFirst { it.length > 5 && it.all { c -> c == '-' } }
        if (shipsEndLineIndex == -1)
            return@run false
        if (lines.getOrNull(shipsEndLineIndex + 1)?.contains(" ships ") != true)
            return@run false
        true
    }

    val linesIterator = lines.iterator()
    val name = if (isTheorycrafterFormat) linesIterator.next().removePrefix("# ") else "Pasted Composition"
    val ships = mutableListOf<StoredComposition.StoredCompositionShip?>()
    for (line in linesIterator) {
        if (line.isBlank())
            continue
        if (isTheorycrafterFormat && line.all { it == '-' })
            break

        var unparsedText = line
        if (unparsedText.trim() == EmptySlotClipboardText) {
            ships.add(null)
            continue
        }

        val isActive = if (isTheorycrafterFormat) {
            val startsWithPlus = unparsedText.startsWith("+ ")
            val startsWithMinus = unparsedText.startsWith("- ")
            if (startsWithPlus || startsWithMinus)
                unparsedText = unparsedText.substring(2)
            !startsWithMinus
        } else true

        // Trim the points, if any
        unparsedText = unparsedText.trimEnd { it.isDigit() || it.isWhitespace() }

        // Remove the last word until the remainder parses as a ship
        var shipType: ShipType?
        do {
            shipType = shipTypeOrNull(unparsedText)
            unparsedText = unparsedText.trimEnd { !it.isWhitespace() }.trimEnd()
        } while ((shipType == null) && (unparsedText.isNotEmpty()))

        if (shipType != null) {
            ships.add(
                StoredComposition.StoredCompositionShip(
                    shipTypeId = shipType.itemId,
                    fitId = null,
                    active = isActive
                )
            )
        }
    }

    val note = linesIterator.asSequence().dropWhile { it != "## Notes" }.drop(1).joinToString("\n")

    return if (ships.isEmpty())
        null
    else
        StoredComposition(
            name = name,
            ships = ships,
            note = note
        )
}


/**
 * Parses a composition in the draft tournament format.
 */
context(EveData)
private fun compositionFromClipboardTextForPositionBasedTournament(
    lines: List<String>,
    rules: DraftCompositionRules
): StoredComposition? {
    val positions = rules.positions
    val isTheorycrafterFormat = run {
        val shipLines = lines.drop(1).withIndex().takeWhile { (index, text) ->
            text.startsWith(
                if (index in positions.indices)
                    positions[index].prefix
                else
                    "*"
            )
        }
        if (shipLines.size < positions.size)
            return@run false
        true
    }

    val linesIterator = lines.iterator()
    val name = if (isTheorycrafterFormat) linesIterator.next().removePrefix("# ") else "Pasted Composition"
    val positionsIterator = positions.iterator()
    var nextPosition: DraftCompositionRules.ShipPosition? = positionsIterator.next()
    val ships = mutableListOf<StoredComposition.StoredCompositionShip?>()
    var firstNonShipLine: String? = null
    for (line in linesIterator) {
        if (line.isBlank())
            continue
        var unparsedText = line

        // Strip the position prefix
        if (isTheorycrafterFormat)
            unparsedText = unparsedText.removePrefix(nextPosition?.prefix ?: "*").trim()

        var shipType: ShipType? = null
        if (isTheorycrafterFormat) {
            if ((nextPosition != null) && (unparsedText == emptySlotInPositionalTournamentClipboardText(nextPosition)))
                ships.add(null)
            else {
                shipType = shipTypeOrNull(unparsedText)
                if (shipType == null) { // Maybe Notes or Utilities line
                    firstNonShipLine = line
                    break
                }
            }
        } else {
            // Remove the last word until the remainder parses as a ship
            do {
                shipType = shipTypeOrNull(unparsedText)
                unparsedText = unparsedText.trimEnd { !it.isWhitespace() }.trimEnd()
            } while ((shipType == null) && (unparsedText.isNotEmpty()))
        }

        if (shipType != null) {
            ships.add(
                StoredComposition.StoredCompositionShip(
                    shipTypeId = shipType.itemId,
                    fitId = null,
                    active = nextPosition != null
                )
            )
        }

        if (isTheorycrafterFormat || (shipType != null)) {
            nextPosition = if (positionsIterator.hasNext()) positionsIterator.next() else null
        }
    }

    if (!isTheorycrafterFormat) {
        // Rearrange the ships, trying to put them into their correct positions
        for ((index, position) in positions.withIndex()) {
            val shipType = ships.getOrNull(index)?.let { shipType(it.shipTypeId) }
            if (shipType in position.legalShipTypes)
                continue

            val correctShipIndex = ((index+1) .. ships.lastIndex).firstOrNull {
                ships[it].let { ship ->
                    (ship != null) && (shipType(ship.shipTypeId) in position.legalShipTypes)
                }
            }
            if (correctShipIndex != null)
                ships.swap(index, correctShipIndex)
        }
    }

    val note = run {
        val notesLine = "## Notes"
        if (firstNonShipLine == notesLine)
            linesIterator.asSequence().joinToString("\n")
        else
            linesIterator.asSequence().dropWhile { it != notesLine }.drop(1).joinToString("\n")
    }

    return if (ships.isEmpty())
        null
    else
        StoredComposition(
            name = name,
            ships = ships,
            note = note
        )
}


/**
 * Parses a doctrine.
 */
context(EveData)
private fun compositionFromClipboardTextForDoctrine(lines: List<String>): StoredComposition? {
    val isTheorycrafterFormat = run {
        if (!lines.first().startsWith("# "))
            return@run false
        val shipLines = lines.drop(1).takeWhile { it.startsWith("- ") }
        if (shipLines.isEmpty())
            return@run false
        if (lines.getOrNull(1 + shipLines.size)?.contains(" ships") != true)
            return@run false
        true
    }

    val linesIterator = lines.iterator()
    val name = if (isTheorycrafterFormat) linesIterator.next().removePrefix("# ") else "Pasted Composition"
    val ships = mutableListOf<StoredComposition.StoredCompositionShip?>()
    for (line in linesIterator) {
        if (line.isBlank())
            continue

        if (isTheorycrafterFormat && line.contains(" ships"))
            break

        var unparsedText = line.removePrefix("- ")

        // Get whether active
        val isActive = if (isTheorycrafterFormat) {
            !unparsedText.startsWith("(") || !unparsedText.endsWith(")")
        } else true
        unparsedText = unparsedText.removePrefix("(").removeSuffix(")")

        // Get the amount
        val amountOrNull = unparsedText.substringBefore('x').trim().toIntOrNull()
        if (amountOrNull != null) {
            unparsedText = unparsedText.substringAfter('x').trim()
        }

        // Remove the last word until the remainder parses as a ship
        val shipType = shipTypeOrNull(unparsedText)
        if (shipType != null) {
            ships.add(
                StoredComposition.StoredCompositionShip(
                    shipTypeId = shipType.itemId,
                    amount = amountOrNull ?: 1,
                    fitId = null,
                    active = isActive
                )
            )
        }
    }

    val note = linesIterator.asSequence().dropWhile { it != "## Notes" }.drop(1).joinToString("\n")

    return if (ships.isEmpty())
        null
    else
        StoredComposition(
            name = name,
            ships = ships,
            note = note
        )
}
