#include "gui/guecstra.hpp"
#include "gui/dnd_loot.hpp"

namespace gui {

  DNDLoot::DNDLoot(StatusUI& status_ui, LootUI& loot_ui, sf::RenderWindow &window, routing::Router& router) :
    $status_ui(status_ui),
    $loot_ui(loot_ui),
    $window(window),
    $router(router)
  {
    event(Event::STARTED);
  }

  bool DNDLoot::event(Event ev, std::any data) {
    switch($state) {
      FSM_STATE(DNDState, START, ev);
      FSM_STATE(DNDState, LOOTING, ev, data);
      FSM_STATE(DNDState, LOOT_GRAB, ev, data);
      FSM_STATE(DNDState, INV_GRAB, ev, data);
      FSM_STATE(DNDState, ITEM_PICKUP, ev, data);
      FSM_STATE(DNDState, INV_PICKUP, ev, data);
      FSM_STATE(DNDState, END, ev, data);
    default:
      dbc::log(fmt::format("event received with data but state={} is not handled", int($state)));
    }

    return !in_state(DNDState::END);
  }

  void DNDLoot::START(Event ev) {
    using enum Event;
    dbc::check(ev == STARTED, "START not given a STARTED event.");
    END(CLOSE);
  }

  void DNDLoot::LOOTING(Event ev, std::any data) {
    using enum Event;

    switch(ev) {
      case LOOT_OPEN:
        END(CLOSE);
        break;
      case LOOT_SELECT:
        $grab_source = start_grab($loot_ui.$gui, data);
        if($grab_source) state(DNDState::LOOT_GRAB);
        break;
      case INV_SELECT:
        $grab_source = start_grab($status_ui.$gui, data);
        if($grab_source) state(DNDState::INV_GRAB);
        break;
      default:
        break; // ignore
    }
  }

  void DNDLoot::LOOT_GRAB(Event ev, std::any data) {
    using enum Event;

    switch(ev) {
      case LOOT_OPEN:
        END(CLOSE);
        break;
      case LOOT_SELECT: {
          auto drop_id = std::any_cast<guecs::Entity>(data);

          if(move_or_swap($loot_ui, drop_id)) {
            state(DNDState::LOOTING);
          }
        } break;
      case INV_SELECT:
        if(commit_drop($loot_ui.$gui,
              $status_ui.$gui, $grab_source, data))
        {
          state(DNDState::LOOTING);
        }
        break;
      default:
         handle_mouse(ev, $loot_ui.$gui);
    }
  }

  void DNDLoot::INV_GRAB(Event ev, std::any data) {
    using enum Event;

    switch(ev) {
      case LOOT_OPEN:
        END(CLOSE);
        break;
      case LOOT_SELECT:
        if(commit_drop($status_ui.$gui,
              $loot_ui.$gui, $grab_source, data))
        {
          state(DNDState::LOOTING);
        }
        break;
      case INV_SELECT: {
          auto drop_id = std::any_cast<guecs::Entity>(data);
          if(move_or_swap($status_ui, drop_id)) {
            state(DNDState::LOOTING);
          }
        } break;
      default:
         handle_mouse(ev, $status_ui.$gui);
    }
  }

  void DNDLoot::INV_PICKUP(Event ev, std::any data) {
    using enum Event;

    switch(ev) {
      case AIM_CLICK: {
          // take from inventory, drop on floor
          bool worked = throw_on_floor($status_ui.$gui, true);
          dbc::check(worked, "Need to fix this, should be able to abort.");
          END(CLOSE);
        } break;
      case INV_SELECT: {
          auto drop_id = std::any_cast<guecs::Entity>(data);
          if(move_or_swap($status_ui, drop_id)) {
            END(CLOSE);
          }
        } break;
      default:
         handle_mouse(ev, $status_ui.$gui);
    }
  }

  void DNDLoot::ITEM_PICKUP(Event ev, std::any data) {
    using enum Event;

    switch(ev) {
      case INV_SELECT:
        if(commit_drop($loot_ui.$gui, $status_ui.$gui, $grab_source, data))
        {
          END(CLOSE);
        }
        break;
      case AIM_CLICK: {
        // THIS IS PUT IT BACK ON THE FLOOR
        bool worked = throw_on_floor($loot_ui.$gui, false);
        dbc::check(worked, "Failed to drop it back on the floor.");
        END(CLOSE);
       } break;
      default:
        handle_mouse(ev, $loot_ui.$gui);
    }
  }

  void DNDLoot::END(Event ev, std::any data) {
    using enum Event;

    switch(ev) {
      case LOOT_ITEM: {
          auto gui_id = $loot_ui.$gui.entity("item_0");
          if(hold_item($loot_ui.$gui, gui_id)) {
            state(DNDState::ITEM_PICKUP);
          }
        } break;
      case INV_SELECT: {
          auto gui_id = std::any_cast<guecs::Entity>(data);
          if(hold_item($status_ui.$gui, gui_id)) {
            state(DNDState::INV_PICKUP);
          }
        } break;
      case LOOT_OPEN:
        open();
        state(DNDState::LOOTING);
        break;
      case CLOSE:
        // called the first time transitioning to END
        close();
        state(DNDState::END);
        break;
      case TICK: // ignored
        break;
      default:
        dbc::log(fmt::format("invalid event: {}", int(ev)));
    }
  }

  void DNDLoot::handle_mouse(Event ev, guecs::UI& gui) {
    using enum Event;

    switch(ev) {
      case MOUSE_DRAG:
      case MOUSE_MOVE: {
           if($grab_source) {
             auto& source = gui.get<guecs::GrabSource>(*$grab_source);
             source.move($window.mapPixelToCoords($router.position));
           }
         } break;
      default:
         break; // ignored
    }
  }

  void DNDLoot::clear_grab() {
    dbc::log("CLEARED!");
    $grab_source = std::nullopt;
    $grab_sprite = nullptr;
  }

  void DNDLoot::open() {
    $loot_ui.active = true;
  }

  void DNDLoot::close() {
    $loot_ui.active = false;
  }

  void DNDLoot::render() {
    if($grab_source && $grab_sprite) {
      $window.draw(*$grab_sprite);
    }
  }

  std::optional<guecs::Entity> DNDLoot::start_grab(guecs::UI& gui, std::any data) {
    auto gui_id = std::any_cast<guecs::Entity>(data);

    if(auto source = gui.get_if<guecs::GrabSource>(gui_id)) {
      $grab_sprite = source->sprite;
      source->grab();
      return gui_id;
    } else {
      return std::nullopt;
    }
  }

  bool DNDLoot::commit_drop(guecs::UI& source, guecs::UI& target,
      std::optional<guecs::Entity> source_id, std::any data)
  {
    if(!source_id) return false;
    auto target_id = std::any_cast<guecs::Entity>(data);

    dbc::check(target.has<guecs::DropTarget>(target_id),
        "gui does not have a DropTarget at that slot");
    dbc::check(source.has<guecs::GrabSource>(*source_id),
        "gui does not have a GrabSource at that slot");

    auto& grab = source.get<guecs::GrabSource>(*source_id);
    auto& drop = target.get<guecs::DropTarget>(target_id);

    if(drop.commit(grab.world_entity)) {
      grab.commit();
      clear_grab();
      return true;
    } else {
      return false;
    }
  }

  bool DNDLoot::commit_move(guecs::UI& gui, std::optional<guecs::Entity> source_id, guecs::Entity drop_id) {
    dbc::check(source_id != std::nullopt, "source_id must exist");

    auto& grab = gui.get<guecs::GrabSource>(*source_id);
    grab.commit();

    auto& drop = gui.get<guecs::DropTarget>(drop_id);

    if(drop.commit(grab.world_entity)) {
      clear_grab();
      return true;
    } else {
      // swap with the target instead
      return false;
    }
  }

  bool DNDLoot::hold_item(guecs::UI &gui, guecs::Entity gui_id) {
    // NOTE: if > 1 items, go to LOOT_OPEN instead
    $grab_source = start_grab(gui, gui_id);

    if($grab_source) {
      auto& source = gui.get<guecs::GrabSource>(*$grab_source);
      $grab_sprite = source.sprite;
      // call this once to properly position the sprite
      handle_mouse(Event::MOUSE_MOVE, gui);
    }

    return $grab_source != std::nullopt;
  }

  /*
   * Dropping on the ground is only possible from the
   * status_ui for now.
   */
  bool DNDLoot::throw_on_floor(guecs::UI& gui, bool from_status) {
    dbc::check($grab_source != std::nullopt, "attempt to commit_drop but no grab_source set");
    dbc::check(gui.has<guecs::GrabSource>(*$grab_source),
        "StatusUI doesn't actually have that GrabSource in the gui.");

    auto& grab = gui.get<guecs::GrabSource>(*$grab_source);
    grab.commit();

    bool result = false;

    if(from_status) {
      result = $status_ui.drop_item(grab.world_entity);
    } else {
      result = $loot_ui.drop_item(grab.world_entity);
    }

    clear_grab();
    return result;
  }

  /*
   * If I refactored everything to use a levelmanager module then
   * this and many other things could go away. Access to $level is
   * making this too complicated.  Do this for now, but fix bug #59.
   */
  bool DNDLoot::move_or_swap(StatusUI& ui, guecs::Entity drop_id) {
    if(ui.occupied(drop_id)) {
      ui.swap(*$grab_source, drop_id);
      clear_grab();
      return true;
    } else {
      return commit_move(ui.$gui, $grab_source, drop_id);
    }
  }

  bool DNDLoot::move_or_swap(LootUI& ui, guecs::Entity drop_id) {
    if(ui.occupied(drop_id)) {
      ui.swap(*$grab_source, drop_id);
      clear_grab();
      return true;
    } else {
      return commit_move(ui.$gui, $grab_source, drop_id);
    }
  }

}
