package net.torvald.terrarum

import com.badlogic.gdx.Screen
import com.badlogic.gdx.graphics.g2d.SpriteBatch
import com.badlogic.gdx.utils.Queue
import net.torvald.terrarum.AppLoader.printdbg
import net.torvald.terrarum.gameactors.Actor
import net.torvald.terrarum.gameworld.GameWorld
import net.torvald.terrarum.modulebasegame.gameactors.ActorHumanoid
import net.torvald.terrarum.realestate.LandUtil
import net.torvald.terrarum.ui.ConsoleWindow
import net.torvald.util.SortedArrayList
import java.util.concurrent.locks.Lock

/**
 * Although the game (as product) can have infinitely many stages/planets/etc., those stages must be manually managed by YOU;
 * this instance only stores the stage that is currently being used.
 */
open class IngameInstance(val batch: SpriteBatch) : Screen {

    var screenZoom = 1.0f
    val ZOOM_MAXIMUM = 4.0f
    val ZOOM_MINIMUM = 1.0f

    open var consoleHandler: ConsoleWindow = ConsoleWindow()

    var paused: Boolean = false
    val consoleOpened: Boolean
        get() = consoleHandler.isOpened || consoleHandler.isOpening

    init {
        consoleHandler.setPosition(0, 0)

        printdbg(this, "New ingame instance ${this.hashCode()}, called from")
        printStackTrace(this)
    }

    open var world: GameWorld = GameWorld.makeNullWorld()
        set(value) {
            printdbg(this, "Ingame instance ${this.hashCode()}, accepting new world ${value.layerTerrain}; called from")
            printStackTrace(this)

            field = value
        }
    /** how many different planets/stages/etc. are thenre. Whole stages must be manually managed by YOU. */
    var gameworldCount = 0
    /** The actor the game is currently allowing you to control.
     *
     *  Most of the time it'd be the "player", but think about the case where you have possessed
     *  some random actor of the game. Now that actor is now actorNowPlaying, the actual gamer's avatar
     *  (reference ID of 0x91A7E2) (must) stay in the actorContainerActive, but it's not a actorNowPlaying.
     *
     *  Nullability of this property is believed to be unavoidable (trust me!). I'm sorry for the inconvenience.
     */
    open var actorNowPlaying: ActorHumanoid? = null
    /**
     * The actual gamer
     */
    val actorGamer: ActorHumanoid
        get() = getActorByID(Terrarum.PLAYER_REF_ID) as ActorHumanoid

    open var gameInitialised = false
        internal set
    open var gameFullyLoaded = false
        internal set

    val ACTORCONTAINER_INITIAL_SIZE = 64
    val actorContainerActive = SortedArrayList<Actor>(ACTORCONTAINER_INITIAL_SIZE)
    val actorContainerInactive = SortedArrayList<Actor>(ACTORCONTAINER_INITIAL_SIZE)

    protected val terrainChangeQueue = Queue<BlockChangeQueueItem>()
    protected val wallChangeQueue = Queue<BlockChangeQueueItem>()
    protected val wireChangeQueue = Queue<BlockChangeQueueItem>()

    override fun hide() {
    }

    override fun show() {
        // the very basic show() implementation
        gameInitialised = true
    }

    override fun render(updateRate: Float) {
    }

    override fun pause() {
    }

    override fun resume() {
    }

    override fun resize(width: Int, height: Int) {
    }

    /**
     * You ABSOLUTELY must call this in your child classes (```super.dispose()```) and the AppLoader to properly
     * dispose of the world, which uses unsafe memory allocation.
     * Failing to do this will result to a memory leak!
     */
    override fun dispose() {
        printdbg(this, "Thank you for properly disposing the world!")
        printdbg(this, "dispose called by")
        printStackTrace(this)

        world.dispose()
    }

    ////////////
    // EVENTS //
    ////////////

    /**
     * Event for triggering held item's `startPrimaryUse(Float)`
     */
    open fun worldPrimaryClickStart(delta: Float) {
    }

    /**
     * Event for triggering held item's `endPrimaryUse(Float)`
     */
    open fun worldPrimaryClickEnd(delta: Float) {
    }

    /**
     * I have decided that left and right clicks must do the same thing, so no secondary use from now on. --Torvald on 2019-05-26
     *
     * Event for triggering held item's `startSecondaryUse(Float)`
     */
    //open fun worldSecondaryClickStart(delta: Float) { }

    /**
     * I have decided that left and right clicks must do the same thing, so no secondary use from now on. --Torvald on 2019-05-26
     *
     * Event for triggering held item's `endSecondaryUse(Float)`
     */
    //open fun worldSecondaryClickEnd(delta: Float) { }

    /**
     * Event for triggering fixture update when something is placed/removed on the world.
     * Normally only called by GameWorld.setTileTerrain
     *
     * Queueing schema is used to make sure things are synchronised.
     */
    open fun queueTerrainChangedEvent(old: Int, new: Int, position: Long) {
        val (x, y) = LandUtil.resolveBlockAddr(world, position)
        terrainChangeQueue.addFirst(BlockChangeQueueItem(old, new, x, y))
    }

    /**
     * Wall version of terrainChanged() event
     */
    open fun queueWallChangedEvent(old: Int, new: Int, position: Long) {
        val (x, y) = LandUtil.resolveBlockAddr(world, position)
        wallChangeQueue.addFirst(BlockChangeQueueItem(old, new, x, y))
    }

    /**
     * Wire version of terrainChanged() event
     *
     * @param old previous settings of conduits in bit set format.
     * @param new current settings of conduits in bit set format.
     */
    open fun queueWireChangedEvent(old: Int, new: Int, position: Long) {
        val (x, y) = LandUtil.resolveBlockAddr(world, position)
        wireChangeQueue.addFirst(BlockChangeQueueItem(old, new, x, y))
    }



    ///////////////////////
    // UTILITY FUNCTIONS //
    ///////////////////////

    fun getActorByID(ID: Int): Actor {
        if (actorContainerActive.size == 0 && actorContainerInactive.size == 0)
            throw IllegalArgumentException("Actor with ID $ID does not exist.")

        var actor = actorContainerActive.searchFor(ID) { it.referenceID!! }
        if (actor == null) {
            actor = actorContainerInactive.searchFor(ID) { it.referenceID!! }

            if (actor == null) {
                /*JOptionPane.showMessageDialog(
                        null,
                        "Actor with ID $ID does not exist.",
                        null, JOptionPane.ERROR_MESSAGE
                )*/
                throw IllegalArgumentException("Actor with ID $ID does not exist.")
            }
            else
                return actor
        }
        else
            return actor
    }

    //fun SortedArrayList<*>.binarySearch(actor: Actor) = this.toArrayList().binarySearch(actor.referenceID!!)
    //fun SortedArrayList<*>.binarySearch(ID: Int) = this.toArrayList().binarySearch(ID)

    open fun removeActor(ID: Int) = removeActor(getActorByID(ID))
    /**
     * get index of the actor and delete by the index.
     * we can do this as the list is guaranteed to be sorted
     * and only contains unique values.
     *
     * Any values behind the index will be automatically pushed to front.
     * This is how remove function of [java.util.ArrayList] is defined.
     */
    open fun removeActor(actor: Actor?) {
        if (actor == null) return

        val indexToDelete = actorContainerActive.searchFor(actor.referenceID!!) { it.referenceID!! }
        if (indexToDelete != null) {
            actorContainerActive.remove(indexToDelete)
        }
    }

    /**
     * Check for duplicates, append actor and sort the list
     */
    open fun addNewActor(actor: Actor?) {
        if (actor == null) return

        if (theGameHasActor(actor.referenceID!!)) {
            throw Error("The actor $actor already exists in the game")
        }
        else {
            actorContainerActive.add(actor)
        }
    }

    fun isActive(ID: Int): Boolean =
            if (actorContainerActive.size == 0)
                false
            else
                actorContainerActive.searchFor(ID) { it.referenceID!! } != null

    fun isInactive(ID: Int): Boolean =
            if (actorContainerInactive.size == 0)
                false
            else
                actorContainerInactive.searchFor(ID) { it.referenceID!! } != null

    /**
     * actorContainerActive extensions
     */
    fun theGameHasActor(actor: Actor?) = if (actor == null) false else theGameHasActor(actor.referenceID!!)

    fun theGameHasActor(ID: Int): Boolean =
            isActive(ID) || isInactive(ID)




    data class BlockChangeQueueItem(val old: Int, val new: Int, val posX: Int, val posY: Int)
}

inline fun Lock.lock(body: () -> Unit) {
    this.lock()
    try {
        body()
    }
    finally {
        this.unlock()
    }
}
