#include "ipc_state.h"

#define _WEBSOCKETPP_CPP11_STRICT_

#include <fmt/core.h>

#include <chrono>
#include <memory>
#include <mutex>
#include <nlohmann/json.hpp>
#include <optional>
#include <set>
#include <string>
#include <thread>
#include <tuple>
#include <wswrap.hpp>

#include "ap_state.h"
#include "logger.h"
#include "tracker_frame.h"

namespace {

struct IPCState {
  std::mutex state_mutex;
  TrackerFrame* tracker_frame = nullptr;

  // Protected state
  bool initialized = false;
  std::string address;
  bool should_disconnect = false;

  std::optional<std::string> status_message;

  bool slot_matches = false;
  std::string tracker_ap_server;
  std::string tracker_ap_user;
  std::string game_ap_server;
  std::string game_ap_user;

  std::optional<std::tuple<int, int>> player_position;
  std::set<std::string> solved_panels;

  // Thread state
  std::unique_ptr<wswrap::WS> ws;
  bool connected = false;

  void SetTrackerFrame(TrackerFrame* frame) { tracker_frame = frame; }

  void Connect(std::string a) {
    // This is the main concurrency concern, as it mutates protected state in an
    // important way. Thread() is documented with how it interacts with this
    // function.
    std::lock_guard state_guard(state_mutex);

    if (!initialized) {
      std::thread([this]() { Thread(); }).detach();

      initialized = true;
    } else if (address != a) {
      should_disconnect = true;
    }

    address = a;
  }

  std::optional<std::string> GetStatusMessage() {
    std::lock_guard state_guard(state_mutex);

    return status_message;
  }

  void SetTrackerSlot(std::string server, std::string user) {
    // This function is called from the APState thread, not the main thread, and
    // it mutates protected state. It only really competes with OnMessage(), when
    // a "Connect" message is received. If this is called right before, and the
    // tracker slot does not match the old game slot, it will initiate a
    // disconnect, and then the OnMessage() handler will see should_disconnect
    // and stop processing the "Connect" message. If this is called right after
    // and the slot does not match, IPC will disconnect, which is tolerable.
    std::lock_guard state_guard(state_mutex);

    tracker_ap_server = std::move(server);
    tracker_ap_user = std::move(user);

    CheckIfSlotMatches();

    if (!slot_matches) {
      should_disconnect = true;
      address.clear();
    }
  }

  bool IsConnected() {
    std::lock_guard state_guard(state_mutex);

    return slot_matches;
  }

  std::optional<std::tuple<int, int>> GetPlayerPosition() {
    std::lock_guard state_guard(state_mutex);

    return player_position;
  }

  std::set<std::string> GetSolvedPanels() {
    std::lock_guard state_guard(state_mutex);

    return solved_panels;
  }

 private:
  void Thread() {
    for (;;) {
      // initialized is definitely true because it is set to true when the thread
      // is created and only set to false within this block, when the thread is
      // killed. Thus, a call to Connect would always at most set
      // should_disconnect and address. If this happens before this block, it is
      // as if we are starting from a new thread anyway because should_disconnect
      // is immediately reset. If a call to Connect happens after this block,
      // then a connection attempt will be made to the wrong address, but the
      // thread will grab the mutex right after this and back out the wrong
      // connection.
      std::string ipc_address;
      {
        std::lock_guard state_guard(state_mutex);

        SetStatusMessage("Disconnected from game.");

        should_disconnect = false;

        slot_matches = false;
        game_ap_server.clear();
        game_ap_user.clear();

        player_position = std::nullopt;
        solved_panels.clear();

        if (address.empty()) {
          initialized = false;
          return;
        }

        ipc_address = address;

        SetStatusMessage("Connecting to game...");
      }

      int backoff_amount = 0;

      TrackerLog(fmt::format("Looking for game over IPC ({})...", ipc_address));

      while (!connected) {
        if (TryConnect(ipc_address)) {
          int backoff_limit = (backoff_amount + 1) * 10;

          for (int i = 0; i < backoff_limit && !connected; i++) {
            // If Connect is called right before this block, we will see and
            // handle should_disconnect. If it is called right after, we will do
            // one bad poll, one sleep, and then grab the mutex again right
            // after.
            {
              std::lock_guard state_guard(state_mutex);
              if (should_disconnect) {
                break;
              }
            }

            ws->poll();

            // Back off
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
          }

          backoff_amount++;
        } else {
          std::lock_guard state_guard(state_mutex);

          if (!should_disconnect) {
            should_disconnect = true;
            address.clear();

            SetStatusMessage("Disconnected from game.");
          }

          break;
        }

        // If Connect is called right before this block, we will see and handle
        // should_disconnect. If it is called right after, and the connection
        // was unsuccessful, we will grab the mutex after one bad connection
        // attempt. If the connection was successful, we grab the mutex right
        // after exiting the loop.
        bool show_error = false;
        {
          std::lock_guard state_guard(state_mutex);

          if (should_disconnect) {
            break;
          } else if (!connected) {
            if (backoff_amount >= 10) {
              should_disconnect = true;
              address.clear();

              SetStatusMessage("Disconnected from game.");

              show_error = true;
            } else {
              TrackerLog(fmt::format("Retrying IPC in {} second(s)...",
                                     backoff_amount + 1));
            }
          }
        }

        // We do this after giving up the mutex because otherwise we could
        // deadlock with the main thread.
        if (show_error) {
          TrackerLog("Giving up on IPC.");

          wxMessageBox("Connection to Lingo timed out.", "Connection failed",
                       wxOK | wxICON_ERROR);
          break;
        }
      }

      // Pretty much every lock guard in the thread is the same. We check for
      // should_disconnect, and if it gets set directly after the block, we do
      // minimal bad work before checking for it again.
      {
        std::lock_guard state_guard(state_mutex);
        if (should_disconnect) {
          ws.reset();
          continue;
        }
      }

      while (connected) {
        ws->poll();

        std::this_thread::sleep_for(std::chrono::milliseconds(100));

        {
          std::lock_guard state_guard(state_mutex);
          if (should_disconnect) {
            ws.reset();
            break;
          }
        }
      }
    }
  }

  bool TryConnect(std::string ipc_address) {
    try {
      ws = std::make_unique<wswrap::WS>(
          ipc_address, [this]() { OnConnect(); }, [this]() { OnClose(); },
          [this](const std::string& s) { OnMessage(s); },
          [this](const std::string& s) { OnError(s); });
      return true;
    } catch (const std::exception& ex) {
      TrackerLog(fmt::format("Error connecting to Lingo: {}", ex.what()));
      wxMessageBox(ex.what(), "Error connecting to Lingo", wxOK | wxICON_ERROR);
      ws.reset();
      return false;
    }
  }

  void OnConnect() {
    connected = true;

    {
      std::lock_guard state_guard(state_mutex);

      slot_matches = false;
      player_position = std::nullopt;
      solved_panels.clear();
    }
  }

  void OnClose() {
    connected = false;

    {
      std::lock_guard state_guard(state_mutex);

      slot_matches = false;
    }
  }

  void OnMessage(const std::string& s) {
    TrackerLog(s);

    auto msg = nlohmann::json::parse(s);

    if (msg["cmd"] == "Connect") {
      std::lock_guard state_guard(state_mutex);
      if (should_disconnect) {
        return;
      }

      game_ap_server = msg["slot"]["server"];
      game_ap_user = msg["slot"]["player"];

      CheckIfSlotMatches();

      if (!slot_matches) {
        tracker_frame->ConnectToAp(game_ap_server, game_ap_user,
                                   msg["slot"]["password"]);
      }
    } else if (msg["cmd"] == "UpdatePosition") {
      std::lock_guard state_guard(state_mutex);

      player_position =
          std::make_tuple<int, int>(msg["position"]["x"], msg["position"]["z"]);

      tracker_frame->RedrawPosition();
    } else if (msg["cmd"] == "SolvePanels") {
      std::lock_guard state_guard(state_mutex);

      for (std::string panel : msg["panels"]) {
        solved_panels.insert(std::move(panel));
      }

      tracker_frame->UpdateIndicators(kUPDATE_ONLY_PANELS);
    }
  }

  void OnError(const std::string& s) {}

  // Assumes mutex is locked.
  void CheckIfSlotMatches() {
    slot_matches = (tracker_ap_server == game_ap_server &&
                    tracker_ap_user == game_ap_user);

    if (slot_matches) {
      SetStatusMessage("Connected to game.");

      Sync();
    } else if (connected) {
      SetStatusMessage("Local game doesn't match AP slot.");
    }
  }

  // Assumes mutex is locked.
  void SetStatusMessage(std::optional<std::string> msg) {
    status_message = msg;

    tracker_frame->UpdateStatusMessage();
  }

  void Sync() {
    nlohmann::json msg;
    msg["cmd"] = "Sync";

    ws->send_text(msg.dump());
  }
};

IPCState& GetState() {
  static IPCState* instance = new IPCState();
  return *instance;
}

}  // namespace

void IPC_SetTrackerFrame(TrackerFrame* tracker_frame) {
  GetState().SetTrackerFrame(tracker_frame);
}

void IPC_Connect(std::string address) { GetState().Connect(address); }

std::optional<std::string> IPC_GetStatusMessage() {
  return GetState().GetStatusMessage();
}

void IPC_SetTrackerSlot(std::string server, std::string user) {
  GetState().SetTrackerSlot(server, user);
}

bool IPC_IsConnected() { return GetState().IsConnected(); }

std::optional<std::tuple<int, int>> IPC_GetPlayerPosition() {
  return GetState().GetPlayerPosition();
}

std::set<std::string> IPC_GetSolvedPanels() {
  return GetState().GetSolvedPanels();
}