#ifndef _HPP_KUNGFLOW_CHARACTERMOVE
#define _HPP_KUNGFLOW_CHARACTERMOVE

#include "gaia/gaia.hpp"
using namespace gaia;

#include "defines.hpp" // defaultvector

#include <boost/signals2.hpp>

enum e_MoveType {
  e_MoveType_Walk,
  e_MoveType_Jab,
  e_MoveType_Dash,
  e_MoveType_Block, // todo?
  e_MoveType_Dodge,
  e_MoveType_Throw,
  e_MoveType_Jump,
  e_MoveType_Landing,
  e_MoveType_Hit,
  e_MoveType_SIZE
};

struct WalkStats {
  float agilityFactor;
  unsigned int walkDuration_ms;
};

struct JabStats {
  float distanceFactor;
  unsigned int onsetDuration_ms;
  unsigned int executeDuration_ms;
  unsigned int successfulSustainDuration_ms;
  unsigned int failureSustainDuration_ms;
  unsigned int cooldownDuration_ms;
};

// context sensitive move (as in, useable for both moving and attacking)
struct DashStats {
  float damageFactor;
  unsigned int onsetDuration_ms;
  //unsigned int executeDuration_ms; decided by target distance
  unsigned int sustainDuration_ms;
  unsigned int cooldownDuration_ms;
};

struct BlockStats {
  unsigned int defaultBlockDuration_ms;
  unsigned int defaultBlockCooldownDuration_ms;
};

struct DodgeStats {
  float distanceFactor;
  unsigned int onsetDuration_ms;
  unsigned int executeDuration_ms;
  unsigned int sustainDuration_ms;
  unsigned int cooldownDuration_ms;
};

struct ThrowStats {
  float maxDistance;
  unsigned int onsetDuration_ms;
  unsigned int executeDuration_ms;
  unsigned int sustainDuration_ms;
  unsigned int cooldownDuration_ms;
};

struct JumpStats {
  float distanceFactor;
  unsigned int onsetDuration_ms;
  unsigned int executeDuration_ms;
  unsigned int sustainDuration_ms;
  unsigned int cooldownDuration_ms;
};

struct LandingStats {
  unsigned int executeDuration_ms;
};

struct HitStats {
  unsigned int executeDuration_ms;
  unsigned int sustainDuration_ms;
};

class PlayState;


// remember that moves are persistent; all types are created for each character at the start of the character's lifetime, and removed only at its demise.
// (why? so we can easily query information from the previous time it was used, especially timeSinceLastActivation_ms)

class CharacterMove {

  public:

    CharacterMove(const PlayState *playState, const e_MoveType &moveType, AnimatedSprite *targetSprite);
    virtual ~CharacterMove();

    unsigned int GetUniqueID() const { return uniqueID; }
    e_MoveType GetMoveType() const { return moveType; }

    virtual void Calculate(const Vector2 &currentMovement, float currentZMovement, const radian currentBodyAngle, const Vector2 &userInputMovement, radian userInputBodyAngle, unsigned int timeStep_ms, Vector2 &resultingMovement, float &resultingZMovement, radian &resultingBodyAngle) const = 0;
    //virtual Vector2 CalculateMovement(const Vector2 &currentMovement, const Vector2 &userInputMovement, unsigned int timeStep_ms) const = 0;
    //virtual radian CalculateBodyAngle(radian currentAngle, radian desiredBodyAngle, unsigned int timeStep_ms) const = 0;
    virtual void SelectSpriteSequence(int directionID) const = 0; // seperate from Calculate(), because this needs only be called @ graphics timestep

    virtual unsigned int GetDuration_ms() const = 0;
    virtual unsigned int GetCooldownTime_ms() const = 0;
    virtual bool IsLethal() const { return false; }
    virtual float GetDamagePotentialFactor() const { return 0.0f; }
    virtual float GetFlowInfluenceFactor() const { return 1.0f; }
    virtual bool IsLegal() const {
      if (timeSinceLastActivation_ms >= GetDuration_ms() + GetCooldownTime_ms()) return true; else return false;
    }

    void ResetAndActivate() {
      uniqueID = ++_moveCounter;
      timeSinceLastActivation_ms = 0;
      active = true;
      Reset();
    }

    float GetCurrentTimeFactor() const { return (float)GetTimeSinceLastActivation_ms() / (float)GetDuration_ms(); }

    void StepTime(unsigned int timeStep_ms) {
      timeSinceLastActivation_ms += timeStep_ms;
      if (active) {
        if (timeSinceLastActivation_ms >= GetDuration_ms()) active = false;
        OnStepTime();
      }
    }

    bool IsActive() const { return active; }

    unsigned int GetTimeSinceLastActivation_ms() const { return timeSinceLastActivation_ms; }

  protected:

    const PlayState *playState;

    virtual void OnStepTime() {};
    virtual void Reset() {};

    AnimatedSprite *targetSprite;

    //unsigned int duration_ms;
    //unsigned int cooldownTime_ms;

  private:

    e_MoveType moveType;

    unsigned int uniqueID;
    static unsigned int _moveCounter; // increments with every new move

    bool active;
    unsigned int timeSinceLastActivation_ms;
    //static std::vector<unsigned int> spriteSequenceIDs[8]; // IDs point to the 8 (=directions) animated sequences belonging to a certain phase of this move

};


class CharacterWalkMove : public CharacterMove {

  public:

    CharacterWalkMove(const PlayState *playState, AnimatedSprite *targetSprite, const WalkStats &stats);
    virtual ~CharacterWalkMove();

    virtual void Calculate(const Vector2 &currentMovement, float currentZMovement, const radian currentBodyAngle, const Vector2 &userInputMovement, radian userInputBodyAngle, unsigned int timeStep_ms, Vector2 &resultingMovement, float &resultingHeight, radian &resultingBodyAngle) const;
    //virtual Vector2 CalculateMovement(const Vector2 &currentMovement, const Vector2 &userInputMovement, unsigned int timeStep_ms) const override;
    //virtual radian CalculateBodyAngle(radian currentAngle, radian desiredBodyAngle, unsigned int timeStep_ms) const override;
    virtual void SelectSpriteSequence(int directionID) const override;

    virtual unsigned int GetDuration_ms() const override { return stats.walkDuration_ms; }
    virtual unsigned int GetCooldownTime_ms() const override { return 0; }

    void SetVariables(const Vector2 &startMovement, const Vector2 &targetMovement) {
      this->startMovement = startMovement;
      this->targetMovement = targetMovement;
    }

    void SetStats(const WalkStats &stats) { this->stats = stats; }

  protected:

  private:

    void InitSpriteSequences(AnimatedSprite *targetSprite); // may insert AnimatedSpriteSequences into the targetSprite (save IDs to spriteSequenceIDs)

    int spriteSequence_Idle_IDs[8];
    int spriteSequence_Walking_IDs[8];

    Vector2 startMovement;
    Vector2 targetMovement;

    WalkStats stats;

};


class CharacterJabMove : public CharacterMove {

  public:

    CharacterJabMove(const PlayState *playState, AnimatedSprite *targetSprite, const JabStats &stats);
    virtual ~CharacterJabMove();

    virtual void Calculate(const Vector2 &currentMovement, float currentZMovement, const radian currentBodyAngle, const Vector2 &userInputMovement, radian userInputBodyAngle, unsigned int timeStep_ms, Vector2 &resultingMovement, float &resultingHeight, radian &resultingBodyAngle) const;
    //virtual Vector2 CalculateMovement(const Vector2 &currentMovement, const Vector2 &userInputMovement, unsigned int timeStep_ms) const override;
    //virtual radian CalculateBodyAngle(radian currentAngle, radian desiredBodyAngle, unsigned int timeStep_ms) const override;
    virtual void SelectSpriteSequence(int directionID) const override;

    virtual unsigned int GetDuration_ms() const override { return stats.onsetDuration_ms + stats.executeDuration_ms + activeSustainDuration_ms; }
    virtual unsigned int GetCooldownTime_ms() const override { return stats.cooldownDuration_ms; }
    virtual bool IsLethal() const override;
    virtual float GetDamagePotentialFactor() const override;
    virtual float GetFlowInfluenceFactor() const override { return 0.7f; }

    //virtual bool IsLegal() const override { printf("(%i, %i, %i) %i >= %i + %i ? %i\n", stats.onsetDuration_ms, stats.executeDuration_ms, activeSustainDuration_ms, GetTimeSinceLastActivation_ms(), GetDuration_ms(), GetCooldownTime_ms(), GetTimeSinceLastActivation_ms() >= GetDuration_ms() + GetCooldownTime_ms()); return true; }

    void SetVariables(const Vector2 &startMovement, const Vector2 &targetDirection, float distanceFactor) {
      this->startMovement = startMovement;
      this->targetDirection = targetDirection;
      this->distanceFactor = distanceFactor;
      /*
      float adaptedPowerFactor = powerFactor * 0.5f + 0.5f;
      stats = stats;
      stats.onsetDuration_ms *= adaptedPowerFactor;
      stats.executeDuration_ms *= adaptedPowerFactor;
      stats.failureSustainDuration_ms *= adaptedPowerFactor;
      stats.successfulSustainDuration_ms *= adaptedPowerFactor;
      stats.cooldownDuration_ms *= adaptedPowerFactor;
      */
    }

    void SetStats(const JabStats &stats) { this->stats = stats; this->stats = stats; }

    void SetHitSomething(bool well) {
      //if (hitSomething = false && well = true)
      hitSomething = well;
      activeSustainDuration_ms = stats.successfulSustainDuration_ms;
    }

    bool GetHitSomething() const {
      return hitSomething;
    }

  protected:

    virtual void Reset() override;

  private:

    void InitSpriteSequences(AnimatedSprite *targetSprite); // may insert AnimatedSpriteSequences into the targetSprite (save IDs to spriteSequenceIDs)

    int spriteSequence_Onset_IDs[8];
    int spriteSequence_Execute_IDs[8];
    int spriteSequence_Sustain_IDs[8];

    Vector2 startMovement;
    Vector2 targetDirection;
    float distanceFactor;

    JabStats stats;
    //JabStats effectiveStats;
    int activeSustainDuration_ms;

    bool hitSomething;

};

class CharacterDashMove : public CharacterMove {

  public:

    CharacterDashMove(const PlayState *playState, AnimatedSprite *targetSprite, const DashStats &stats);
    virtual ~CharacterDashMove();

    virtual void Calculate(const Vector2 &currentMovement, float currentZMovement, const radian currentBodyAngle, const Vector2 &userInputMovement, radian userInputBodyAngle, unsigned int timeStep_ms, Vector2 &resultingMovement, float &resultingHeight, radian &resultingBodyAngle) const;
    //virtual Vector2 CalculateMovement(const Vector2 &currentMovement, const Vector2 &userInputMovement, unsigned int timeStep_ms) const override;
    //virtual radian CalculateBodyAngle(radian currentAngle, radian desiredBodyAngle, unsigned int timeStep_ms) const override;
    virtual void SelectSpriteSequence(int directionID) const override;

    virtual unsigned int GetDuration_ms() const override { return stats.onsetDuration_ms + executeDuration_ms + stats.sustainDuration_ms; }
    virtual unsigned int GetCooldownTime_ms() const override { return stats.cooldownDuration_ms; }
    virtual bool IsLethal() const override;
    virtual float GetDamagePotentialFactor() const override;

    void SetVariables(const Vector2 &startMovement, const Vector2 &toTargetTranslation, radian &targetBodyAngle) {
      this->startMovement = startMovement;
      this->toTargetTranslation = toTargetTranslation.GetNormalizedMax(5.0f);
      this->executeDuration_ms = (unsigned int)(this->toTargetTranslation.GetLength() * 50);
      this->targetBodyAngle = targetBodyAngle;
    }

    void SetStats(const DashStats &stats) { this->stats = stats; }

    void SetHitSomething(bool well) {
      hitSomething = well;
    }

    bool GetHitSomething() const {
      return hitSomething;
    }

  protected:

    virtual void Reset() override;

  private:

    void InitSpriteSequences(AnimatedSprite *targetSprite); // may insert AnimatedSpriteSequences into the targetSprite (save IDs to spriteSequenceIDs)

    int spriteSequence_Onset_IDs[8];
    int spriteSequence_Execute_IDs[8];
    int spriteSequence_Sustain_IDs[8];

    Vector2 startMovement;
    Vector2 toTargetTranslation;
    radian targetBodyAngle;
    unsigned int executeDuration_ms;

    DashStats stats;

    bool hitSomething;

};

class CharacterDodgeMove : public CharacterMove {

  public:

    CharacterDodgeMove(const PlayState *playState, AnimatedSprite *targetSprite, const DodgeStats &stats);
    virtual ~CharacterDodgeMove();

    virtual void Calculate(const Vector2 &currentMovement, float currentZMovement, const radian currentBodyAngle, const Vector2 &userInputMovement, radian userInputBodyAngle, unsigned int timeStep_ms, Vector2 &resultingMovement, float &resultingHeight, radian &resultingBodyAngle) const;
    //virtual Vector2 CalculateMovement(const Vector2 &currentMovement, const Vector2 &userInputMovement, unsigned int timeStep_ms) const override;
    //virtual radian CalculateBodyAngle(radian currentAngle, radian desiredBodyAngle, unsigned int timeStep_ms) const override;
    virtual void SelectSpriteSequence(int directionID) const override;

    virtual unsigned int GetDuration_ms() const override { return stats.onsetDuration_ms + stats.executeDuration_ms + stats.sustainDuration_ms; }
    virtual unsigned int GetCooldownTime_ms() const override { return stats.cooldownDuration_ms; }
    virtual float GetFlowInfluenceFactor() const override { return Curve(pow(GetCurrentTimeFactor(), 1.0f), 0.8f); }

    void SetVariables(const Vector2 &startMovement, const Vector2 &targetDirection, radian &targetBodyAngle) {
      this->startMovement = startMovement;
      this->targetDirection = targetDirection;
      this->targetBodyAngle = targetBodyAngle;
    }

    void SetStats(const DodgeStats &stats) { this->stats = stats; }

  protected:

  private:

    void InitSpriteSequences(AnimatedSprite *targetSprite); // may insert AnimatedSpriteSequences into the targetSprite (save IDs to spriteSequenceIDs)

    int spriteSequence_Onset_IDs[8];
    int spriteSequence_Execute_IDs[8];
    int spriteSequence_Sustain_IDs[8];

    Vector2 startMovement;
    Vector2 targetDirection;
    radian targetBodyAngle;

    DodgeStats stats;

};

class CharacterThrowMove : public CharacterMove {

  public:

    CharacterThrowMove(const PlayState *playState, AnimatedSprite *targetSprite, const ThrowStats &stats);
    virtual ~CharacterThrowMove();

    virtual void Calculate(const Vector2 &currentMovement, float currentZMovement, const radian currentBodyAngle, const Vector2 &userInputMovement, radian userInputBodyAngle, unsigned int timeStep_ms, Vector2 &resultingMovement, float &resultingHeight, radian &resultingBodyAngle) const;
    virtual void SelectSpriteSequence(int directionID) const override;

    virtual unsigned int GetDuration_ms() const override { return stats.onsetDuration_ms + stats.executeDuration_ms + stats.sustainDuration_ms; }
    virtual unsigned int GetCooldownTime_ms() const override { return stats.cooldownDuration_ms; }
    virtual float GetFlowInfluenceFactor() const override { return 0.75f; }//pow(GetCurrentTimeFactor(), 0.2f) * 0.5f + 0.5f; }

    void SetVariables(const Vector2 &startMovement, const Vector2 &toTargetTranslation) {
      this->startMovement = startMovement;
      this->toTargetTranslation = toTargetTranslation;
      this->targetDirection = toTargetTranslation.GetNormalized(_DEFAULTVECTOR);
    }

    void SetStats(const ThrowStats &stats) { this->stats = stats; }

    mutable boost::signals2::signal<void(const Vector2&, const Vector2&)> sig_Throw;

  protected:

    virtual void OnStepTime() override; // for throwing
    virtual void Reset() override { hasThrown = false; }; // for reset throwing

  private:

    void InitSpriteSequences(AnimatedSprite *targetSprite); // may insert AnimatedSpriteSequences into the targetSprite (save IDs to spriteSequenceIDs)

    int spriteSequence_Onset_IDs[8];
    int spriteSequence_Execute_IDs[8];
    int spriteSequence_Sustain_IDs[8];

    Vector2 startMovement;
    Vector2 toTargetTranslation;
    Vector2 targetDirection;

    mutable Vector2 latestUserInputMovement;
    mutable radian latestUserInputBodyAngle;

    bool hasThrown;

    ThrowStats stats;

};

class CharacterJumpMove : public CharacterMove {

  public:

    CharacterJumpMove(const PlayState *playState, AnimatedSprite *targetSprite, const JumpStats &stats);
    virtual ~CharacterJumpMove();

    virtual void Calculate(const Vector2 &currentMovement, float currentZMovement, const radian currentBodyAngle, const Vector2 &userInputMovement, radian userInputBodyAngle, unsigned int timeStep_ms, Vector2 &resultingMovement, float &resultingHeight, radian &resultingBodyAngle) const;
    virtual void SelectSpriteSequence(int directionID) const override;

    virtual unsigned int GetDuration_ms() const override { return effectiveStats.onsetDuration_ms + effectiveStats.executeDuration_ms + effectiveStats.sustainDuration_ms; }
    virtual unsigned int GetCooldownTime_ms() const override { return effectiveStats.cooldownDuration_ms; }

    //virtual bool IsLegal() const { if (nthTime < 1) return true; else return false; }

    void SetVariables(const Vector2 &startMovement, const Vector2 &targetDirection, float powerFactor) {//, int nthTime) {
      this->startMovement = startMovement;
      this->targetDirection = targetDirection;
      this->powerFactor = powerFactor;
      //this->nthTime = nthTime;
      float adaptedPowerFactor = powerFactor * 0.5f + 0.5f;
      effectiveStats = stats;
      effectiveStats.onsetDuration_ms *= adaptedPowerFactor;
      effectiveStats.executeDuration_ms *= adaptedPowerFactor;
      effectiveStats.sustainDuration_ms *= adaptedPowerFactor;
      effectiveStats.cooldownDuration_ms *= adaptedPowerFactor;
    }

    //void ResetNthTime() { nthTime = 0; }

    void SetStats(const JumpStats &stats) { this->stats = stats; this->effectiveStats = stats; }

  protected:

    //virtual void Reset() { nthTime = 0; }

  private:

    void InitSpriteSequences(AnimatedSprite *targetSprite); // may insert AnimatedSpriteSequences into the targetSprite (save IDs to spriteSequenceIDs)

    int spriteSequence_Onset_IDs[8];
    int spriteSequence_Execute_IDs[8];
    int spriteSequence_Sustain_IDs[8];

    Vector2 startMovement;
    Vector2 targetDirection;
    float powerFactor;
    //int nthTime;

    JumpStats stats;
    JumpStats effectiveStats;

};

class CharacterLandingMove : public CharacterMove {

  public:

    CharacterLandingMove(const PlayState *playState, AnimatedSprite *targetSprite, const LandingStats &stats);
    virtual ~CharacterLandingMove();

    virtual void Calculate(const Vector2 &currentMovement, float currentZMovement, const radian currentBodyAngle, const Vector2 &userInputMovement, radian userInputBodyAngle, unsigned int timeStep_ms, Vector2 &resultingMovement, float &resultingHeight, radian &resultingBodyAngle) const;
    //virtual Vector2 CalculateMovement(const Vector2 &currentMovement, const Vector2 &userInputMovement, unsigned int timeStep_ms) const override;
    //virtual radian CalculateBodyAngle(radian currentAngle, radian desiredBodyAngle, unsigned int timeStep_ms) const override;
    virtual void SelectSpriteSequence(int directionID) const override;

    virtual unsigned int GetDuration_ms() const override { return stats.executeDuration_ms * durationMultiplier; }
    virtual unsigned int GetCooldownTime_ms() const override { return 0; }
    virtual bool IsLegal() const { return true; }
    virtual float GetFlowInfluenceFactor() const { return 0.75f; }

    void SetVariables(const Vector2 &startMovement, const Vector2 &targetDirection, float durationMultiplier = 1.0f) {
      this->startMovement = startMovement;
      this->targetDirection = targetDirection;
      this->durationMultiplier = durationMultiplier;
    }

    void SetStats(const LandingStats &stats) { this->stats = stats; }//this->effectiveStats = stats; }

  protected:

  private:

    void InitSpriteSequences(AnimatedSprite *targetSprite); // may insert AnimatedSpriteSequences into the targetSprite (save IDs to spriteSequenceIDs)

    int spriteSequence_Execute_IDs[8];
    int spriteSequence_Sustain_IDs[8];

    Vector2 startMovement;
    Vector2 targetDirection;
    float durationMultiplier;

    LandingStats stats;

};

class CharacterHitMove : public CharacterMove {

  public:

    CharacterHitMove(const PlayState *playState, AnimatedSprite *targetSprite, const HitStats &stats);
    virtual ~CharacterHitMove();

    virtual void Calculate(const Vector2 &currentMovement, float currentZMovement, const radian currentBodyAngle, const Vector2 &userInputMovement, radian userInputBodyAngle, unsigned int timeStep_ms, Vector2 &resultingMovement, float &resultingHeight, radian &resultingBodyAngle) const;
    //virtual Vector2 CalculateMovement(const Vector2 &currentMovement, const Vector2 &userInputMovement, unsigned int timeStep_ms) const override;
    //virtual radian CalculateBodyAngle(radian currentAngle, radian desiredBodyAngle, unsigned int timeStep_ms) const override;
    virtual void SelectSpriteSequence(int directionID) const override;

    virtual unsigned int GetDuration_ms() const override { return stats.executeDuration_ms + stats.sustainDuration_ms * sustainMultiplier; }
    virtual unsigned int GetCooldownTime_ms() const override { return 0; }
    virtual bool IsLegal() const { return true; }
    virtual float GetFlowInfluenceFactor() const { return 0.25f; }

    void SetVariables(const Vector2 &translation, float sustainMultiplier = 1.0f) {
      //this->startMovement = startMovement;
      this->translation = translation;
      this->sustainMultiplier = sustainMultiplier;
      //this->powerFactor = powerFactor;

      //this->effectiveStats = stats;
      //float adaptedPowerFactor = powerFactor * 0.5f + 0.5f;
      //this->effectiveStats.executeDuration_ms *= adaptedPowerFactor;
      //this->effectiveStats.sustainDuration_ms *= sustainMultiplier;
    }

    void SetStats(const HitStats &stats) { this->stats = stats; }//this->effectiveStats = stats; }

  protected:

  private:

    void InitSpriteSequences(AnimatedSprite *targetSprite); // may insert AnimatedSpriteSequences into the targetSprite (save IDs to spriteSequenceIDs)

    int spriteSequence_Execute_IDs[8];
    int spriteSequence_Sustain_IDs[8];

    //Vector2 startMovement;
    Vector2 translation;
    float sustainMultiplier;
    //float powerFactor;

    HitStats stats;
    //HitStats effectiveStats;

};

#endif
