/**
 * NOTE. I don't like this class! Because if there's a bug in *any* code, the Editor cannot start up.
 * Either I should ensure that the Editor can start, even though the "Layouts" cannot be created correctly,
 * or :
 * Rename this to just "Grid", have PlayDirector have a Grid field, and use a regular GameStage.
 *
 *
 *
 * A stage, whos Actors are arranged in a grid. Each square typically has only one actor.
 *
 * All of the actors within the GridStage are assumed to be of type Item.
 * Using other roles won't work correctly.
 *
 * The way "tick" is handled by Item is slightly different to a regular Role.
 * Item.myTurn() is called whenever the item can choose a new action.
 * However, while an Item is moving from one square to another, Item.myTurn() is NOT called.
 * Instead, the movement is performed by replacing the ActionRole's Action with a
 * Movement (which implements Action).
 *
 * In order for the game to be predictable (such as which rock should roll into a gap, the left or right one?)
 * the Actor's ticks are performed according to their position within the grid.
 * Ticks start at the top left, moving right along a row, and then moving down to subsequent rows.
 *
 * The Player Role is special, it is performed before everything else in the grid.
 * This is so that there isn't any weird behaviour, such as cars to the left of the Player acting differently
 * to the cars to the right (due to the left car ticking BEFORE the Player, and the right car ticking AFTER).
 * 
 * Each square can have up to three Items. The occupant, and alternateOccupant, and an entrant.
 * alternateOccupants are static (not Movable) Items, such as Soil, Coin, Stamp etc.
 * occupants are Movable Items, such as Player, Rock etc.
 * When a Movable Item is in the process of moving from one square to the next, it is the OCCUPANT of the
 * square it is moving FROM, and the ENTRANT of the square it is moving TO.
 * During this process, the Item.square refers to the square it is moving FROM.
 * During a move, Movable.movement is set, and the Movement contains more information, including the toSquare.
 *
 * Half way through a Movement, the occupant of the toSquare, is informed of the imminent invasion via 
 * Item.invading( Movement ) method. This is typically used to start an animation which "destroys" the
 * item being invaded.
 *
 * At the end of the Movement, the occupant of the toSquare is informated via Item.invaded( Movement ).
 * This can be used to kill the invaded item, if not already done so at the end of the animation kicked of
 * from Item.invading.
 * The item moving is informed of its arrival via Item.moved( Movement ) method.
 * The base implementation in Movable.moved() calls :
 *
 *     replaceAction( createAction() )
 *
 * Many Items need to look around to determine their actions.
 * For Movable Items, this is typically to test if it can move in a given direction.
 * e.g. A Rock looks south to see if it is empty (or squashable).
 * Looking around is more complex than you may imagine. If there is another falling Rock
 * to the south, then top-most Rock should still fall, because it knows that the other Rock
 * will have moved out of the way, so the Square WILL be empty (but currently isn't).
 * To make the game code simple, the look methods do fancy tricks...
 * 
 * Each look call has a SPEED, which defaults to the Movable.speed or
 * Item.IMMEDIATELY for non-movable items.
 * If the occupant at the square being looked at is moving out, then it will be ignored if
 * it will have left the square by the time invader gets to its new square.
 * Some Movables have two speeds, e.g. a Ballon moves sidewards at a different speed to upwards.
 * In such cases, be sure NOT to use the default speed when it would move at a non-default speed.
 * 
 * As previously mentioned, each Square can have upto three Items : occupant, alternateOccupant and entrant.
 * When looking at a Square, which one is returned?
 * Most of the time, the Square will return one of those three, but there are other possibilities :
 * 
 * Outside : A special Item, when you look outside of the bounds of the grid.
 *
 * Empty : Another special Item, when there are no Items in the Square 
 * (or only a moving ones that will leave, and therefore can be ignored).
 *
 * LookedAtTwoItems : This is the tricky one to deal with! Some items Movable Items, such as
 * as Bee can co-exist with stationary items, such as soil. In which case, look returns
 * a LookedAtTwoItems.
 * LookedAtTwoItems is also returned when a Movable is moving into an already occupied square.
 *
 * Many attributes of LookedAtTwoItems are straight forward. For example squashable( int direction )
 * and rounded( int direction ) are true if both items are squashable (or rounded).
 *
 * Others tests are not so simple.
 * In particular, do NOT use :
 *
 *     look(xxx) is MyItem
 *
 * because this will never be true when a LookedAtTwoItems is returned. (Though simetimes this may be
 * the behavior you want).
 *
 * Instead, consider using :
 * 
 *     look(xxx).item() is MyItem
 *
 * item() is a no-op for simple Items, but returns the "main/first" Item of LookedAtTwoItems.
 *
 * NOTE. The look methods return a LookedAt, not an Item, so do not use methods/attributes of an
 * Item, that are not part of the LookedAt interface.
 * These can cause rare (hard to reproduce) bugs, because the code will only fail with a
 * LookedAtTwoItems. This is one of the times that using a dynamically typed Scripting language sucks.
 * To mitigate this, I should introduce a debug mode, where most often a LookedAtOneItem is returned.
 * If any tests fail in this mode, then they will probably fail at some time in "normal" mode.
 */
class GridStage : GameStage {

    @Attribute
    var gridSize = 60

    var offsetX = 0

    var offsetY = 0

    var across = 0

    var down =  0

    var squares = Array<Square>(0,0)

    val outsideSquare = OutsideSquare(this)

    val roleList = listOf<Item>()
    val playerList = listOf<Item>()

    fun squareAtPoint( x : double, y : double ) : Square {
        return square( Math.round(((x - offsetX) / gridSize)), Math.round(((y - offsetY) / gridSize)) )
    }

    fun square( x : int, y : int ) : Square {
        if ( x < 0 || y < 0 || x >= across || y >= down ) {
            return outsideSquare
        } else {
            return squares[x][y]
        }
    }

    override fun begin() {
        var minX = 1000
        var maxX = -1000
        var minY = 1000.0
        var maxY = -1000.0
        for ( actor in actors ) {
            if (actor.x < minX) minX = actor.x
            if (actor.x > maxX) maxX = actor.x
            if (actor.y < minY) minY = actor.y
            if (actor.y > maxY) maxY = actor.y
        }
        offsetX = minX
        offsetY = minY
        across = Math.ceil( (maxX - minX) / gridSize ) + 1
        down = Math.ceil( (maxY - minY) / gridSize ) + 1

        if (across < 1) across = 1
        if (down < 1) down = 1

        squares = Array<Square>(across,down)

        for ( y in 0 until down ) {
            for ( x in 0 until across ) {
                squares[x][y] = Square(this, x, y)
            }
        }

        super.begin()
    }

    fun addSquare ( square : Square ) {
        val occupant = square.occupant
        if ( occupant != null ) {
            if ( occupant.isPlayer() ) {
                playerList.add(occupant)
            } else {
                roleList.add(occupant)
            }
        }
        if ( square.alternateOccupant != null ) {
            roleList.add(square.alternateOccupant)
        }
    }

    fun buildLists() {
        for ( y in down-1 downTo 0 ) {
            for ( x in 0 until across ) {
                addSquare( squares[x][y])
            }
        }
    }

    fun tickRole( role : Role ) {
         try {
            if (role.actor.stage != null) { // Prevent tick from newly dead actors.
                role.tick()
            }
        } catch (e: Exception ) {
            e.printStackTrace()
            role.actor.die() // Kill off actors that throw an exception
        }
    }

    fun tickRoleList() {
        for ( role in roleList ) {
            tickRole(role)
        }
    }

    fun tickPlayerList() {
        for ( role in playerList) {
             tickRole(role)
        }
    }

    fun tickLists() {
        tickPlayerList()
        tickRoleList()
    }

    /**
     * We need a consistant order of ticks (left to right, then down a line).
     * Roles are added to a list, so that movement does not cause one role to be ticked twice in a
     * single frame.
     */
    override fun tick() {
        roleList.clear()
        playerList.clear()
        buildLists()
        tickLists()

        roleList.clear()
        playerList.clear()
    }
    

    fun willMiss( item : Item, speed : int, direction : int ) : boolean {

        if (speed == 0 || ! item.isMoving() ) return false
        val movableItem = item as Movable

        // Heading INTO each other.
        if ( ( direction >= 0 ) && ( (movableItem.movement().direction + 2) % 4 == direction ) ) return false

        // If the movement is in the same direction, they can be nose to tail.
        // If the movement is at right angles, then the follower must wait extra time, before it can enter
        // This extra time may vary depending on the type of game, and therefore sub classes of GridStage may change this.
        val extra = willMissExtra( movableItem, movableItem.movement().direction == direction, speed )

        // So item is moving out of the square. Will it exit before the new entrant can get here?
        val distance = gridSize - movableItem.movement().moved
        val time = Math.ceil( distance / movableItem.movement().speed )  // Time in ticks

        return time + extra < Math.ceil( gridSize / speed )
    }

    // This is an extra amount that time needed for two items travelling at right angles within
    // the same square. The default is to make them wait the time it takes to move half a square.
    // In this way, the items won't overlap (much) for example when one is moving out North,
    // and another tries to move in East or West.
    // This can be overriden. For example, I may make CavernQuest always return zero.
    //
    // Return a time in ticks.
    //
    fun willMissExtra( item: Item, sameDirection : boolean, speed : int ) : int {
        return if (sameDirection)  0 else (gridSize ~/2 ~/ speed)
    }

}

