-- Urho2D platformer example.
-- This sample demonstrates:
--    - Creating an orthogonal 2D scene from tile map file
--    - Displaying the scene using the Renderer subsystem
--    - Handling keyboard to move a character and zoom 2D camera
--    - Generating physics shapes from the tmx file's objects
--    - Mixing physics and translations to move the character
--    - Using Box2D Contact listeners to handle the gameplay
--    - Displaying debug geometry for physics and tile map
-- Note that this sample uses some functions from Sample2D utility class.

require "LuaScripts/Utilities/Sample"
require "LuaScripts/Utilities/2D/Sample2D"


function Start()
    -- Set filename for load/save functions
    demoFilename = "Platformer2D"

    -- Execute the common startup for samples
    SampleStart()

    -- Create the scene content
    CreateScene()

    -- Create the UI content
    CreateUIContent("PLATFORMER 2D DEMO")

    -- Hook up to the frame update events
    SubscribeToEvents()
end

function CreateScene()
    scene_ = Scene()

    -- Create the Octree, DebugRenderer and PhysicsWorld2D components to the scene
    scene_:CreateComponent("Octree")
    scene_:CreateComponent("DebugRenderer")
    scene_:CreateComponent("PhysicsWorld2D")

    -- Create camera
    cameraNode = Node()
    local camera = cameraNode:CreateComponent("Camera")
    camera.orthographic = true
    camera.orthoSize = graphics.height * PIXEL_SIZE
    camera.zoom = 1.8 * Min(graphics.width / 1280, graphics.height / 800) -- Set zoom according to user's resolution to ensure full visibility (initial zoom (1.8) is set for full visibility at 1280x800 resolution)

    -- Setup the viewport for displaying the scene
    renderer:SetViewport(0, Viewport:new(scene_, camera))
    renderer.defaultZone.fogColor = Color(0.2, 0.2, 0.2) -- Set background color for the scene

    -- Create tile map from tmx file
    local tileMapNode = scene_:CreateChild("TileMap")
    local tileMap = tileMapNode:CreateComponent("TileMap2D")
    tileMap.tmxFile = cache:GetResource("TmxFile2D", "Urho2D/Tilesets/Ortho.tmx")
    local info = tileMap.info

    -- Create Spriter Imp character (from sample 33_SpriterAnimation)
    CreateCharacter(info, true, 0.8, Vector3(1, 8, 0), 0.2)

    -- Generate physics collision shapes from the tmx file's objects located in "Physics" (top) layer
    local tileMapLayer = tileMap:GetLayer(tileMap.numLayers - 1)
    CreateCollisionShapesFromTMXObjects(tileMapNode, tileMapLayer, info)

    -- Instantiate enemies and moving platforms at each placeholder of "MovingEntities" layer (placeholders are Poly Line objects defining a path from points)
    PopulateMovingEntities(tileMap:GetLayer(tileMap.numLayers - 2))

    -- Instantiate coins to pick at each placeholder of "Coins" layer (placeholders for coins are Rectangle objects)
    PopulateCoins(tileMap:GetLayer(tileMap.numLayers - 3))

    -- Instantiate triggers (for ropes, ladders, lava, slopes...) at each placeholder of "Triggers" layer (placeholders for triggers are Rectangle objects)
    PopulateTriggers(tileMap:GetLayer(tileMap.numLayers - 4))

    -- Create background
    CreateBackgroundSprite(info, 3.5, "Textures/HeightMap.png", true)

    -- Check when scene is rendered
    SubscribeToEvent("EndRendering", HandleSceneRendered)
end

function HandleSceneRendered()
    UnsubscribeFromEvent("EndRendering")
    SaveScene(true) -- Save the scene so we can reload it later
    scene_.updateEnabled = false -- Pause the scene as long as the UI is hiding it
end

function SubscribeToEvents()
    -- Subscribe HandleUpdate() function for processing update events
    SubscribeToEvent("Update", "HandleUpdate")

    -- Subscribe HandlePostUpdate() function for processing post update events
    SubscribeToEvent("PostUpdate", "HandlePostUpdate")

    -- Subscribe to PostRenderUpdate to draw debug geometry
    SubscribeToEvent("PostRenderUpdate", "HandlePostRenderUpdate")

    -- Subscribe to Box2D contact listeners
    SubscribeToEvent("PhysicsBeginContact2D", "HandleCollisionBegin")
    SubscribeToEvent("PhysicsEndContact2D", "HandleCollisionEnd")

    -- Unsubscribe the SceneUpdate event from base class to prevent camera pitch and yaw in 2D sample
    UnsubscribeFromEvent("SceneUpdate")
end

function HandleUpdate(eventType, eventData)
    -- Zoom in/out
    if cameraNode ~= nil then
        Zoom(cameraNode:GetComponent("Camera"))
    end

    -- Toggle debug geometry with 'Z' key
    if input:GetKeyPress(KEY_Z) then drawDebug = not drawDebug end

    -- Check for loading / saving the scene
    if input:GetKeyPress(KEY_F5) then
        SaveScene(false)
    end
    if input:GetKeyPress(KEY_F7) then
        ReloadScene(false)
    end
end

function HandlePostUpdate(eventType, eventData)
    if character2DNode == nil or cameraNode == nil then
        return
    end
    cameraNode.position = Vector3(character2DNode.position.x, character2DNode.position.y, -10) -- Camera tracks character
end

function HandlePostRenderUpdate(eventType, eventData)
    if drawDebug then
        scene_:GetComponent("PhysicsWorld2D"):DrawDebugGeometry()

        local tileMapNode = scene_:GetChild("TileMap", true)
        local map = tileMapNode:GetComponent("TileMap2D")
        map:DrawDebugGeometry(scene_:GetComponent("DebugRenderer"), false)
    end
end

function HandleCollisionBegin(eventType, eventData)
    -- Get colliding node
    local hitNode = eventData["NodeA"]:GetPtr("Node")
    if hitNode.name == "Imp" then
        hitNode = eventData["NodeB"]:GetPtr("Node")
    end
    local nodeName = hitNode.name
    local character = character2DNode:GetScriptObject()

    -- Handle ropes and ladders climbing
    if nodeName == "Climb" then
        if character.isClimbing then -- If transition between rope and top of rope (as we are using split triggers)
            character.climb2 = true
        else
            character.isClimbing = true

            -- Override gravity so that the character doesn't fall
            local body = character2DNode:GetComponent("RigidBody2D")
            body.gravityScale = 0

            -- Clear forces so that the character stops (should be performed by setting linear velocity to zero, but currently doesn't work)
            body.linearVelocity = Vector2.ZERO
            body.awake = false
            body.awake = true
        end
    end

    if nodeName == "CanJump" then
        character.aboveClimbable = true
    end

    -- Handle coins picking
    if nodeName == "Coin" then
        hitNode:Remove()
        character.remainingCoins = character.remainingCoins - 1
        if character.remainingCoins == 0 then
            ui.root:GetChild("Instructions", true).text = "!!! Go to the Exit !!!"
        end
        ui.root:GetChild("CoinsText", true).text = character.remainingCoins -- Update coins UI counter
        PlaySound("Powerup.wav")
    end

    -- Handle interactions with enemies
    if nodeName == "Enemy" or nodeName == "Orc" then
        local animatedSprite = character2DNode:GetComponent("AnimatedSprite2D")
        local deltaX = character2DNode.position.x - hitNode.position.x

        -- Orc killed if character is fighting in its direction when the contact occurs (flowers are not destroyable)
        if nodeName == "Orc" and animatedSprite.animation == "attack" and (deltaX < 0 == animatedSprite.flipX) then
            hitNode:GetScriptObject().emitTime = 1
            if not hitNode:GetChild("Emitter", true) then
                hitNode:GetComponent("RigidBody2D"):Remove() -- Remove Orc's body
                SpawnEffect(hitNode)
                PlaySound("BigExplosion.wav")
            end

        -- Player killed if not fighting in the direction of the Orc when the contact occurs, or when colliding with a flower
        else
            if not character2DNode:GetChild("Emitter", true) then
                character.wounded = true
                if nodeName == "Orc" then
                    hitNode:GetScriptObject().fightTimer = 1
                end
                SpawnEffect(character2DNode)
                PlaySound("BigExplosion.wav")
            end
        end
    end

    -- Handle exiting the level when all coins have been gathered
    if nodeName == "Exit" and character.remainingCoins == 0 then
        -- Update UI
        local instructions = ui.root:GetChild("Instructions", true)
        instructions.text = "!!! WELL DONE !!!"
        instructions.position = IntVector2.ZERO

        -- Put the character outside of the scene and magnify him
        character2DNode.position = Vector3(-20, 0, 0)
        character2DNode:SetScale(1.2)
    end

    -- Handle falling into lava
    if nodeName == "Lava" then
        local body = character2DNode:GetComponent("RigidBody2D")
        body:ApplyLinearImpulse(Vector2(0, 1) * MOVE_SPEED, body.massCenter, true) -- Violently project character out of lava
        if not character2DNode:GetChild("Emitter", true) then
            character.wounded = true
            SpawnEffect(character2DNode)
            PlaySound("BigExplosion.wav")
        end
    end

    -- Handle climbing a slope
    if nodeName == "Slope" then
        character.onSlope = true
    end
end

function HandleCollisionEnd(eventType, eventData)
    -- Get colliding node
    local hitNode = eventData["NodeA"]:GetPtr("Node")
    if hitNode.name == "Imp" then
        hitNode = eventData["NodeB"]:GetPtr("Node")
    end
    local nodeName = hitNode.name
    local character = character2DNode:GetScriptObject()

    -- Handle leaving a rope or ladder
    if nodeName == "Climb" then
        if character.climb2 then
            character.climb2 = false
        else
            character.isClimbing = false
            local body = character2DNode:GetComponent("RigidBody2D")
            body.gravityScale = 1 -- Restore gravity
        end
    end

    if nodeName == "CanJump" then
        character.aboveClimbable = false
    end

    -- Handle leaving a slope
    if nodeName == "Slope" then
        character.onSlope = false
        -- Clear forces (should be performed by setting linear velocity to zero, but currently doesn't work)
        local body = character2DNode:GetComponent("RigidBody2D")
        body.linearVelocity = Vector2.ZERO
        body.awake = false
        body.awake = true
    end
end


-- Character2D script object class
Character2D = ScriptObject()

function Character2D:Start()
    self.wounded = false
    self.killed = false
    self.timer = 0
    self.maxCoins = 0
    self.remainingCoins = 0
    self.remainingLifes = 3
    self.isClimbing = false
    self.climb2 = false -- Used only for ropes, as they are split into 2 shapes
    self.aboveClimbable = false
    self.onSlope = false
end

function Character2D:Save(serializer)
    self.isClimbing = false -- Overwrite before auto-deserialization
end

function Character2D:Update(timeStep)
    if character2DNode == nil then
        return
    end

    -- Handle wounded/killed states
    if self.killed then
        return
    end

    if self.wounded then
        self:HandleWoundedState(timeStep)
        return
    end

    -- Set temporary variables
    local node = self.node
    local body = node:GetComponent("RigidBody2D")
    local animatedSprite = node:GetComponent("AnimatedSprite2D")
    local onGround = false
    local jump = false

    -- Collision detection (AABB query)
    local characterHalfSize = Vector2(0.16, 0.16)
    local collidingBodies = scene_:GetComponent("PhysicsWorld2D"):GetRigidBodies(Rect(node.worldPosition2D - characterHalfSize - Vector2(0, 0.1), node.worldPosition2D + characterHalfSize))

    if table.maxn(collidingBodies) > 1 and not self.isClimbing then
        onGround = true
    end

    -- Set direction
    local moveDir = Vector2.ZERO -- Reset

    if input:GetKeyDown(KEY_LEFT) or input:GetKeyDown(KEY_A) then
        moveDir = moveDir + Vector2.LEFT
        animatedSprite.flipX = false -- Flip sprite (reset to default play on the X axis)
    end

    if input:GetKeyDown(KEY_RIGHT) or input:GetKeyDown(KEY_D) then
        moveDir = moveDir + Vector2.RIGHT
        animatedSprite.flipX = true -- Flip sprite (flip animation on the X axis)
    end

    -- Jump
    if (onGround or self.aboveClimbable) and (input:GetKeyPress(KEY_UP) or input:GetKeyPress(KEY_W)) then
        jump = true
    end

    -- Climb
    if self.isClimbing then
        if not self.aboveClimbable and (input:GetKeyDown(KEY_UP) or input:GetKeyDown(KEY_W)) then
            moveDir = moveDir + Vector2.UP
        end

        if input:GetKeyDown(KEY_DOWN) or input:GetKeyDown(KEY_S) then
            moveDir = moveDir + Vector2.DOWN
        end
    end

    -- Move
    if not moveDir:Equals(Vector2.ZERO) or jump then
        if self.onSlope then
            body:ApplyForceToCenter(moveDir * MOVE_SPEED / 2, true) -- When climbing a slope, apply force (todo: replace by setting linear velocity to zero when will work)
        else
            node:Translate(Vector3(moveDir.x, moveDir.y, 0) * timeStep * 1.8)
        end
        if jump then
            body:ApplyLinearImpulse(Vector2(0, 0.17) * MOVE_SPEED, body.massCenter, true)
        end
    end

    -- Animate
    if input:GetKeyDown(KEY_SPACE) then
        if animatedSprite.animation ~= "attack" then
            animatedSprite:SetAnimation("attack", LM_FORCE_LOOPED)
            animatedSprite.speed = 1.5
        end
    elseif not moveDir:Equals(Vector2.ZERO) then
        if animatedSprite.animation ~= "run" then
            animatedSprite:SetAnimation("run")
        end
    elseif animatedSprite.animation ~= "idle" then
        animatedSprite:SetAnimation("idle")
    end
end

function Character2D:HandleWoundedState(timeStep)
    local node = self.node
    local body = node:GetComponent("RigidBody2D")
    local animatedSprite = node:GetComponent("AnimatedSprite2D")

    -- Play "hit" animation in loop
    if animatedSprite.animation ~= "hit" then
        animatedSprite:SetAnimation("hit", LM_FORCE_LOOPED)
    end

    -- Update timer
    self.timer = self.timer + timeStep

    -- End of timer
    if self.timer > 2 then
        -- Reset timer
        self.timer = 0

        -- Clear forces (should be performed by setting linear velocity to zero, but currently doesn't work)
        body.linearVelocity = Vector2.ZERO
        body.awake = false
        body.awake = true

        -- Remove particle emitter
        node:GetChild("Emitter", true):Remove()

        -- Update lifes UI and counter
        self.remainingLifes = self.remainingLifes - 1
        ui.root:GetChild("LifeText", true).text = self.remainingLifes -- Update lifes UI counter

        -- Reset wounded state
        self.wounded = false

        -- Handle death
        if self.remainingLifes == 0 then
            self:HandleDeath()
            return
        end

        -- Re-position the character to the nearest point
        if node.position.x < 15 then
            node.position = Vector3(1, 8, 0)
        else
            node.position = Vector3(18.8, 9.2, 0)
        end
    end
end

function Character2D:HandleDeath()
    local node = self.node
    local body = node:GetComponent("RigidBody2D")
    local animatedSprite = node:GetComponent("AnimatedSprite2D")

    -- Set state to 'killed'
    self.killed = true

    -- Update UI elements
    local instructions = ui.root:GetChild("Instructions", true)
    instructions.text = "!!! GAME OVER !!!"
    ui.root:GetChild("ExitButton", true).visible = true
    ui.root:GetChild("PlayButton", true).visible = true

    -- Show mouse cursor so that we can click
    input.mouseVisible = true

    -- Put character outside of the scene and magnify him
    node.position = Vector3(-20, 0, 0)
    node:SetScale(1.2)

    -- Play death animation once
    if animatedSprite.animation ~= "dead2" then
        animatedSprite:SetAnimation("dead2")
    end
end
