
#include "mob_player.hh"

#include <algorithm>

#include "itty/paths.hh"
#include "itty/input.hh"

#include "conf.hh"
#include "mob_effect.hh"
#include "strings.hh"
#include "scene.hh"

// Left/right movement: top speed and acceleration 
static const float _running_speed = 3.f;
static const float _land_acceleration = 0.25f;
static const float _land_deceleration = 0.5f;
static const float _air_acceleration = 0.25f;
static const float _air_deceleration = 0.1f;

// Jumping: total impulse added to y when jumping, and additional impulse added
// to x when jumping off a wall.
static const float _full_jump_impulse = -8.f;
static const float _wall_jump_impulse = 3.f;

// Floatiness: when close to the top of a jump arc (as long as the absolute
// value of v.y is under the threshold), keeping the jump button pressed
// applies an additional upwards acceleration to the player.
static const float _jump_peak_threshold = 3.f;
static const float _jump_peak_floatiness = 1.f;

// Grip: when touching a wall and moving downwards, the player's vertical speed
// can't exceed this value.
static const float _max_sliding_speed = 1.0f;

// Coyote time in seconds
static const float _coyote_time = 0.1f;

// Duration of the land animation in seconds
static const float _land_animation_time = 0.05f;

// Jump buffering time in seconds
static const float _jump_buffer_time = 0.2f;

// Invincibility time after getting hit in seconds
static const float _invincibility_frames = 0.5f;

// Amount of idle time required before health regenerates
static const float _recovery_time = 1.f;

// Impulse added by a hit
static const itty::Vec _hurt_impulse{3.f, -6.f};

// Time that the attack animation lasts
static const float _pre_attack_time = 0.5f;
static const float _attack_time = 0.4f;
static const float _post_attack_time = 0.3f;

static itty::Vec _head_positions[] = {
        // idle
        { 0, -9 },
        // walk
        { 0, -8 }, { 1, -8 }, { 0, -8 }, { 0, -9 }, { 0, -9 },
        { 0, -8 }, { 1, -8 }, { 0, -8 }, { 0, -9 }, { 0, -9 },
        // run
        { 2, -8 }, { 2, -6 }, { 2, -8 }, { 2, -10 }, { 2, -9 },
        { 2, -8 }, { 2, -6 }, { 2, -8 }, { 2, -10 }, { 2, -9 },
        // jump
        { 1, -8 }, { 1, -6 }, { 1, -9 }, { 1, -9 }, { 1, -9 },
        { 1, -8 }, { 1, -6 }, { 1, -9 }, { 1, -9 }, { 1, -9 },
        // skid
        { 0, -8 },
        // hurt
        { 1, -7 }, { 2, -6 },
        // reel
        { 0, -9 },
        // attack
        { -2, -8 }, { 2, -8 }, { 2, -8 }, { 2, -8 },
};

static int _head_frames[] = {
        0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0,
        1, 0,
        2,
        0, 0, 0, 0,
};

Mob_player::Mob_player(Mobs& mobs, const itty::Box& box, Place*) : Mob{box, SOLID|PHYS}
{
        brush_idle = mobs.load_frames("player_idle.png", 1, 1);
        brush_walk = mobs.load_frames("player_walk.png", 10, 1);
        brush_run = mobs.load_frames("player_run.png", 10, 1);
        brush_jump = mobs.load_frames("player_jump.png", 10, 1);
        brush_skid = mobs.load_frames("player_skid.png", 1, 1);
        brush_hurt = mobs.load_frames("player_hurt.png", 2, 1);
        brush_reel = mobs.load_frames("player_reel.png", 1, 1);
        brush_attk = mobs.load_frames("player_attk.png", 4, 1);
        brush_head = mobs.load_frames("player_head.png", 3, 1);

        sound_step = mobs.load_audio("step.wav");
        sound_jump = mobs.load_audio("jump.wav");
        sound_land = mobs.load_audio("land.wav");
        sound_skid = mobs.load_audio("skid.wav");
        sound_dunk = mobs.load_audio("dunk.wav");
        sound_hurt = mobs.load_audio("hurt.wav");
        sound_attk = mobs.load_audio("attk.wav");

        mobs.pc = this;
}

bool Mob_player::update(Scene& sc)
{
        prev_frame = frame;

        update_rest(sc);
        update_move(sc);
        update_jump(sc);
        update_frame(sc);
        update_effects(sc);
        update_health(sc);
        update_attack(sc);

        if (land > 0)
                land--;
        if (jump > 0)
                jump--;
        if (rest > 0)
                rest--;
        if (hurt > 0)
                hurt--;
        if (idle > 0)
                idle--;
        if (attk > 0)
                attk--;

        sc.phys.add_engine(this, itty::Vec{0, 1}, INFINITY, 1.6f);
        return false;
}

void Mob_player::render(Mobs& mobs)
{
        auto mat = itty::Mat::translate(p.floor());
        if (is(FLIP))
                mat *= itty::Mat::scale(-1, 1);
        mobs.quads.push(frame, mat);
        mat *= itty::Mat::translate(_head_positions[frame]);
        mobs.quads.push(brush_head + _head_frames[frame], mat);
}

// The rest counter is coyote time for the HIT_DN flag.
// when it is reset from being zero, play the landing sound and create some
// dust particles to indicate that the player landed.

void Mob_player::update_rest(Scene& sc)
{
        if (is(HIT_DN)) {
                if (rest == 0) {
                        land = itty::conf.fps * _land_animation_time;
                        MIX_PlayAudio(itty::mixer, sound_land);
                        Mob_dust::make_from(sc.mobs, this);
                }
                rest = itty::conf.fps * _coyote_time;
        }
}

// The desired horizontal speed is calculated from the player input and the
// maximum running speed.
//
// The player is considered to be accelerating when the desired horizontal
// speed is not zero, and decelerating otherwise; in either case, there's a
// different acceleration value depending on the player being resting or not.
//
// When touching a wall, the vertical speed of the player is clamped to the
// maximum sliding speed.

void Mob_player::update_move(Scene& sc)
{
        float a = is(HIT_DN) ? _land_acceleration : _air_acceleration;
        float d = is(HIT_DN) ? _land_deceleration : _air_deceleration;
        float s = (itty::inputs[Input_right].value - itty::inputs[Input_left].value) * _running_speed;

        if (s < 0)
                set(FLIP);
        if (s > 0)
                unset(FLIP);

        if (s == 0 || attk)
                sc.phys.add_engine(this, itty::Vec{1, 0}, 0, d);
        else
                sc.phys.add_engine(this, itty::Vec{1, 0}, s, a);

        if ((is(HIT_LF) || is(HIT_RT)) && v.y > _max_sliding_speed)
                v.y = _max_sliding_speed;
}

// The jump value is a buffer for the pressing of the button; a jump can start
// while the jump value is greater than zero if the player is in a situation
// where it can happen:
//
// If the player is resting (that is, has touched the ground very recently), a
// land jump can happen.
//
// Otherwise, if the player is touching a wall, a wall jump can happen.
//
// In both cases, a successful jump sets the JUMPING bit. The bit persists
// until the player releases the jump button.
//
// The player can extend a jump by keeping the button pressed: if the JUMPING
// bit is on, when the player's vertical speed is in the jump peak threshold
// range, an additional force is applied upwards.

void Mob_player::update_jump(Scene& sc)
{
        if (jump) {
                if (rest) {
                        v.y = _full_jump_impulse;
                        set(JUMPING);
                        rest = 0;
                } else if (is(HIT_LF)) {
                        v.x = _wall_jump_impulse;
                        v.y = _full_jump_impulse;
                        set(JUMPING);
                        unset(FLIP);
                } else if (is(HIT_RT)) {
                        v.x = -_wall_jump_impulse;
                        v.y = _full_jump_impulse;
                        set(JUMPING);
                        set(FLIP);
                }
                if (is(JUMPING)) {
                        jump = 0;
                        MIX_PlayAudio(itty::mixer, sound_jump);
                }
        }
        if (is(JUMPING) && fabsf(v.y) < _jump_peak_threshold)
                sc.phys.add_engine(this, itty::Vec{0, -1}, INFINITY, _jump_peak_floatiness);
}

// if the player released the attack button recently, they are in the process
// of attacking mobs around themselves.

void Mob_player::update_attack(Scene& sc)
{
        if (attk < itty::conf.fps * _attack_time && attk > itty::conf.fps * _post_attack_time) {
                if (!is(HIT_DN))
                        hit = itty::Box::from_center(p + itty::Vec{0, -8}, itty::Vec{16, 8});
                else if (is(FLIP))
                        hit = itty::Box::from_center(p + itty::Vec{-8, 0}, itty::Vec{8, 4});
                else
                        hit = itty::Box::from_center(p + itty::Vec{8, 0}, itty::Vec{8, 4});
        } else {
                hit = itty::Box{INFINITY, INFINITY, INFINITY, INFINITY};
        }
}

// The actual animation frame used by the player depends on many internal
// variables, in order:
//
// If the player was very recently hurt, the "reel" animation is used
//
// If the player is not standing on the floor, the "jump" animation is used;
// the actual frame depends on the vertical speed.
//
// If the player landed very recently, a specific landing animation shows very
// briefly.
//
// If horizontal speed is zero, the player is either "idle" (if totally
// healthy) or "hurt" (if not)
//
// If horizontal speed is opposite to the FLIP flag, the "skid" animation is
// used.
//
// If the absolute value of the horizontal speed is small enough, the "walk"
// animation is used; the frame depends on the horizontal coordinate.
//
// Otherwise, the "run" animation is used; the frame depends on the horizontal
// coordinate.

void Mob_player::update_frame(Scene& sc)
{
        if (attk >= itty::conf.fps * _attack_time)
                frame = brush_attk;
        else if (attk > itty::conf.fps * _post_attack_time)
                frame = brush_attk + (is(HIT_DN) ? 2 : 3);
        else if (hurt > itty::conf.fps * _invincibility_frames * 0.5f)
                frame = brush_reel;
        else if (attk)
                frame = brush_attk + 1;
        else if (rest == 0)
                frame = brush_jump + std::clamp(((v.y + 8) / 1.5f), 0.f, 9.f);
        else if (land != 0)
                frame = brush_run + 1;
        else if (v.x == 0)
                frame = (health == 127) ? brush_idle : (brush_hurt + !(sc.mobs.frame & 8));
        else if ((v.x < 0) != is(FLIP))
                frame = brush_skid;
        else if (v.x > -1.5 && v.x < 1.5)
                frame = brush_walk + ((size_t)(is(FLIP) ? -p.x : p.x) / 6) % 10;
        else
                frame = brush_run + ((size_t)(is(FLIP) ? -p.x : p.x) / 6) % 10;
}

// Several sounds and particle effects can be generated in specific conditions
//
// if entering one of the frames where the foot touches the ground, play step
// if entering the braking frame play skid
// if entering a running frame where the foot touches the ground make dust
// if entering the landing frame make dust
// if skidding maybe make dust

void Mob_player::update_effects(Scene& sc)
{
        if (land)
                return;

        if (prev_frame != frame) {
                if (frame == brush_walk + 1 || frame == brush_walk + 6 ||
                    frame == brush_run + 1 || frame == brush_run + 6)
                        MIX_PlayAudio(itty::mixer, sound_step);
                if (frame == brush_skid)
                        MIX_PlayAudio(itty::mixer, sound_skid);
        }

        if (is(HIT_DN)) {
                if (prev_frame != frame)
                        if (frame == brush_run + 1 || frame == brush_run + 6)
                                Mob_dust::make_from(sc.mobs, this);

                if (frame == brush_skid && is(HIT_DN))
                        if (SDL_randf() < 0.5f)
                                Mob_dust::make_from(sc.mobs, this);
        }
}

// If the player's speed is not zero, the idle counter is set to recovery time;
// the counter is decremented by one every frame elsewhere
//
// If this is a 30fps tick, the player is idle, and its health is lower than
// the max value, increment health by one.

void Mob_player::update_health(Scene& sc)
{
        if (health <= 0)
                set(DOWN);
        if (v != itty::Vec{0, 0} || attk)
                idle = itty::conf.fps * _recovery_time;
        if (sc.mobs.tick && idle == 0 && health < 127)
                health++;
}

void Mob_player::begin_jump()
{
        jump = itty::conf.fps * _jump_buffer_time;
}

void Mob_player::end_jump()
{
        jump = 0;
        unset(JUMPING);
}

void Mob_player::begin_attack()
{
        if (attk == 0) {
                attk = itty::conf.fps * _pre_attack_time;
                MIX_PlayAudio(itty::mixer, sound_attk);
        }
}

void Mob_player::end_attack()
{
}

// If the invincibility counter is zero and the princess's body box intersects
// with the given box, she takes damage: play the appropriate sound and reset
// the invincibility counter.

bool Mob_player::harm(const itty::Box& hit)
{
        if (hurt > 0 || !hit.overlaps(box))
                return false;

        v.x = hit.ct().x < p.x ? _hurt_impulse.x : -_hurt_impulse.x;
        v.y = _hurt_impulse.y;
        MIX_PlayAudio(itty::mixer, sound_hurt);
        hurt = itty::conf.fps * _invincibility_frames;
        health -= 32;
        return true;
}

void Mob_player::reset()
{
        unset(DOWN);
        v = itty::Vec{0, 0};
        rest = 0;
        land = 0;
        jump = 0;
        hurt = 0;
        idle = 0;
        health = 127;
        frame = 0;
        prev_frame = 0;
}

