package theorycrafter.tournaments

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import theorycrafter.storage.CorruptFitRepository
import theorycrafter.utils.StoredCollection
import java.io.DataInput
import java.io.DataOutput
import java.io.File
import kotlin.concurrent.atomics.AtomicInt
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.concurrent.atomics.incrementAndFetch


/**
 * The repository of tournament related data.
 */
@OptIn(ExperimentalAtomicApi::class)
class TournamentRepository private constructor(


    /**
     * The description of the tournament this repository is for.
     */
    private val tournamentDesc: TournamentDescriptor,


    /**
     * The stored collection of repository items.
     */
    private val storedCollection: StoredCollection<TournamentRepoMetadata, TournamentRepoItem>,


) {


    /**
     * Whether this repository has been closed.
     */
    @Volatile
    private var closed = false


    /**
     * A channel to pass commands to the I/O coroutine.
     */
    private val ioCommands = Channel<IoCommand>(capacity = UNLIMITED)


    /**
     * The stored compositions, mapped by their id.
     */
    private val compById: MutableMap<Int, StoredComposition> = storedCollection.items()
        .mapNotNull { (it as? TournamentRepoItem.Composition)?.storedComp }
        .sortedBy { it.id!! }  // Sort so that `compositions` are in the correct order (mutableMapTo preserves insertion order)
        .associateByTo(mutableMapOf()) { it.id!! }


    /**
     * The collection of stored compositions.
     */
    val compositions: Collection<StoredComposition>
        get() = compById.values


    /**
     * The highest known [StoredComposition] id.
     *
     * Whenever a new composition is created, we increment this and use the new value for the id of the new composition.
     */
    private val highestCompId = AtomicInt(
        compositions.maxOfOrNull { it.id!! } ?: 0
    )


    init {
        CoroutineScope(Dispatchers.IO).launch {
            for (command in ioCommands) {
                when (command) {
                    is IoCommand.WriteNewComp -> {
                        val compId = command.storedComp.id ?: error("Can't write composition with no id")
                        val metadata = storedCollection.userMetadata
                        if (compId > metadata.maxCompositionId)
                            storedCollection.setUserMetadata(metadata.copy(maxCompositionId = compId))
                        storedCollection.add(command.storedComp.asRepoItem())
                    }
                    is IoCommand.DeleteComps -> storedCollection.removeMatching {
                        (it is TournamentRepoItem.Composition) && (it.storedComp.id in command.compIds)
                    }
                    is IoCommand.ReplaceComp -> {
                        storedCollection.removeMatching {
                            (it is TournamentRepoItem.Composition) && (it.storedComp.id == command.compId)
                        }
                        storedCollection.add(command.updated.asRepoItem())
                    }
                }
            }
        }
    }


    /**
     * Throws an [IllegalStateException] if this repository is closed.
     */
    private fun checkNotClosed() {
        check(!closed) { "$this is closed" }
    }



    /**
     * Closes this repository.
     */
    fun close() {
        closed = true
        ioCommands.close()
    }


    /**
     * Adds a new composition to the repo, assigning it an id, and returns the id.
     */
    fun addComposition(storedComposition: StoredComposition): Int {
        checkNotClosed()

        val id = highestCompId.incrementAndFetch()
        storedComposition.id = id
        compById[id] = storedComposition

        ioCommands.trySend(IoCommand.WriteNewComp(storedComposition))

        return id
    }


    /**
     * Rewrites the composition with the id of [updated] with a new one.
     */
    fun updateComposition(id: Int, updated: StoredComposition) {
        assert(updated.id == id)

        compById[id] = updated
        ioCommands.trySend(IoCommand.ReplaceComp(id, updated))
    }


    /**
     * Deletes the given compositions.
     */
    fun deleteCompositions(compositionIds: Set<Int>) {
        checkNotClosed()

        compById.keys.removeAll(compositionIds)
        ioCommands.trySend(IoCommand.DeleteComps(compositionIds))
    }


    override fun toString(): String {
        return "TournamentRepository[id=${tournamentDesc.id}]"
    }


    companion object {


        /**
         * Creates a [TournamentRepository] for the given tournament descriptor, loading it from the given directory.
         */
        suspend fun create(
            tournamentDesc: TournamentDescriptor,
            tournamentsDirectory: File,
        ): TournamentRepository {
            return withContext(Dispatchers.IO) {
                val file = File(tournamentsDirectory, "${tournamentDesc.id}.dat")
                val storage = StoredCollection(
                    storageFile = file,
                    userMetadataSerializer = TournamentRepoMetadata.Serializer,
                    initialUserMetadata = { TournamentRepoMetadata(maxCompositionId = 0) },
                    userMetadataUpgrader = TournamentRepoMetadata.Serializer::upgrade,
                    itemSerializer = Serializer
                )
                TournamentRepository(tournamentDesc, storage)
            }
        }


    }


    /**
     * Encapsules commands for modifying the data stored on disk.
     */
    private sealed interface IoCommand {

        data class WriteNewComp(val storedComp: StoredComposition): IoCommand

        data class DeleteComps(val compIds: Collection<Int>): IoCommand

        data class ReplaceComp(val compId: Int, val updated: StoredComposition): IoCommand

    }


}


/**
 * The metadata we store in the tournament repo.
 */
private data class TournamentRepoMetadata(


    /**
     * The highest composition id that has ever been used.
     *
     * Note that it doesn't necessarily still exist, as it could have been deleted.
     */
    val maxCompositionId: Int


) {


    /**
     * The serializer for [TournamentRepoMetadata].
     */
    companion object Serializer: StoredCollection.Serializer<TournamentRepoMetadata> {


        override val itemFormatVersion = 1


        override fun readItem(input: DataInput, formatVersion: Int): TournamentRepoMetadata {
            val maxCompositionId = if (formatVersion >= 1) input.readInt() else -1
            return TournamentRepoMetadata(maxCompositionId)
        }


        override fun writeItem(output: DataOutput, item: TournamentRepoMetadata) {
            output.writeInt(item.maxCompositionId)
        }


        /**
         * Returns the [TournamentRepoItem] for the current format version, based on the value at the given version.
         */
        fun upgrade(
            @Suppress("UNUSED_PARAMETER") metadata: TournamentRepoMetadata,
            version: Int,
            items: Sequence<TournamentRepoItem>
        ): TournamentRepoMetadata {
            if (version == 0) {
                val maxCompositionid = items
                    .mapNotNull { (it as? TournamentRepoItem.Composition)?.storedComp }
                    .maxOfOrNull {
                        it.id!!
                    } ?: 0

                return TournamentRepoMetadata(maxCompositionid)
            }
            else {
                throw CorruptFitRepository("Unknown metadata format version: $version")
            }
        }


    }


}


/**
 * The union type for all types of objects we store in the repository.
 */
private sealed interface TournamentRepoItem {

    /**
     * The wrapper for stored compositions.
     */
    data class Composition(val storedComp: StoredComposition): TournamentRepoItem

}


/**
 * Returns a [TournamentRepoItem] wrapping the given [StoredComposition].
 */
private fun StoredComposition.asRepoItem() = TournamentRepoItem.Composition(this)


/**
 * The serializer of [TournamentRepoItem]s.
 */
private val Serializer = object: StoredCollection.Serializer<TournamentRepoItem> {


    /**
     * The type id of [TournamentRepoItem.Composition].
     */
    private val COMP_TYPE_ID = 1


    override val itemFormatVersion = StoredComposition.Serializer.itemFormatVersion


    override fun readItem(input: DataInput, formatVersion: Int): TournamentRepoItem {
        return when (val typeId = input.readInt()) {
            COMP_TYPE_ID -> StoredComposition.Serializer.readItem(input, formatVersion).asRepoItem()
            else -> throw CorruptTournamentRepository("Bad item type id: $typeId")
        }
    }


    override fun writeItem(output: DataOutput, item: TournamentRepoItem) {
        when (item) {
            is TournamentRepoItem.Composition -> {
                output.writeInt(COMP_TYPE_ID)
                StoredComposition.Serializer.writeItem(output, item.storedComp)
            }
        }
    }


}


/**
 * The exception thrown when the repository data is unexpected in some way.
 */
class CorruptTournamentRepository(message: String): IllegalArgumentException(message)


