#include <boost/format.hpp>
#include <cctype>

#include "boxes.h"
#include "dispatchers.h"
#include "logging.h"

Dispatch::Dispatch(Session *s) : sess{s} {};

Dispatch::~Dispatch(){};

void Dispatch::to_server(const std::string &send) { sess->to_server(send); }
void Dispatch::to_client(const std::string &send) { sess->to_client(send); }
const std::string &Dispatch::get_prompt(void) { return sess->get_prompt(); }

void Dispatch::setNotify(notifyFunc nf) { notify_ = nf; }

void Dispatch::notify(void) {
  if (notify_) {
    sess->post(notify_);
    notify_ = nullptr;
  }
}

MainDispatch::MainDispatch(Session *s) : Dispatch{s}, id{s}, md{s} {
  BUGZ_LOG(warning) << "MainDispatch()";
}

MainDispatch::~MainDispatch() { BUGZ_LOG(warning) << "~MainDispatch()"; }

void MainDispatch::activate(void) {
  // how to set this event to our method?
  sess->emit_server_line = [this](const std::string &s) { server_line(s); };
  sess->emit_server_prompt = [this](const std::string &s) { server_prompt(s); };
  sess->emit_client_input = nullptr;
  // sess->emit_client_input = [this](const std::string &s) { client_input(s);
  // };
  sess->show_client = false; // don not auto-send server to client
  sess->talk_direct = false; // do not auto-send client to server
  count = 0;
  old_prompt = sess->get_prompt();

  /*
  ╔══════════════════════════════╗
  ║    TradeWars Proxy Active    ║
  ╚══════════════════════════════╝
     -=>
  */

  Boxes box(30, 1, true);
  box.boxcolor = "\x1b[1;33;44m";
  box.textcolor = "\x1b[1;33;44m";
  to_client("\n\r");
  to_client(box.top());
  std::string output = "    TradeWars Proxy \x1b[5mActive\x1b[0;1;33;44m    ";
  to_client(box.row(output));
  to_client(box.bottom());

  // to_client("\n\r\x1b[1;34mWELCOME!  You are now in the proxy zone...\n\r");
  id.prompt = "\x1b[0m    \x1b[1;33;44m-=>\x1b[0m \x1b[1;37;44m";
  id.max_length = 15;
  id.setNotify([this]() { this->have_input(); });
  id.activate();
}

void MainDispatch::have_input(void) {
  ++count;
  std::string output =
      str(boost::format("Your Input (%2%): [%1%]\n\r") % id.input % count);
  to_client("\x1b[0m");
  to_client(output);
  if (id.input == "?") {
    // Maybe?  Maybe not.
  }

  if (id.input == "menu") {
    md.menu_box_color = "\x1b[1;33;44m";
    md.menu_text_color = "\x1b[1;37;44m";
    md.menu_title = "Proxy Menu";
    md.menu_options_color = "\x1b[1;36;40m";
    // md.menu_prompt = " --==>> ";

    // bold white to white ---  black to green
    // md.menu_prompt = "\x1b[1;37;47m\xdb\xb2\xb1\xb0\x1b[0;30;47m RED GREEN
    // \x1b[30;42m\xdb\xb2\xb1\xb0\x1b[0m : ";

    // md.menu_prompt = "\x1b[0;31;47m\xdb\xb2\xb1\xb0\x1b[0;30;47m RED
    // GREEN\x1b[37;42m\xdb\xb2\xb1\xb0\x1b[0m : ";
    const char *CP437_GRADIENT = "\xdb\xb2\xb1\xb0 "; // 100, 75, 50, 25, 0

    md.menu_prompt = "\x1b[0;31;40m\xdb\xb2\xb1\xb0 \x1b[31;40mRED "
                     "\x1b[32;40mGREEN\x1b[30;42m\xdb\xb2\xb1\xb0 \x1b[0m : ";
    md.lazy = true;
    md.menu = {{"A", "Apple"}, {"B", "Blue"}, {"R", "Rabbit"}, {"Z", "ZOOO!"}};
    md.setNotify([this]() { this->menu_choice(); });
    md.activate();
    return;
  }

  if (id.input == "menu2") {
    md.lazy = false;
    md.setNotify([this]() { this->menu_choice(); });
    md.activate();
    return;
  }
  if (id.input.empty()) {
    // if (count >= 5) {
    auto lines = Boxes::alert(" Returning you to the game... ", "",
                              "\x1b[1;32m", 30, 1, true);
    // I'm not setting the box color, so the last color bleeds over.
    to_client("\x1b[0m");
    for (auto line : lines) {
      to_client(line);
    };
    to_client("Returning you to the game...\n\r");
    deactivate();
  } else {
    // prompt it again, sam.
    id.setNotify([this]() { this->have_input(); });
    id.activate();
  }
}

void MainDispatch::menu_choice(void) {
  if (md.input.empty()) {
    to_client("Menu abort.\n\r");
  } else {
    std::string text = "Back from menu [";
    text.append(md.input);
    text.append("] was your selection.\n\r");
    to_client(text);
  }

  id.max_length = 15;
  id.setNotify([this]() { this->have_input(); });
  id.activate();
}

void MainDispatch::deactivate(void) {
  // Since we're the main thing there --
  sess->emit_server_line = nullptr;
  sess->emit_server_prompt = nullptr;
  sess->emit_client_input = nullptr;
  sess->show_client = true;
  sess->talk_direct = true;
  sess->set_prompt(old_prompt);
  notify();
}

void MainDispatch::server_line(const std::string &line) {
  BUGZ_LOG(info) << "MDSL: " << line;
  to_client("SL: ");
  to_client(line);
  to_client("\n\r");
}

void MainDispatch::server_prompt(const std::string &prompt) {
  BUGZ_LOG(info) << "MDSP: " << prompt;
}

#ifdef NEVERMORE
void MainDispatch::client_input(const std::string &input) {
  // I don't care what the old prompt looked liked at this point.
  BUGZ_LOG(warning) << "Got: " << input; //  << " prompt=" << get_prompt();

  // Serious problem when the input = "\x1b" ESC.  The output gets gummed/locked
  // up.
  if (input == "\x1b") {
    return;
  }

  ++count;
  std::string output = str(boost::format("MSG %1%: [%2%]\n\r") % count % input);
  to_client(output);
  if (count >= 5) {
    to_client("And we're outta here!\n\r");
    deactivate();
  }
}
#endif

InputDispatch::InputDispatch(Session *s) : Dispatch(s) {
  BUGZ_LOG(warning) << "InputDispatch()";
}

InputDispatch::~InputDispatch() { BUGZ_LOG(warning) << "~InputDispatch()"; }

void InputDispatch::activate(void) {
  ds = sess->save_settings();
  sess->emit_server_line = [this](const std::string &s) { server_line(s); };
  sess->emit_server_prompt =
      nullptr; // [this](const std::string &s) { server_prompt(s); };
  sess->emit_client_input = [this](const std::string &s) { client_input(s); };
  sess->show_client = false; // don not auto-send server to client
  sess->talk_direct = false; // do not auto-send client to server

  input.clear();
  to_client(prompt);
}

void InputDispatch::deactivate(void) {
  sess->restore_settings(ds);
  notify();
}

void InputDispatch::server_line(const std::string &line) {}
// void InputDispatch::server_prompt(const std::string &prompt) {}

void InputDispatch::client_input(const std::string &cinput) {
  for (const char ch : cinput) {
    if (isprint(ch)) {
      // Ok!
      if (input.length() < max_length) {
        to_client(std::string(1, ch));
        input += ch;
      }
    } else if ((ch == '\b') || (ch == 0x7f)) {
      // Backspace or rubout
      if (input.length() > 0) {
        to_client("\b \b");
        input.erase(input.size() - 1);
      }
    } else if (ch == '\r') {
      // Ok, we're done!
      BUGZ_LOG(info) << "InputDispatch done: " << input;
      to_client("\x1b[0m\n\r");
      deactivate();
    }
  }
}

/**
 * Menu Dispatch
 *
 * Two types of menus:
 * lazy:  display the menu name and show prompt. ? shows menu.
 * non-lazy:  displays menu + prompts.
 *
 *
 */

MenuDispatch::MenuDispatch(Session *s) : Dispatch{s} {
  BUGZ_LOG(warning) << "MenuDispatch()";
}

MenuDispatch::~MenuDispatch() { BUGZ_LOG(warning) << "~MenuDispatch()"; }

void MenuDispatch::activate(void) {
  calculate_widths();
  input.clear();

  BUGZ_LOG(warning) << "MenuDispatch::activate() " << max_width << ", "
                    << max_option_width;

  ds = sess->save_settings();
  sess->emit_server_line = [this](const std::string &s) { server_line(s); };
  sess->emit_server_prompt = nullptr;
  sess->emit_client_input = [this](const std::string &s) { client_input(s); };

  if (lazy)
    menubox();
  else
    help();
  to_client(menu_prompt);
}

void MenuDispatch::deactivate(void) {
  sess->restore_settings(ds);
  notify();
}

void MenuDispatch::help(void) {
  size_t max = max_width;
  Boxes mbox(max, 1, true);
  mbox.boxcolor = menu_box_color;

  if (lazy) {
    // just the menu
    mbox.textcolor = menu_options_color;
    to_client(mbox.top());
    for (auto const menu_item : menu) {
      std::string text = " ";
      text.append(menu_item.first);
      text.append(" - ");
      text.append(menu_item.second);
      while (text.length() < max)
        text.append(1, ' ');
      to_client(mbox.row(text));
    }
    to_client(mbox.bottom());
  } else {
    // full menu
    mbox.textcolor = menu_text_color;
    to_client(mbox.top());
    std::string title = centered(max, menu_title);
    BUGZ_LOG(debug) << "help max=" << max << " [" << title << "]";

    to_client(mbox.row(title));
    to_client(mbox.middle());
    mbox.textcolor = menu_options_color;
    for (auto const menu_item : menu) {
      std::string text = " ";
      text.append(menu_item.first);
      text.append(" - ");
      text.append(menu_item.second);
      while (text.length() < max)
        text.append(1, ' ');
      to_client(mbox.row(text));
    }
    to_client(mbox.bottom());
  }
}

std::string MenuDispatch::centered(int length, const std::string &s) {
  std::string text = s;
  size_t leftovers = length - text.length();
  int count = leftovers / 2;
  if (count > 0) {
    text.insert(0, count, ' ');
    text.append(count, ' ');
  }

  if (leftovers % 1 == 1)
    text.append(1, ' ');
  return text;
}

void MenuDispatch::menubox(void) {
  // just the menu box
  std::string title = centered(max_width, menu_title);
  /*
  int leftovers = max - menu_title.length();
  while (leftovers > 2) {
    title.insert(0, 1, " ");
    title.append(1, " ");
    leftovers -= 2;
  };
  if (leftovers == 1)
    title.append(1, " ");
  */
  BUGZ_LOG(debug) << "menubox max=" << max_width << " [" << title << "]";

  auto abox =
      Boxes::alert(title, menu_box_color, menu_text_color, max_width, 1, true);
  for (auto line : abox) {
    to_client(line);
  }
}

void MenuDispatch::calculate_widths(void) {
  max_width = menu_title.length() + 2;
  max_option_width = 0;
  for (auto key : menu) {
    size_t menu_line_length =
        1 + key.first.length() + 3 + key.second.length() + 1;
    if (menu_line_length > max_width)
      max_width = menu_line_length;
    if (key.first.length() > max_option_width)
      max_option_width = key.first.length();
  }
  instant = max_option_width == 1;
}

void MenuDispatch::server_line(const std::string &line) {}

void MenuDispatch::client_input(const std::string &cinput) {
  for (auto const ch : cinput) {
    // not likely that we'd have more then one,
    // but deal with it correctly.

    if (ch == '\r') {
      // enter
      if (instant)
        return;

      for (auto const mnu : menu) {
        if (mnu.first == input) {
          to_client("\x1b[0m\n\r");
          deactivate();
          return;
        }
      }

      // input wasn't found ?
      while (input.length() > 0) {
        to_client("\b \b");
        input.erase(input.length() - 1);
      }
      return; // don't continue ...
    }

    if (ch == '\x1b') {
      // [ESC] - erase the input string
      while (input.length() > 0) {
        to_client("\b \b");
        input.erase(input.length() - 1);
      }

      // Exit - allow escape from menu
      deactivate();
      return;
    }

    if (ch == '\b') {
      if (input.length() > 0) {
        to_client("\b \b");
        input.erase(input.length() - 1);
      }
    }

    if (ch == '?') {
      to_client(cinput); // display what they entered.
      to_client("\x1b[0m\n\r");
      help();
      to_client(menu_prompt);
      return;
    }

    if (isprint(ch)) {
      char c = ch;
      if (!case_sensitive)
        c = toupper(ch);

      // ok, it's a printable character
      if (input.length() < max_option_width) {
        // ok, there's room.
        to_client(std::string(1, c));
        input.append(1, c);
      }

      if (instant) {
        for (auto const mnu : menu) {
          if (mnu.first == input) {
            to_client("\x1b[0m\n\r");
            deactivate();
            return;
          }
        }
      }
    }
  }
}

/*
 * CoreDispatch:  This is an example class that does dispatch.
 * Copy this and make changes from there...
 */
CoreDispatch::CoreDispatch(Session *s) : Dispatch{s} {
  BUGZ_LOG(warning) << "CoreDispatch()";
}

void CoreDispatch::activate(void) {
  // save things, set things
}

void CoreDispatch::deactivate(void) {
  // restore things
  notify();
}

void CoreDispatch::server_line(const std::string &line) {}
void CoreDispatch::server_prompt(const std::string &prompt) {}
void CoreDispatch::client_input(const std::string &input) {
  BUGZ_LOG(warning) << "Got: " << input << " prompt=" << get_prompt();
}