#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 <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) {
  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)
    : socket_(std::move(socket)), io_service_{io_service},
      resolver_{io_service}, server_{io_service}, timer_{io_service},
      keep_alive_{io_service}, host{hostname}, port{port} {
  // server_sent = 0;
  time_ms = stoi(from_config("prompt_timeout", "50"));
  keepalive_secs = stoi(from_config("keepalive", "45"));

  // director.push(new CoreDispatch((*this)));
}

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::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");

  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);
  }
}

void Session::dispatch_line(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);
  // NOTE:  We get "TradeWars Game Server\n"  (Missing \r)
  // We add the \r with our injection line.

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

  // additional analysis needed here
  // state :  where are we, what is this line?
  // collect all data we can from the server.  (P1)

  if (temp.find("Selection (? for menu): ") != std::string::npos) {
    char ch = temp[temp.length() - 1];
    if (ch >= 'A' && ch < 'Q') {
      BUGZ_LOG(warning) << "GAME " << ch << " activated!";
    }
  }

  // "Selection (? for menu): ?"
  // This gives us the current game that we're in.
  // (excluding ?, #, ! and Q)

  BUGZ_LOG(info) << "SL: " << 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();
        dispatch_line(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";
    }
    */

    dispatch_line(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_timer();
  }
}

void Session::set_timer(void) {
  timer_.expires_after(std::chrono::milliseconds(time_ms));
  timer_.async_wait(
      boost::bind(&Session::on_timer, this, boost::asio::placeholders::error));
}

void Session::reset_timer(void) { timer_.cancel(); }

void Session::on_timer(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()) {
        BUGZ_LOG(warning) << "SP: [" << clean << "]";
        // emit
      }
      // 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()";
          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) {
  if (active) {
    // do something amazing with the user's input.
  } else {
    if (input == "\x1b" || input == "~") {
      std::string prompt = clean_string(get_prompt());

      BUGZ_LOG(debug) << "CI: ACTIVATE prompt shows: [" << prompt << "]";
    }
  }
  if (talk_direct)
    to_server(input);
  BUGZ_LOG(info) << "CI: " << input;
}

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()";
          socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both);
        }
      });

  // 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...
    to_server(" ");
    BUGZ_LOG(warning) << "Session::stayin_alive()";
  }
}

Server::Server(boost::asio::io_service &io_service,
               const boost::asio::ip::tcp::endpoint &endpoint, std::string host,
               std::string port)
    : io_service_{io_service}, acceptor_{io_service_, endpoint}, host_{host},
      port_{port} {
  only_one = from_config("one_connection", "0") == "1";
  do_accept();
}

/**
 * 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 (!only_one)
      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;
}