#include "doctor_frenzied.h"

#include "../../ObjManager.h"
#include "../../common/misc.h"
#include "../../game.h"
#include "../../graphics/Renderer.h"
#include "../../player.h"
#include "../../sound/SoundManager.h"
#include "../ai.h"
#include "../stdai.h"
#include "doctor.h"
#include "doctor_common.h"

using namespace NXE::Graphics;

enum STATES
{
  // 0-9 are used by in-game scripts during intro
  // 500+ is triggered for defeat sequence

  STATE_BASE = 10, // fall; once on ground for a moment pick next attack

  STATE_JUMP           = 20, // jumps at player
  STATE_JUMP_WITH_GP   = 30, // taller jump and enable "ground pound"
  STATE_IN_AIR         = 40, // in air
  STATE_IN_AIR_WITH_GP = 50, // in air and will "ground pound" if passes over player
  STATE_LANDED         = 60, // landed from a jump

  STATE_RED_DASH  = 70, // red dash; try to hit player with elbow
  STATE_MEGA_BATS = 80, // arms out & spawn mega bats

  STATE_TELEPORT = 90, // teleport away and reappear over players head

  STATE_DEFEATED = 500, // script constant
  STATE_DISSOLVE = 510  // also scripted
};

#define DAMAGE_NORMAL 5
#define DAMAGE_RED_DASH 10

// the Doctor repeats this series of attacks over and over,
// when he reaches the -1, it starts over at the beginning.
static const int attack_pattern[]
    = {STATE_JUMP_WITH_GP, STATE_TELEPORT,  STATE_RED_DASH,     STATE_JUMP, STATE_JUMP, STATE_RED_DASH,
       STATE_TELEPORT,     STATE_MEGA_BATS, STATE_JUMP_WITH_GP, STATE_JUMP, STATE_JUMP, -1};

INITFUNC(AIRoutines)
{
  ONTICK(OBJ_BOSS_DOCTOR_FRENZIED, ai_boss_doctor_frenzied);
  ONTICK(OBJ_DOCTOR_BAT, ai_doctor_bat);
}

/*
void c------------------------------() {}
*/

static void run_jumps(Object *o)
{

  switch (o->state)
  {
    // jump at player
    case STATE_JUMP:
    {
      o->frame = 3;
      FACEPLAYER;

      if (++o->timer > 20)
      {
        o->state     = STATE_IN_AIR;
        o->frame     = 4;
        o->animtimer = 0;

        o->yinertia = -0x600;
        XMOVE(0x400);
      }
    }
    break;

    // slight taller jump with no delay, and can "ground pound"
    case STATE_JUMP_WITH_GP:
    {
      FACEPLAYER;

      o->state     = STATE_IN_AIR_WITH_GP;
      o->frame     = 4;
      o->animtimer = 0;

      o->yinertia = -0x800;
      XMOVE(0x400);
    }
    break;

    case STATE_IN_AIR:         // in air (normal)
    case STATE_IN_AIR_WITH_GP: // in air; can "ground pound" if we pass over player
    {
      ANIMATE(1, 4, 5);
      o->yinertia += 0x40;

      if (o->state == STATE_IN_AIR_WITH_GP)
      {
        if (pdistlx(8 * CSFI) && player->y >= o->y)
        {
          o->xinertia = 0;
          o->yinertia = 0x5ff;
          o->state    = STATE_IN_AIR;
        }
      }
      else
      {
        FACEPLAYER;
      }

      if (o->blockd && o->yinertia > 0)
        o->state = STATE_LANDED;
    }
    break;

    // landed from jump
    case STATE_LANDED:
    {
      o->frame = 3;
      quake(10);

      o->timer = 0;
      o->state++;
    }
    case STATE_LANDED + 1:
    {
      o->yinertia += 0x80;

      o->xinertia *= 7;
      o->xinertia /= 8;

      if (++o->timer > 10)
        o->state = STATE_BASE;
    }
    break;
  }
}

/*
void c------------------------------() {}
*/

// flashing red elbow dash
static void run_red_dash(Object *o)
{
  switch (o->state)
  {
    // prepare to dash
    case STATE_RED_DASH:
    {
      o->frame = 3; // crouch down; look mean
      o->flags |= (FLAG_SOLID_MUSHY | FLAG_INVULNERABLE);

      o->timer = 0;
      o->state++;
    }
    case STATE_RED_DASH + 1:
    {
      if (++o->timer > 20)
      {
        o->frame = 7; // elbow-out dash frame
        o->timer = 0;
        o->state++;

        NXE::Sound::SoundManager::getInstance()->playSfx(NXE::Sound::SFX::SND_FUNNY_EXPLODE);
        XMOVE(0x5ff);

        o->damage = DAMAGE_RED_DASH;
        o->flags |= FLAG_NOREARTOPATTACK;
        o->yinertia = 0; // he does not fall during dash
      }
    }
    break;

    // doing red dash
    case STATE_RED_DASH + 2:
    {
      // flash red
      o->timer++;
      o->frame = (o->timer & 2) ? 7 : 8;

      // time to stop?
      if ((o->blockl && o->xinertia < 0) || (o->blockr && o->xinertia > 0) || o->timer > 30)
      {
        if (o->timer > 30) // stopped because timeout
          o->state++;
        else // stopped because hit a wall
          o->state = STATE_JUMP;

        o->flags &= ~(FLAG_SOLID_MUSHY | FLAG_NOREARTOPATTACK | FLAG_INVULNERABLE);
        o->damage = DAMAGE_NORMAL;
        o->timer  = 0;
      }
    }
    break;

    // dash ending due to timeout
    case STATE_RED_DASH + 3:
    {
      o->yinertia += 0x80;
      o->frame = 3;

      o->xinertia *= 7;
      o->xinertia /= 8;

      if (++o->timer > 10)
        o->state = STATE_BASE;
    }
    break;
  }
}

// arms thrust out, spawn oodles of bouncing bats
static void run_mega_bats(Object *o)
{

  switch (o->state)
  {
    case STATE_MEGA_BATS:
    {
      o->state++;
      o->timer = 0;
    }
    case STATE_MEGA_BATS + 1:
    {
      o->frame = 6;
      o->timer++;

      if (o->timer > 20 && (o->timer % 3) == 1)
      {
        Object *bat = CreateObject(o->x + (8 * CSFI), o->y - (4 * CSFI), OBJ_DOCTOR_BAT);

        bat->xinertia = random(0x400, 0x800);
        bat->yinertia = random(-0x200, 0x200);
        bat->dir      = o->dir;

        if (o->dir == LEFT)
        {
          bat->x -= (16 * CSFI);
          bat->xinertia = -bat->xinertia;
        }

        NXE::Sound::SoundManager::getInstance()->playSfx(NXE::Sound::SFX::SND_EM_FIRE);
      }

      if (o->timer > 90)
        o->state = STATE_BASE;
    }
    break;
  }
}

// teleport away and return
static void run_teleport(Object *o)
{
  switch (o->state)
  {
    case STATE_TELEPORT:
    {
      o->flags &= ~FLAG_SHOOTABLE;
      o->damage = 0;

      o->state++;
      dr_tp_out_init(o);
    }
    case STATE_TELEPORT + 1:
    {
      if (dr_tp_out(o))
      {
        o->state++;
        o->timer = 0;

        // mark the location above player's head where we'll reappear
        o->xmark = player->x;
        o->ymark = player->y - (32 * CSFI);

// don't be fooled into going off bounds of map
#define TP_X_MIN ((4 * TILE_W) * CSFI)
#define TP_X_MAX ((36 * TILE_W) * CSFI)
#define TP_Y_MIN ((4 * TILE_W) * CSFI)

        if (o->xmark < TP_X_MIN)
          o->xmark = TP_X_MIN;
        if (o->xmark > TP_X_MAX)
          o->xmark = TP_X_MAX;
        if (o->ymark < TP_Y_MIN)
          o->ymark = TP_Y_MIN;
      }
    }
    break;

    // invisible...waiting to reappear
    case STATE_TELEPORT + 2:
    {
      if (++o->timer > 40)
      {
        o->x     = o->xmark;
        o->y     = o->ymark;
        o->frame = 4;

        FACEPLAYER;

        o->state++;
      }
    }
    break;

    // reappear
    case STATE_TELEPORT + 3:
    {
      dr_tp_in_init(o);
      o->yinertia = 0;
      o->state++;
    }
    case STATE_TELEPORT + 4:
    {
      if (dr_tp_in(o))
      {
        o->flags |= FLAG_SHOOTABLE;
        o->damage = DAMAGE_NORMAL;

        o->xinertia = 0;
        o->yinertia = -0x200;
        o->state    = STATE_IN_AIR;
      }
    }
    break;
  }
}

/*
void c------------------------------() {}
*/

// initilization/transformation animation and "prepare to fight" states.
// this are of course all script-triggered and need to stay constant.
static void run_init(Object *o)
{
  switch (o->state)
  {
    case 0:
    {
      // put ourselves at correct position over the other doctor
      o->dir = (crystal_xmark > player->x) ? LEFT : RIGHT;

      o->x = crystal_xmark + ((o->dir == RIGHT) ? (6 * CSFI) : -(6 * CSFI));
      o->y = crystal_ymark;

      // make sure we're front of other doctor
      o->BringToFront();
      // make sure front crystal is in front of us
      crystal_tofront = true;

      // because we moved our x/y directly
      o->UpdateBlockStates(ALLDIRMASK);
      o->state = 1;
    }
    case 1: // appearing/transforming
    {
      o->yinertia += 0x80;

      o->timer++;
      o->frame = (o->timer & 2) ? 0 : 3;
    }
    break;

    // standing idle & panting
    // this state doesn't seem to ever be used;
    // AFAIK can only be triggered by modifying the script.
    case 5:
    {
      o->frame     = 1;
      o->animtimer = 0;
      o->state     = 6;
    }
    case 6:
    {
      o->yinertia += 0x80;
      ANIMATE(30, 1, 2);
    }
    break;

    case 7: // prepare-to-fight pause
    {
      o->state = 8;
      o->timer = 0;
      o->frame = 3;
    }
    case 8:
    {
      o->yinertia += 0x40;

      if (++o->timer > 40)
        o->state = STATE_BASE;
    }
    break;
  }
}

// defeated states and animation
static void run_defeat(Object *o)
{
  switch (o->state)
  {
    // Defeated!
    case STATE_DEFEATED:
    {
      KillObjectsOfType(OBJ_DOCTOR_BAT);
      o->flags &= ~FLAG_SHOOTABLE;
      o->damage   = 0;
      o->frame    = 4;
      o->xinertia = 0;
      o->state++;
    }
    case STATE_DEFEATED + 1: // wait till we hit ground
    {
      o->yinertia += 0x20;

      if (o->blockd && o->yinertia > 0)
      {
        o->state++;
        o->timer = 0;
        o->xmark = o->x;
        FACEPLAYER;
      }
    }
    break;

    // shaking (script tells us when to stop)
    case STATE_DEFEATED + 2:
    {
      o->frame = 9;
      o->timer++;

      o->x = o->xmark;
      if (!(o->timer & 2))
        o->x += (1 * CSFI);
    }
    break;

    // dissolve into red energy
    // we already did this once before; think Pooh Black.
    case STATE_DISSOLVE:
    {
      o->frame = 9;
      o->x     = o->xmark;

      o->ResetClip();
      o->clip_enable = true;

      o->state++;
      o->timer = 0;
    }
    case STATE_DISSOLVE + 1:
    {
      o->timer++;

      // shaking
      o->x = o->xmark;
      if (!(o->timer & 2))
        o->x += (1 * CSFI);

      game.quaketime = 2;

      // sound
      if ((o->timer % 6) == 3)
        NXE::Sound::SoundManager::getInstance()->playSfx(NXE::Sound::SFX::SND_FUNNY_EXPLODE);

      // move energy spawn point
      if (++o->timer2 >= 8)
      {
        o->timer2 = 0;

        o->clipy1++;
        if (o->clipy1 >= Renderer::getInstance()->sprites.sprites[o->sprite].h)
          o->invisible = true;
      }

      // spawn copious amount of energy
      for (int i = 0; i < 3; i++)
      {
        int x, y;

        x = o->x + (random(-16, 16) * CSFI);
        y = (o->y - o->DrawPointY()) + (o->clipy1 * CSFI);

        Object *drip = CreateObject(x, y, OBJ_RED_ENERGY);

        drip->xinertia = random(-0x200, 0x200);
        drip->yinertia = random(-0x400, 0);
        drip->angle    = DOWN;
        // otherwise during the last few frames they'll get stuck in the floor
        // (they still delete themselves once they hit the floor, just are
        // able to come up out of it then back down during last few moments).
        drip->flags |= FLAG_IGNORE_SOLID;
      }

      // he doesn't take up the entire height of the sprite,
      // so we stop a little bit early.
      if (o->clipy1 >= 44)
      {
        o->invisible = true;
        o->frame     = 0;
        o->state++;
      }
    }
    break;

    // script: crystal up and away
    case 520:
    {
      crystal_ymark = -(128 * CSFI);
    }
    break;
  }
}

/*
void c------------------------------() {}
*/

// this is a lesser-seen attack in which he pushes you away amongst
// a shower of red sparks. To trigger it, immediately after he lands
// you must walk directly up to him and deal more than 20 damage.
static void do_redsplode(Object *o)
{
  // arms out full
  o->frame = 6;
  FACEPLAYER;

  player->yinertia = -0x400;
  player->xinertia = (o->x > player->x) ? -0x5ff : 0x5ff;

  hurtplayer(5);
  quake(10);

  // big shower of red energy
  for (int i = 0; i < 100; i++)
  {
    int x = o->x + (random(-16, 16) * CSFI);
    int y = o->y + (random(-16, 16) * CSFI);

    Object *spark = CreateObject(x, y, OBJ_RED_ENERGY);

    spark->xinertia = random(-0x600, 0x600);
    spark->yinertia = random(-0x600, 0x600);
    spark->angle    = DOWN;
  }
}

/*
void c------------------------------() {}
*/

// the red energy that oozes off of him during most of the battle
static void run_red_drip(Object *o)
{
  if (random(0, 3) == 2)
  {
    int x = o->x + (random(-16, 16) * CSFI);
    int y = o->y + (random(-8, 4) * CSFI);

    Object *drip   = CreateObject(x, y, OBJ_RED_ENERGY);
    drip->xinertia = o->xinertia;
    drip->angle    = DOWN;
  }
}

void ai_boss_doctor_frenzied(Object *o)
{
  // AIDEBUG;

  switch (o->state)
  {
    // fight begin/base state
    case STATE_BASE:
    {
      o->flags |= FLAG_SHOOTABLE;
      o->flags &= ~FLAG_INVULNERABLE;

      o->xinertia = 0;
      o->frame    = 1;

      o->timer     = 0;
      o->animtimer = 0;

      o->savedhp = o->hp;
      o->state++;
    }
    case STATE_BASE + 1:
    {
      o->yinertia += 0x80;
      FACEPLAYER;

      // select frame
      if (o->frame == 6) // arms out
      {
        // must have done "redsplode" attack; leave alone for the duration
      }
      else if (!o->blockd)
      { // falling
        o->frame = 4;
      }
      else
      { // panting animation
        ANIMATE(10, 1, 2);

        // "redsplode" attack if possible
        if ((o->savedhp - o->hp) > 20)
        {
          if (pdistlx(48 * CSFI) && player->blockd)
            do_redsplode(o);
        }
      }

      // after a moment select next attack in the loop
      if (++o->timer > 30 || (o->savedhp - o->hp) > 20)
      {
        o->state = attack_pattern[o->timer2];
        o->timer = 0;

        // move to next state
        if (attack_pattern[++o->timer2] == -1)
          o->timer2 = 0;
      }
    }
    break;
  }

  run_jumps(o);

  run_red_dash(o);
  run_mega_bats(o);
  run_teleport(o);
  run_init(o);
  run_defeat(o);

  if (o->state < STATE_DISSOLVE)
    run_red_drip(o);

  // set crystal follow position
  // still set it on first 2 DEFEATED states (falling to ground)
  // but not after that (alert: this seems pretty damn bug prone, fixme)
  if (o->state >= STATE_BASE && o->state <= STATE_DEFEATED + 1)
  {
    if (o->invisible) // in middle of teleport: after tp out, before tp in
    {
      crystal_xmark = o->xmark;
      crystal_ymark = o->ymark;
    }
    else
    {
      crystal_xmark = o->x;
      crystal_ymark = o->y;
    }
  }

  if (o->yinertia > 0x5ff)
    o->yinertia = 0x5ff;
}

/*
void c------------------------------() {}
*/

void ai_doctor_bat(Object *o)
{
  ANIMATE(2, 0, 2);

  if ((o->blockl && o->xinertia < 0) || (o->blockr && o->xinertia > 0))
  {
    o->dir ^= 1;
    o->xinertia = -o->xinertia;
  }
  else if ((o->blocku && o->yinertia < 0) || (o->blockd && o->yinertia > 0))
  {
    o->yinertia = -o->yinertia;
  }
}
