//
// Created by rhetenor on 5/21/19.
//

#include "FlippRServer.h"
#include "output/OutputRequestHandlerFactory.h"
#include "input/InputSocketHandlerFactory.h"
#include "DriverFactory.h"
#include "utility/Colors.h"
#include "utility/LoggerFactory.h"

#include <Poco/Net/SocketAddress.h>
#include <Poco/Net/ServerSocket.h>
#include <Poco/Net/HTTPServer.h>
#include <Poco/Util/HelpFormatter.h>
#include <Poco/JSON/Parser.h>

#include <iostream>
#include <fstream>
#include <algorithm>
#include <numeric>
#include <boost/filesystem.hpp>

int main(int argc, char** argv)
{
  std::cout << FGRN("Starting FlippR-Server ... ") << std::endl;

  flippR_driver::networking::FlippRServer app;
  return app.run(argc, argv);
}

namespace flippR_driver
{
namespace networking
{
using namespace Poco::Net;
using namespace Poco::Util;
using namespace Poco::JSON;
using namespace Poco;

namespace fs = boost::filesystem;

FlippRServer::FlippRServer() :
  help_requested(false),
  input_port(9980),
  output_port(9981)
{}

void FlippRServer::parse_server_config_file()
{
  std::ifstream config;

  Parser parser;
  Object::Ptr json;

  try
  {
    config.open(this->server_config);
  }
  catch(const std::exception e)
  {
    logger().error(FCYN("server_config.json not specified!"));
    exit(Application::EXIT_USAGE);
  }

  try
  {
    auto parse = parser.parse(config);
    json = parse.extract<Object::Ptr>();
  }
  catch(const std::exception e)
  {
    logger().error(e.what());
    exit(Application::EXIT_IOERR);
  }

  logger().information(FCYN("Parsing server_config.json..."));

  Object::NameList keys = json->getNames();
  std::sort(keys.begin(), keys.end());

  if(!std::includes(keys.begin(), keys.end(), REQUIRED_CONFIG_KEYS.begin(), REQUIRED_CONFIG_KEYS.end()))
  {
    std::string config_keys;
    config_keys = std::accumulate(REQUIRED_CONFIG_KEYS.begin(), REQUIRED_CONFIG_KEYS.end(), config_keys);
    logger().error("Need all of the following keys to be specified in server_config json" + config_keys);
    Application::EXIT_USAGE;
  }

  this->configs = *json;

  config.close();
}

void FlippRServer::uninitialize()
{
  this->output_driver->shut_down_driver();
  this->output_server->stop();

  ServerApplication::uninitialize();

  Poco::File socket_file(this->output_socket_file_path);
  if (socket_file.exists())
    socket_file.remove();

  Poco::File input_socket_file(this->input_socket_file_path);
  if (input_socket_file.exists())
    input_socket_file.remove();
}

/**
 * Initially called before main.
 */
void FlippRServer::initialize(Application &self)
{
  this->parse_server_config_file();
  //Todo May restructure with subsystems
  //make this one application and subsystems ServerApplications

  this->initialize_output_driver();
  this->initialize_input_driver();

  this->output_server = std::unique_ptr<Poco::Net::HTTPServer>(this->build_output_server());
  this->output_server->start();

  this->input_server = std::unique_ptr<TCPServer>(this->build_input_server());
  this->input_server->start();
  //logger().warning(FRED("Input server not started!"));

  ServerApplication::initialize(self);
}

void FlippRServer::initialize_output_driver()
{
  std::string lamp_config_path;
  std::string solenoid_config_path;
  std::string sound_config_path;
  std::string display_config_path;

  try
  {
    lamp_config_path = this->configs["lamp-config"].toString();
    solenoid_config_path = this->configs["solenoid-config"].toString();
    sound_config_path = this->configs["sound-config"].toString();
    display_config_path = this->configs["display-config"].toString();
  }
  catch(const std::exception& e)
  {
    logger().error(e.what());
    exit(EXIT_FAILURE);
  }

  this->output_driver = flippR_driver::get_OutputDriver(solenoid_config_path,
                  lamp_config_path,
                  sound_config_path,
                  display_config_path);
}

void FlippRServer::initialize_input_driver()
{
  std::ifstream input_config_stream;
  std::ifstream matrix_config_stream;

  try
  {
    input_config_stream.open(this->configs["input-config"].toString());
    matrix_config_stream.open(this->configs["matrix-config"].toString());
  }
  catch(const std::exception& e)
  {
    logger().error(e.what());
    exit(EXIT_FAILURE);
  }

  this->input_driver = flippR_driver::get_InputDriver(input_config_stream, matrix_config_stream);
}

int FlippRServer::main(const std::vector<std::string>& args)
{
  if(!help_requested)
  {
    logger().information(FGRN("Server running!"));
    waitForTerminationRequest();
  }
  return Application::EXIT_OK;

}

HTTPServer* FlippRServer::build_output_server()
{
  unsigned short port = (unsigned short) config().getInt("FlippRServer.port", this->output_port);

  this->output_socket_file_path = this->get_runtime_dir() + OUTPUT_SOCKET_NAME;
  Poco::File socket_file(output_socket_file_path);
  if (socket_file.exists())
    socket_file.remove();

  SocketAddress address(SocketAddress::UNIX_LOCAL, socket_file.path());
  ServerSocket server_socket(address);

  return new HTTPServer(new OutputRequestHandlerFactory(this->output_driver), server_socket, new HTTPServerParams);
}

TCPServer* FlippRServer::build_input_server()
{
  unsigned short port = (unsigned short) config().getInt("FlippRServer.port", this->input_port);

  this->input_socket_file_path = this->get_runtime_dir() + INPUT_SOCKET_NAME;
  Poco::File socket_file(input_socket_file_path);
  if (socket_file.exists())
  {
    socket_file.remove();
    logger().information(FRED("Seems that server is already running. Make sure that only one instance of server is running."));
  }


  SocketAddress address(SocketAddress::UNIX_LOCAL, socket_file.path());
  ServerSocket server_socket(address);

  return new TCPServer(new input::InputSocketHandlerFactory(this->input_driver), server_socket);
}

void FlippRServer::defineOptions(OptionSet& options)
{
  ServerApplication::defineOptions(options);

  options.addOption(
    Option("help", "h", "display this help")
      .required(false)
      .repeatable(false)
      .callback(OptionCallback<FlippRServer>(
        this, &FlippRServer::handle_help)));

  options.addOption(Option("input-port", "i", "Define the port for the TCP-Input-Server, which represents the flipper inputs. Default 9980")
    .required(false)
    .repeatable(false)
    .callback(OptionCallback<FlippRServer>(this, &FlippRServer::handle_config_file))
    .argument("input-port", true));

  options.addOption(Option("output-port", "o", "Define the port for the HTTP-Output-Server, which represents the flipper outputs. Default 9981")
    .required(false)
    .repeatable(false)
    .callback(OptionCallback<FlippRServer>(this, &FlippRServer::handle_config_file))
    .argument("output-port", true));

  options.addOption(Option("server-config", "s", "Specify where the server-config file with paths to the other configs is located. Only needed when not in this folder.")
    .required(true)
    .repeatable(false)
    .callback(OptionCallback<FlippRServer>(this, &FlippRServer::handle_config_file))
    .argument("server-config", true));

  options.addOption(Option("debug", "d", "Switch debug messages on.")
    .required(false)
    .repeatable(true)
    .callback(OptionCallback<FlippRServer>(this, &FlippRServer::create_debug_logger)));
}

void FlippRServer::handle_config_file(const std::string &name, const std::string &value)
{
  if(name == "input-port")
    this->input_port = std::stoi(value);
  else if(name == "output-port")
    this->output_port = std::stoi(value);
  else if(name == "server-config")
    this->server_config = value;
  else
  {
    logger().information("Configuration \"" + name + "\" is not known.");
    return;
  }
  logger().information(KMAG "Set " + name + " to " + value + RST);
}

void FlippRServer::handle_help(const std::string& name, const std::string& value)
{
  Poco::Util::HelpFormatter helpFormatter(options());
  helpFormatter.setCommand(commandName());
  helpFormatter.setUsage("OPTIONS");
  helpFormatter.setHeader(
    "The FlippR-Server, one must specify a json with all needed config files.");
  helpFormatter.format(std::cout);
  stopOptionsProcessing();
  help_requested = true;
}

std::string FlippRServer::get_runtime_dir()
{
  return std::getenv("XDG_RUNTIME_DIR") ? std::string(std::getenv("XDG_RUNTIME_DIR")) + "/" : DEFAULT_RUNTIME_DIR;
}

void FlippRServer::create_debug_logger(const std::string &name, const std::string &value)
{
  utility::LoggerFactory::ActivateDebugLog();
  logger().information(FCYN("Activated debug logging."));
}

}
}
