#include "gameplay/weapons/weapon.h"
#include "gameplay/gameactor.h"
#include "gameplay/physics.h"
#include "gameplay/weapons/bullet.h"
#include "gameplay/weapons/bulletmanager.h"
#include <SDL_timer.h>

#include "utility/component.h"
#include "utility/debugdraw.h"
#include "utility/events.h"
#include "utility/logger.h"
#include "utility/resourcemanager.h"
#include "utility/script.h"

// TODO: Regular clean up, make this mess readable!

Weapon::Weapon(const WeaponData *data, const unsigned weaponShaderID,
               const unsigned bulletShaderID, ResourceManager *resourceManager)
    : Entity(weaponShaderID), weaponType(data->id),
      weaponSize(glm::vec2(data->sizeX, data->sizeY)),
      weaponOffset(glm::vec2(data->offsetX, data->offsetY)),
      weaponMag(data->clipSize), weaponMagSize(data->clipSize),
      weaponAmmo(data->maxAmmo), bulletSpeed(data->bulletSpeed),
      bulletDrop(data->bulletDrop), fireSpeed(data->fireSpeed),
      bulletSize(glm::vec2(data->bulletSizeX, data->bulletSizeY)),
      bulletShaderID(bulletShaderID),
      bulletManager(std::make_shared<BulletManager>()),
      bulletSpread(std::make_unique<UTIL::RandomGenerator>(-data->bulletSpread,
                                                           data->bulletSpread)),
      bulletModifer(
          std::make_unique<UTIL::RandomGenerator>(data->modMin, data->modMax)) {
  if (data->bulletAnimated)
    bulletSprite = std::make_unique<AnimationComponent>(
        resourceManager->loadAnimationSet(data->bulletGraphic, entityid));
  else
    bulletSprite = std::make_unique<SpriteComponent>(
        resourceManager->loadSpriteStatic(data->bulletGraphic));

  if (data->animated) {
    addComponent(std::make_unique<AnimationComponent>(
        resourceManager->loadAnimationSet(data->id, entityid)));
  } else
    addComponent(std::make_unique<SpriteComponent>(
        resourceManager->loadSpriteStatic(data->graphic)));
  this->setScale(glm::vec3(weaponSize.x, weaponSize.y, 1.0f));
};

void Weapon::addComponent(std::unique_ptr<Component> comp) {
  components.push_back(std::move(comp));
}

void Weapon::reload() {
  if (auto event = eventManager.lock()) {
    event->notify<EntityReloadEvent>(
        {entityid, wielder->getPosition(), weaponType});
    reloading = true;
    if (weaponAmmo < weaponMagSize) {
      weaponMag = weaponAmmo;
      weaponAmmo = 0;
    } else {
      weaponMag = weaponMagSize;
      weaponAmmo -= weaponMagSize;
    }
  }
}

bool Weapon::shoot() {
  bool shotsFired = false;
  if (wielder) {
    Uint32 currentTime = SDL_GetTicks();
    if (currentTime - lastFireTime >= fireSpeed && !reloading) {
      if (weaponMag > 0) {
        shotsFired = true;
        if (auto event = eventManager.lock())
          event->notify<EntityFireEvent>(
              {entityid, fireSpeed, wielder->getPosition(), weaponType});
        if (!weaponScript || !weaponScript->lua["onShoot"].valid()) {
          // create bullet using this generated data
          BulletData b = genBulletData();
          createBullet(b);
          weaponMag -= 1;
        } else {
          auto result = weaponScript->lua["onShoot"]();
          if (!result.valid()) {
            sol::error err = result;
            std::cerr << "lua error: " << err.what() << std::endl;
          }
          // auto reload
          if ((weaponMag -= 1) <= 0)
            reload();
        }
      } else if (weaponMag <= 0)
        reload();
      lastFireTime = currentTime;
    }
  }
  return shotsFired;
}

void Weapon::hookEventManager(std::weak_ptr<EventManager> eventManager) {
  this->eventManager = eventManager;

  for (auto &component : components) {
    if (component->getType() == Component::TYPE::ANIMATION) {
      auto animComponent = static_cast<AnimationComponent *>(component.get());
      animComponent->getAnimationSet()->attachEventManager(eventManager);
    }
  }

  if (auto event = this->eventManager.lock()) {
    auto self = this;
    event->subscribe<AnimationFinishedEvent>(
        [self](const AnimationFinishedEvent &e) {
          if (self) {
            if (e.entityid == self->entityid && e.animType == "reload") {
              if (self->reloading) {
                self->reloading = false;
                self->wasReloading = true;
              }
            }
          }
        });
  }

  bulletManager->hookEventManager(eventManager);
}

void Weapon::attachScript(std::unique_ptr<WeaponScript> script) {
  weaponScript = std::move(script);
  weaponScript->lua["weapon"] = this;
  LOG(DEBUG, "Weapon state bound", NULL);
}

void Weapon::onHitCallback(GameActor *target, PhysicsComponent *bullet,
                           const glm::vec2 &normal) {
  if (weaponScript && weaponScript->lua["onHit"].valid()) {
    auto result = weaponScript->lua["onHit"](target, bullet, normal);
    if (!result.valid()) {
      sol::error err = result;
      std::cerr << "lua error: " << err.what() << std::endl;
    }
  }
}

void Weapon::update(double deltaTime) {
  Entity::update(deltaTime);
  if (wielded) {
    // move the weapon into place as the wielder rotates and moves
    if (wielder)
      adjustWeapon();

    for (auto &component : components)
      component->update();
    if (wasReloading) {
      wasReloading = false;
      if (auto event = eventManager.lock()) {
        event->notify<EntityFinishReloadEvent>({entityid});
      }
    }
  }
  bulletManager->update(deltaTime);
}

void Weapon::draw() {
  Entity::draw();
  if (wielded) {
    for (auto &component : components) {
      component->play();
      component->bind();
      component->render();
    }
  }
  // bulletManager->draw();
}

void Weapon::adjustWeapon() {
  float rotation = glm::radians(wielder->getRotation());

  glm::vec3 offset = glm::vec3(
      cos(rotation) * ((wielder->getScale().x) - weaponSize.x * 0.5f),
      sin(rotation) * ((wielder->getScale().y) - weaponSize.y * 0.5f), 0.0f);
  glm::vec3 origin = wielder->getCenter() + offset;
  // origin.x += (weaponSize.x) * 0.25f;
  // origin.y += (weaponSize.y) * 0.25f;

  // Flip the texture if the weapon is facing upwards or downwards
  Direction d = getDirectionFromRotation(glm::degrees(rotation));
  if ((lastDir == Direction::Up || lastDir == Direction::Left) &&
      (d == Direction::Down || d == Direction::Right) && !isFlipped())
    flip();
  if ((lastDir == Direction::Down || lastDir == Direction::Right) &&
      (d == Direction::Up || d == Direction::Left) && isFlipped())
    flip();

  setRotation(wielder->getRotation() - 180);
  setPosition(origin);
  lastDir = getDirectionFromRotation(glm::degrees(rotation));
}

Weapon::BulletData Weapon::genBulletData() {
  BulletData b;
  float rotation = glm::radians(wielder->getRotation());
  float spreadOffset =
      glm::radians(static_cast<float>(bulletSpread->genFloat()));
  b.mass = 0.1f;
  glm::vec2 facing =
      glm::vec2(cos(rotation + spreadOffset), sin(rotation + spreadOffset));
  b.direction = glm::normalize(facing);
  b.sizeMod = bulletModifer->genFloat();
  b.speedMod = bulletModifer->genFloat();
  b.dropMod = bulletModifer->genFloat();

  double radius = wielder->getScale().x + (weaponSize.x * 0.5) + weaponOffset.x;

  b.origin = glm::vec3(
      // x offset from the wielder
      wielder->getCenter().x + cos(rotation) * radius -
          sin(rotation) * weaponOffset.y,
      // y offset from the wielder
      wielder->getCenter().y + sin(rotation) * radius +
          cos(rotation) * weaponOffset.y,
      0.0f);
  b.origin.x -= ((bulletSize.x) * b.sizeMod) * 0.5f;
  b.origin.y -= ((bulletSize.y) * b.sizeMod) * 0.5f;
  return b;
}

void Weapon::createBullet(const Weapon::BulletData &data) {
  auto bullet = std::make_shared<Bullet>(wielder->getEntityID(), bulletShaderID,
                                         data.origin, data.direction,
                                         bulletSpeed, bulletDrop, bulletSize);
  bullet->addComponent(bulletSprite.get());
  bullet->addPhysicsComponent(
      std::make_shared<PhysicsComponent>(PhysicsComponentFactory::makeBullet(
          wielder->getEntityID(), data.origin, data.mass, bulletSize.x / 2)));
  bullet->getPhysicsComponent()->rigidBody.velocity +=
      bulletSpeed * data.direction / data.mass;

  if (auto event = eventManager.lock())
    event->notify<BulletFiredEvent>({bullet});
  bulletManager->addBullet(bullet);
}

Weapon::~Weapon() {
  if (weaponScript) {
    weaponScript->lua["onShoot"] = sol::nil;
    weaponScript->lua.collect_gc();
  }
}
