#include <boost/bind.hpp>
#include <iostream>

#include <boost/format.hpp>
// #include <boost/log/core.hpp>
// #include <boost/log/trivial.hpp>

#include <regex>

#include "config.h"
#include "logging.h"
#include "session.h"

#include "galaxy.h"

#include <string>

// #include <boost/log/attributes/named_scope.hpp>

bool replace(std::string &str, const std::string &from, const std::string &to) {
  size_t start_pos = str.find(from);
  if (start_pos == std::string::npos)
    return false;
  do {
    str.replace(start_pos, from.length(), to);
  } while ((start_pos = str.find(from)) != std::string::npos);
  return true;
}

bool replace(std::string &str, const char *from, const char *to) {
  size_t start_pos = str.find(from);
  if (start_pos == std::string::npos)
    return false;
  do {
    str.replace(start_pos, strlen(from), to);
  } while ((start_pos = str.find(from)) != std::string::npos);
  return true;
}

void ansi_clean(std::string &str) {
  static std::regex ansi_cleaner("\x1b\[[0-9;]*[A-Zmh]",
                                 std::regex_constants::ECMAScript);
  str = std::regex_replace(str, ansi_cleaner, "");
}

void high_ascii(std::string &str) {
  // the + replaces all of them into one.  I want each high ascii replaced with
  // #.
  static std::regex high_cleaner("[\x80-\xff]",
                                 std::regex_constants::ECMAScript);
  str = std::regex_replace(str, high_cleaner, "#");
}

std::smatch ansi_newline(const std::string &str) {
  static std::regex ansi_nl("\x1b\[[0-9;]*[JK]",
                            std::regex_constants::ECMAScript);
  std::smatch m;
  std::regex_search(str, m, ansi_nl);
  return m;
}

std::string clean_string(const std::string &source) {
  // BOOST_LOG_NAMED_SCOPE("clean_string");
  std::string clean = source;

  replace(clean, "\n", "\\n");
  replace(clean, "\r", "\\r");
  replace(clean, "\b", "\\b");

  // ANSI too
  ansi_clean(clean);
  // BUGZ_LOG(error) << "cleaned: " << clean;
  high_ascii(clean);

  replace(clean, "\x1b", "^");

  return clean;
}

Session::Session(boost::asio::ip::tcp::socket socket,
                 boost::asio::io_service &io_service, std::string hostname,
                 std::string port)
    : main(this), socket_(std::move(socket)), io_service_{io_service},
      resolver_{io_service}, server_{io_service}, prompt_timer_{io_service},
      keep_alive_{io_service}, host{hostname}, port{port} {
  BUGZ_LOG(info) << "Session::Session()";
  // server_sent = 0;
  time_ms = 50;
  if (CONFIG["prompt_timeout"])
    time_ms = CONFIG["prompt_timeout"].as<int>();

  keepalive_secs = 45;
  if (CONFIG["keepalive"])
    keepalive_secs = CONFIG["keepalive"].as<int>();
}

void Session::start(void) {
  // BOOST_LOG_NAMED_SCOPE();

  // If I want the file and line number information, here's how to do it:
  // BUGZ_LOG(info) << boost::format("(%1%:%2%) ") % __FILE__ % __LINE__

  BUGZ_LOG(info) << "Session::start()";
  auto self(shared_from_this());
  // read_buffer.reserve(1024);
  // do_write("Welcome!\n");
  client_read();
}

Session::~Session() { BUGZ_LOG(info) << "~Session"; }

/**
 * Returns the current server prompt.
 *
 * NOTE:  This is the raw string from the server, so it can contain
 * color codes.  Make sure you clean it before trying to test it for
 * any text.
 *
 * @return const std::string&
 */
const std::string &Session::get_prompt(void) { return server_prompt; }
void Session::set_prompt(const std::string &prompt) { server_prompt = prompt; }

void Session::post(notifyFunc nf) {
  if (nf) {
    BUGZ_LOG(info) << "Session::post()";
    io_service_.post(nf);
  } else {
    BUGZ_LOG(error) << "Session::post( nullptr )";
  }
}

void Session::parse_auth(void) {
  // how many nulls should I be seeing?
  // \0user\0pass\0terminal/SPEED\0
  // If I don't have a proper rlogin value here, it isn't going
  // to work when I try to connect to the rlogin server.

  if (rlogin_auth.size() > 10)
    rlogin_name = rlogin_auth.c_str() + 1;
  else
    rlogin_name = "?";
}

void Session::on_connect(const boost::system::error_code error) {
  // We've connected to the server!  WOOT WOOT!
  // BOOST_LOG_NAMED_SCOPE("Session");

  SL_parser = nullptr;

  if (!error) {
    BUGZ_LOG(info) << "Connected to " << host;
    to_client("Connected...\n\r");
    connected = true;
    if (rlogin_auth[0] != 0) {
      // Ok, the rlogin information was junk --
      to_client("Let me make up some fake rlogin data for you...\n\r");
      char temp[] = "\0test\0test\0terminal/9600\0";
      std::string tmp(temp, sizeof(temp));
      to_server(tmp);
    } else {
      to_server(rlogin_auth);
    }

    server_read();
  } else {
    // TODO:
    std::string output =
        str(boost::format("Failed to connect: %1%:%2%\n\r") % host % port);
    to_client(output);
    BUGZ_LOG(error) << "Failed to connect to " << host << ":" << port;
    BUGZ_LOG(warning) << "socket.shutdown()";
    socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both);
  }
}

/**
 * Called with the current line received from the server.
 *
 * This will do server parsing.  Sector/Ports/Connecting Sectors.
 * Port status/inventory/%.
 *
 * See \ref split_lines()
 * @param line
 */
void Session::on_server_line(const std::string &line) {
  BUGZ_LOG(info) << "SL: [" << line << "]";

  if (line.find("TradeWars Game Server   ") != std::string::npos) {
    to_client("\rTradeWars Proxy v2++ READY (~ or ESC to activate)\n\r");
    game = 0;
    // reset "active game" -- we're back at the menu
  }

  /*
     ____                             _     _
    / ___|  ___ _ ____   _____ _ __  | |   (_)_ __   ___
    \___ \ / _ \ '__\ \ / / _ \ '__| | |   | | '_ \ / _ \
     ___) |  __/ |   \ V /  __/ |    | |___| | | | |  __/
    |____/ \___|_|    \_/ \___|_|    |_____|_|_| |_|\___|

     ____                _
    |  _ \ __ _ _ __ ___(_)_ __   __ _
    | |_) / _` | '__/ __| | '_ \ / _` |
    |  __/ (_| | |  \__ \ | | | | (_| |
    |_|   \__,_|_|  |___/_|_| |_|\__, |
                                 |___/

  This is where all of the server lines are gleaned for all the
  information that we can get out of them.

   */

  if (line.find("Selection (? for menu): ") != std::string::npos) {
    char ch = line[line.length() - 1];
    if (ch >= 'A' && ch < 'Q') {
      game = ch;
      BUGZ_LOG(warning) << "GAME " << game << " activated!";
    }
    // not needed (handled by above Game Server check).
    if (ch == 'Q')
      game = 0;
  }

  // Do I need to run through the tests (below) before calling the parser here?
  // Or will the parsers know when they are done processing, and clear?

  if (SL_parser) {
    SL_parser(line);
  }

  // ok, maybe that was the end of parsing?

  if (!SL_parser) {
    if ((line.substr(0, 19) == "The shortest path (") ||
        (line.substr(0, 7) == "  TO > ")) {
      SL_parser = [this](const std::string s) { this->SL_warpline(s); };
      SL_warpline(line);
    } else {
      if (line.substr(0, 43) == " Items     Status  Trading % of max OnBoard") {
        SL_parser = [this](const std::string s) { this->SL_portline(s); };
        SL_parser(line);
      } else {
        if (line.substr(0, 10) == "<Thievery>") {
          SL_parser = [this](const std::string s) { this->SL_thiefline(s); };
          SL_parser(line);
        } else {
          if (line == ": ") {
            SL_parser = [this](const std::string s) { this->SL_cimline(s); };
          } else {
            if (line.substr(0, 1) == "Sector  : ") {
              SL_parser = [this](const std::string s) {
                this->SL_sectorline(s);
              };
              SL_parser(line);
            }
          }
        }
      }
    }
  }

  // should I have an internal emit_server_line for parsing sections?
  // rather then having a weird state machine to track where we are?

  if (emit_server_line)
    emit_server_line(line);
}

void Session::SL_cimline(const std::string &line) {
  if (line == ": ENDINTERROG") {
    SL_parser = nullptr;
    return;
  }
  if (line == ": ") {
    // do I need to do anything special here for this?
    return;
  }
  if (line.empty()) {
    SL_parser = nullptr;
    return;
  }

  // parse cimline
  size_t pos = line.find('%');
  std::string work = line;

  if (pos == line.npos) {
    // warpcim

  } else {
    // portcim
  }
}
void Session::SL_thiefline(const std::string &line) {
  size_t pos = line.find("Suddenly you're Busted!");
  bool busted = pos != line.npos;
  if (busted) {
    BUGZ_LOG(fatal) << "set bust";
    SL_parser = nullptr;
  } else {
    pos = line.find("(You realize the guards saw you last time!)");
    if (pos != line.npos)
      SL_parser = nullptr;
  }

  // Are those the two ways to exit from this state?
}
void Session::SL_sectorline(const std::string &line) {}
void Session::SL_portline(const std::string &line) {
  if (line.empty()) {
    SL_parser = nullptr;
    return;
  }
  BUGZ_LOG(info) << "portline : " << line;
  size_t pos = line.find('%');
  if (pos != line.npos) {
    // Ok, this is a valid portline
    std::string work = line;
    replace(work, "Fuel Ore", "Fuel");
    BUGZ_LOG(fatal) << "re.split? : [" << work << "]";
  }
}

void Session::SL_warpline(const std::string &line) {
  if (line.empty()) {
    SL_parser = nullptr;
    return;
  }

  // process warp line
}
/**
 * Split server input into lines.
 *
 * @param line
 */
void Session::split_lines(std::string line) {
  // Does this have \n\r still on it?  I don't want them.

  // cleanup backspaces
  size_t pos;
  while ((pos = line.find('\b')) != std::string::npos) {
    // backspace?  OK!  (unless)
    if (pos == 0) {
      // first character, so there's nothing "extra" to erase.
      line = line.erase(pos, 1);
    } else
      line = line.erase(pos - 1, 2);
  }

  std::string temp = clean_string(line);
  on_server_line(temp);
}

/*
Call this with whatever we just received.

That will allow me to send "just whatever I got"
this time around, rather then trying to figure out
what was just added to server_prompt.

What about \r, \b ?  Should that "reset" the server_prompt?

\r should not, because it is followed by \n (eventually)
and that completes my line.

 */
void Session::process_lines(std::string &received) {
  // break server_prompt into lines and send/process one by one.

  size_t pos, rpos;
  server_prompt.append(received);

  // I also need to break on r"\x1b[\[0-9;]*JK", treat these like \n

  while ((pos = server_prompt.find('\n', 0)) != std::string::npos) {

    std::string line;

    std::smatch m = ansi_newline(server_prompt);
    if (!m.empty()) {
      // We found one.
      size_t mpos = m.prefix().length();
      // int mlen = m[0].length();

      if (mpos < pos) {
        // Ok, the ANSI newline is before the \n

        // perform this process with the received line
        std::smatch rm = ansi_newline(received);
        if (!rm.empty()) {
          size_t rpos = rm.prefix().length();
          int rlen = rm[0].length();
          if (show_client) {
            line = received.substr(0, rpos + rlen);
            to_client(line);
          }
          received = rm.suffix();
        }

        // perform this on the server_prompt line
        line = m.prefix();
        split_lines(line);
        server_prompt = m.suffix();
        // redo this loop -- there's still a \n in there
        continue;
      }
    }
    // process "line" in received

    rpos = received.find('\n', 0);

    // get line to send to the client
    if (show_client) {
      // that is, if we're sending to the client!
      line = received.substr(0, rpos + 1);
      /*
      std::string clean = clean_string(line);
      BUGZ_LOG(error) << "rpos/show_client:" << clean;
      */
      to_client(line);
    }
    received = received.substr(rpos + 1);

    // process "line" in server_prompt

    line = server_prompt.substr(0, pos + 1);
    server_prompt = server_prompt.substr(pos + 1);

    // Remove \n for dispatching
    std::string part = line.substr(0, pos);

    /*
    if (server_sent != 0) {
      line = line.substr(server_sent);
      server_sent = 0;
    };
    */

    // display on?
    // to_client(line);

    // How should I handle \r in lines?  For now, remove it
    // but LOG that we did.

    replace(part, "\r", "");
    /*
    if (replace(part, "\r", "")) {
      BUGZ_LOG(warning) << "\\r removed from line";
    }
    */

    split_lines(part);
  }

  // Ok, we have sent all of the \n lines.

  if (!received.empty())
    if (show_client) {
      to_client(received);
      // std::string clean = clean_string(received);
      // BUGZ_LOG(error) << "show_client/leftovers:" << clean;
    }

  // This is eating the entire string.  String is partial line
  // portcim line, ending with '\r', this eats the line.
  /*
  // check the server prompt here:
  if ((pos = server_prompt.rfind('\r')) != std::string::npos) {
    // server_prompt contains \r, remove it.
    server_prompt = server_prompt.substr(pos + 1);
  }
  */

  while ((pos = server_prompt.find('\b')) != std::string::npos) {
    // backspace?  OK!  (unless)
    if (pos == 0) {
      // first character, so there's nothing "extra" to erase.
      server_prompt = server_prompt.erase(pos, 1);
    } else
      server_prompt = server_prompt.erase(pos - 1, 2);
  }

  if (!server_prompt.empty()) {
    // We have something remaining -- start the timer!
    set_prompt_timer();
  }
}

void Session::set_prompt_timer(void) {
  prompt_timer_.expires_after(std::chrono::milliseconds(time_ms));
  prompt_timer_.async_wait(boost::bind(&Session::on_prompt_timeout, this,
                                       boost::asio::placeholders::error));
}

void Session::reset_prompt_timer(void) { prompt_timer_.cancel(); }

void Session::on_server_prompt(const std::string &prompt) {
  BUGZ_LOG(warning) << "SP: [" << prompt << "]";
  if (emit_server_prompt) {
    emit_server_prompt(prompt);
  }
}

void Session::on_prompt_timeout(const boost::system::error_code error) {
  if (error != boost::asio::error::operation_aborted) {
    // Ok, VALID timeout
    if (!server_prompt.empty()) {
      // Here's what is happening:
      // SP: [ESC[2JESC[H]
      // which after clean_string is empty.

      std::string clean = clean_string(server_prompt);
      if (!clean.empty()) {
        on_server_prompt(clean);
      }
      // BUGZ_LOG(trace) << "SP: [" << server_prompt << "]";
    }
  }
}

void Session::server_read(void) {
  auto self(shared_from_this());

  boost::asio::async_read(
      server_, boost::asio::buffer(server_buffer, sizeof(server_buffer) - 1),
      boost::asio::transfer_at_least(1),
      [this, self](boost::system::error_code ec, std::size_t length) {
        if (!ec) {
          server_buffer[length] = 0;

          // server_prompt.append(server_buffer, length);
          std::string received(server_buffer, length);
          process_lines(received);

          /*
          I don't believe I need to consume this,
          I'm not async_reading from a stream.
          */

          /*
            if (length) {
              // std::cout << length << std::endl;
              std::cout << "S: " << server_buffer << std::endl;
              do_write(server_buffer);
            }
            */

          server_read();
        } else {
          BUGZ_LOG(warning) << "S: read_failed: socket.shutdown()";
          connected = false;
          socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both);
        }
      });
}

void Session::on_resolve(
    const boost::system::error_code error,
    const boost::asio::ip::tcp::resolver::results_type results) {
  //
  auto self(shared_from_this());

  if (!error) {
    // Take the first endpoint.
    boost::asio::ip::tcp::endpoint const &endpoint = *results;

    server_.async_connect(endpoint,
                          boost::bind(&Session::on_connect, this,
                                      boost::asio::placeholders::error));

  } else {
    // TO DO:
    // BOOST_LOG_NAMED_SCOPE("Session");
    BUGZ_LOG(error) << "Unable to resolve: " << host;
    std::string output =
        str(boost::format("Unable to resolve: %1%\n\r") % host);
    to_client(output);
    BUGZ_LOG(warning) << "socket.shutdown()";
    socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both);
  }
}

void Session::client_input(const std::string &input) {

  BUGZ_LOG(info) << "CI: " << input;

  // Is "proxy" active
  if (active) {
    // do something amazing with the user's input.
  } else {
    if (input == "\x1b" || input == "~") {
      std::string prompt = clean_string(get_prompt());
      BUGZ_LOG(trace) << "CI: ACTIVATE prompt shows: [" << prompt << "]";

      if (prompt == "Selection (? for menu): ") {
        to_client("\n\rThere's not much we can do here.  Activate in-game at a "
                  "Command prompt.\n\r");
        to_client(get_prompt());
        return;
      }

      // easter-eggs:

      if (prompt == "Enter your choice: ") {
        to_client("\n\r\x1b[1;36mI'd choose \x1b[1;37m`T`\x1b[1;36m, but "
                  "that's how I was coded.\n\r");
        to_client(get_prompt());
        return;
      }

      // easter-egg
      if (prompt == "[Pause]") {
        to_client(" \x1b[1;36mMeow\x1b[0m\n\r");
        to_client(get_prompt());
        return;
      }

      //
      // The command prompt that we're looking for:
      //
      // "Command [TL=00:00:00]:[242] (?=Help)? : "
      // the time, and the sector number vary...
      if (prompt.substr(0, 9) == "Command [") {
        int len = prompt.length();
        if (prompt.substr(len - 14) == "] (?=Help)? : ") {
          proxy_activate();
          /*
          to_client("\n\r\x1b[1;34mWELCOME!  This is where the proxy would "
                    "activate.\n\r");
          // active = true;
          // show_client = true;  // because if something comes (unexpected)
          // from the server? talk_direct = false;

          // but we aren't activating (NNY)
          to_client(get_prompt());
          */
          return;
        }
      }

      // eat this input.
      BUGZ_LOG(warning) << "CI: unable to activate, prompt was: [" << prompt
                        << "]";
      return;
    }
  }

  // as the above code matures, talk_direct might get changed.
  // keep this part here (and not above).
  if (talk_direct) {
    to_server(input);
  }

  if (emit_client_input) {
    emit_client_input(input);
  }
}

DispatchSettings Session::save_settings(void) {
  DispatchSettings ss{emit_server_line, emit_server_prompt, emit_client_input,
                      show_client, talk_direct};
  return ss;
}

void Session::restore_settings(const DispatchSettings &ss) {
  emit_server_line = ss.server_line;
  emit_server_prompt = ss.server_prompt;
  emit_client_input = ss.client_input;
  show_client = ss.show_client;
  talk_direct = ss.talk_direct;
}

void Session::proxy_activate(void) {
  active = true;
  start_keepin_alive(); // kickstart the keepalive timer
  main.setNotify([this](void) { this->proxy_deactivate(); });
  main.activate();
}

void Session::proxy_deactivate(void) {
  // Ok, how do we return?
  active = false;
  to_client(get_prompt());
  // to_client(" \b");
}

void Session::client_read(void) {
  auto self(shared_from_this());

  boost::asio::async_read( // why can't I async_read_some here?
      socket_, boost::asio::buffer(read_buffer, sizeof(read_buffer) - 1),
      boost::asio::transfer_at_least(1),
      [this, self](boost::system::error_code ec, std::size_t length) {
        if (!ec) {
          read_buffer[length] = 0;
          if (rlogin_auth.empty()) {
            // first read should be rlogin information
            rlogin_auth.assign(read_buffer, length);

            // parse authentication information
            parse_auth();
            to_client(std::string(1, 0));

            to_client("Welcome, ");
            to_client(rlogin_name);
            to_client("\n\r");

            // Activate the connection to the server

            /* // this fails, and I'm not sure why.  I've used code like this
               before. resolver_.async_resolve( host, port, std::bind(
               &Session::on_resolve, this, _1, _2)); */

            // This example shows using boost::bind, which WORKS.
            // https://stackoverflow.com/questions/6025471/bind-resolve-handler-to-resolver-async-resolve-using-boostasio
            resolver_.async_resolve(
                host, port,
                boost::bind(&Session::on_resolve, this,
                            boost::asio::placeholders::error,
                            boost::asio::placeholders::iterator));

          } else if (length) {
            // Proxy Active?
            // BOOST_LOG_NAMED_SCOPE("Session");

            std::string line(read_buffer, length);
            client_input(line);

            // do_write(output);
          }
          client_read();
        } else {
          BUGZ_LOG(warning) << "CI: read_failed";
          if (connected) {
            BUGZ_LOG(warning) << "Server.shutdown()";
            server_.shutdown(boost::asio::ip::tcp::socket::shutdown_both);
          }
        }
      });
}

void Session::to_client(const std::string &message) {
  auto self(shared_from_this());
  // output the cleaned string (so I can see what we're sending in the
  // logs)

  std::string clean = clean_string(message);
  BUGZ_LOG(trace) << "2C: " << clean;

  boost::asio::async_write(
      socket_, boost::asio::buffer(message),
      [this, self](boost::system::error_code ec, std::size_t /*length*/) {
        if (!ec) {

        } else {
          BUGZ_LOG(warning) << "2C: write failed? closed? Server.shutdown()";
          if (connected) {
            BUGZ_LOG(warning) << "Server.shutdown()";
            server_.shutdown(boost::asio::ip::tcp::socket::shutdown_both);
          }
        }
      });
}

void Session::to_server(const std::string &message) {
  auto self(shared_from_this());
  boost::asio::async_write(
      server_, boost::asio::buffer(message),
      [this, self](boost::system::error_code ec, std::size_t /*length*/) {
        if (!ec) {

        } else {
          BUGZ_LOG(warning) << "S: write failed? closed? socket.shutdown()";
          // we're no longer connected.
          connected = false;
          socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both);
        }
      });

  if (active) {
    start_keepin_alive();
  }
}

void Session::start_keepin_alive(void) {
  // keep alive timer
  keep_alive_.expires_after(std::chrono::seconds(keepalive_secs));
  keep_alive_.async_wait(boost::bind(&Session::stayin_alive, this,
                                     boost::asio::placeholders::error));
}

void Session::stayin_alive(const boost::system::error_code error) {
  if (error != boost::asio::error::operation_aborted) {
    // stayin' alive, stayin' alive...
    if (active) {
      to_server(" ");
      BUGZ_LOG(warning) << "Session::stayin_alive()";
    }
  }
}

Server::Server(boost::asio::io_service &io_service,
               const boost::asio::ip::tcp::endpoint &endpoint,
               const std::string &host, const std::string &port)
    : io_service_{io_service}, acceptor_{io_service_, endpoint},
      signal_{io_service, SIGUSR1, SIGTERM}, host_{host}, port_{port} {
  keep_accepting = true;

  BUGZ_LOG(info) << "Server::Server()";
  signal_.async_wait(boost::bind(&Server::on_signal, this,
                                 boost::asio::placeholders::error,
                                 boost::asio::placeholders::signal_number));
  do_accept();
}

void Server::on_signal(const boost::system::error_code &ec, int signal) {
  BUGZ_LOG(info) << "on_signal() :" << signal;
  keep_accepting = false;
  boost::system::error_code error;
  acceptor_.cancel(error);
  BUGZ_LOG(info) << "cancel: " << error;
  acceptor_.close(error);
  BUGZ_LOG(info) << "close: " << error;
}

Server::~Server() {
  CONFIG = YAML::Node();
  BUGZ_LOG(info) << "Server::~Server()";
}
/**
 * setup async connect accept
 *
 * This creates a session for each connection.  Using make_shared allows the
 * session to automatically clean up when it is no longer active/has anything
 * running in the reactor.
 */
void Server::do_accept(void) {
  acceptor_.async_accept([this](boost::system::error_code ec,
                                boost::asio::ip::tcp::socket socket) {
    if (!ec) {
      BUGZ_LOG(info) << "Server::do_accept()";
      std::make_shared<Session>(std::move(socket), io_service_, host_, port_)
          ->start();
    }

    if (keep_accepting) {
      BUGZ_LOG(info) << "do_accept()";
      do_accept();
    }
  });
}

/**
 * Clean up the trailing ../ in __FILE__
 *
 * This is used by the logging macro.
 *
 * @param filepath
 * @return const char*
 */
const char *trim_path(const char *filepath) {
  if (strncmp(filepath, "../", 3) == 0) {
    filepath += 3;
  }
  return filepath;
}