#include "galaxy.h"

#include <algorithm>  // sort
#include <boost/format.hpp>
#include <chrono>
#include <exception>
#include <fstream>
#include <ostream>
#include <set>
#include <string>

#include "logging.h"
#include "yaml-cpp/yaml.h"

// c++ default exceptions list
// https://en.cppreference.com/w/cpp/error/exception


std::ostream &operator<<(std::ostream &os, const port &p) {
  if (p.type == 0) {
    os << p.sector << ": " << (int)p.type;
  } else {
    os << p.sector << ": " << (int)p.type << " " << text_from_type(p.type)
       << " " << p.amount[0] << "," << p.amount[1] << "," << p.amount[2];
  }
  return os;
}

bool port::unknown(void) {
  for (int x = 0; x < 3; ++x) {
    if (percent[x] != 0) return false;
  }
  return true;
}

trade_type_result trade_type_info(port_type port1, port_type port2) {
  // NONE = 0
  // GOOD = 1 = OE PAIR
  // OK   = 2 = ?? Pair
  // FAIR = 3 = B / S

  buysell p1 = get_buysell(port1);
  buysell p2 = get_buysell(port2);

  buysell inv2 = invert_buysell(p2);
  int matches = 0;  // or pos.size();
  std::vector<int> pos;

  // find which FOE are flipped.  Save index pos.
  for (int x = 0; x < 3; ++x) {
    inv2.foe[x] = (p1.foe[x] == inv2.foe[x]);
    if (inv2.foe[x]) {
      matches++;
      pos.push_back(x);
    }
  }

  if (matches > 1) {
    // O != E for both ports, and O != O
    if ((p1.foe[ORG] != p1.foe[EQU]) && (p2.foe[ORG] != p2.foe[EQU]) &&
        (p1.foe[ORG] != p2.foe[ORG])) {
      return trade_type_result{1, inv2};
    }

    // at least 2 matches.  but are they trade pairs?
    // I can tell by comparing the last two positions in the same port.
    if (p1.foe[pos[matches - 1]] == p1.foe[pos[matches - 2]]) {
      // they are NOT.
      return trade_type_result{3, inv2};
    }
    return trade_type_result{2, inv2};
  }

  if (matches == 1) {
    if (inv2.foe[FUEL]) return trade_type_result{4, inv2};
    return trade_type_result{3, inv2};
  }
  return trade_type_result{0, inv2};
}

int trade_type(port_type port1, port_type port2) {
  trade_type_result r = trade_type_info(port1, port2);
  return r.type;
}

/*
// adding this breaks test-galaxy's port = {2, 2, {1,2,3}, {1,2,3}} code.
port::port() {
  sector = 0;
  type = 0;
  for (int x = 0; x < 3; x++) {
    amount[x] = 0;
    percent[x] = 0;
  }
}
*/

sector_warps::sector_warps() {
  sector = 0;
  // for (int x = 0; x < MAX_WARPS; ++x) warps[x] = 0;
}

void sector_warps::add(sector_type new_sector) {
  warps.insert(new_sector);
  /*
  for (int x = 0; x < MAX_WARPS; ++x) {
    if (warps[x] == new_sector) return;
    if (warps[x] == 0) {
      warps[x] = new_sector;
      return;
    }
  }
  std::string message = str(boost::format("More then MAX %1% sectors for %2%") %
                            MAX_WARPS % (int)sector);
  throw std::out_of_range(message);
  */
}

std::ostream &operator<<(std::ostream &os, const sector_warps &warps) {
  os << "Sector: " << warps.sector << " ";
  bool comma = false;
  for (auto const &warp : warps.warps) {
    if (comma)
      os << ",";
    else
      comma = true;
    os << warp;
  }
  /*
  for (int x = 0; x < MAX_WARPS; ++x) {
    if (warps.warps[x] != 0) {
      if (x != 0) os << ",";
      os << warps.warps[x];
    }
  }
  */
  return os;
}

#define GTEST_COUT std::cerr << "[          ] [ INFO ]"
// #define GTEST_DEBUG

// TODO:  fix this.  I want some trace output, but I don't want
// my logs flooded ...

struct port parse_portcim(const std::string line) {
  struct port p;
  p.sector = std::stoi(line);
  //  20 - 1708  97% -  710  56%    287  15%
  static std::regex portrx(
      "[ ]*([0-9]+) (.)[ ]+([0-9]+)[ ]+([0-9]+%) (.)[ "
      "]+([0-9]+)[ ]+([0-9]+%) (.)[ ]+([0-9]+)[ ]+([0-9]+%)[ ]*",
      std::regex_constants::ECMAScript);

  // does it not understand {3} ??
  // NO, it does not, from regex101.com:
  // A repeated capturing group will only capture the last iteration. Put a
  // capturing group around the repeated group to capture all iterations or use
  // a non-capturing group instead if you're not interested in the data
  //
  //  static std::regex portrx("[ ]*([0-9]+)( (.)[ ]+([0-9]+)[ ]+([0-9]+%)){3}[
  //  ]*",
  //                           std::regex_constants::ECMAScript);

  // sector + amount pct + amount pct + amount pct
  // 1      2 3      4   5 6      7   8 9      10

#ifdef GTEST_DEBUG
  GTEST_COUT << "Sector: " << p.sector << std::endl;
  GTEST_COUT << "Line: [" << line << "]" << std::endl;
#endif

  buysell port_buysell;

  std::smatch matches;
  if (std::regex_match(line, matches, portrx)) {
#ifdef GTEST_DEBUG
    for (size_t x = 1; x < matches.size(); ++x) {
      GTEST_COUT << x << " : " << matches[x] << std::endl;
    }
#endif

    if (matches.size() != 11) {
#ifdef GTEST_DEBUG
      GTEST_COUT << "Now you have 101 problems." << std::endl;
#endif
      p.sector = 0;
      p.type = 0;
      return p;
    }
    // GTEST_COUT << "matches: " << matches.size() << std::endl;
    p.sector = stoi(matches[1]);
    // GTEST_COUT << "sector: " << matches[1] << std::endl;
    // for (int x = 1; x < 11; ++x) {
    //   GTEST_COUT << x << " : " << matches[x] << std::endl;
    // }
    for (int x = 0; x < 3; ++x) {
      int pos = x * 3;
      port_buysell.foe[x] = matches[pos + 2] == "-";
      p.amount[x] = stoi(matches[pos + 3]);
      p.percent[x] = stoi(matches[pos + 4]);
    }
    p.type = type_from_buysell(port_buysell);
#ifdef GTEST_DEBUG
    GTEST_COUT << "port is type " << (int)p.type << std::endl;
#endif
    return p;
  } else {
#ifdef GTEST_DEBUG
    GTEST_COUT << "regex_match failed." << std::endl;
#endif
    p.type = 0;
    p.sector = 0;
    return p;
  }
}

Galaxy::Galaxy() { burnt_percent = 40; }

Galaxy::~Galaxy() { BUGZ_LOG(fatal) << "Galaxy::~Galaxy()"; }

void Galaxy::reset(void) {
  meta = YAML::Node();
  config = YAML::Node();
  ports.clear();
  warps.clear();
}

void Galaxy::add_warp(sector_warps sw) {
  auto pos = warps.find(sw.sector);

  if (pos == warps.end()) {
    // not found
    // sw.sort();
    warps[sw.sector] = sw;
    // BUGZ_LOG(info) << "add_warp NEW " << sw.sector;
  } else {
    // found!
    if (pos->second.warps == sw.warps) {
      // BUGZ_LOG(trace) << "add_warp: Yup, I already know about " << sw.sector;
    } else {
      BUGZ_LOG(info) << "add_warp:  Warps don't match! Updating...";
      BUGZ_LOG(warning) << "Have: " << pos->second;
      BUGZ_LOG(warning) << "Got : " << sw;
      warps[sw.sector] = sw;
    }
  }
}

void Galaxy::add_port(sector_type sector, int port_type) {
  auto pos = ports.find(sector);
  if (pos == ports.end()) {
    // no such port.
    port p;
    p.sector = sector;
    p.type = port_type;
    for (int x = 0; x < 3; x++) {
      p.amount[x] = 0;
      p.percent[x] = 0;
    }
    // BUGZ_LOG(trace) << "add_port: " << sector << ", " << port_type << " : "
    // << p;
    ports[sector] = p;
  } else {
    // port was found, so:
    if (pos->second.type == port_type) {
      // BUGZ_LOG(trace) << "add_port: Yup, port " << sector << " is class " <<
      // port_type;
    } else {
      BUGZ_LOG(fatal) << "add_port: " << sector << " shows " << pos->second.type
                      << " >> set to " << port_type;
      pos->second.type = port_type;
    }
  }
}

void Galaxy::add_port(port p) {
  auto pos = ports.find(p.sector);
  if (pos == ports.end()) {
    // BUGZ_LOG(trace) << "add_port: NEW " << p;
    ports[p.sector] = p;
  } else {
    if (pos->second.type != p.type) {
      if ((pos->second.type == 9) && (p.type == 8)) {
        BUGZ_LOG(trace) << "add_port: StarDock " << p.sector;
        p.type = 9;
        ports[p.sector] = p;
      } else {
        BUGZ_LOG(fatal) << "add_port: " << pos->second << " NEW : " << p;
        ports[p.sector] = p;
      }
    } else {
      if (pos->second.amount != p.amount) {
        // BUGZ_LOG(info) << "add_port: UPDATE " << p.sector;
        pos->second = p;
      } else {
        // BUGZ_LOG(info) << "add_port: Yup " << p.sector;
      }
    }
  }
}

void Galaxy::load(void) {
  std::string filename =
      str(boost::format("galaxy-%1%-%2%.yaml") % game % username);
  // reset ?
  meta = YAML::Node();
  config = YAML::Node();
  ports.clear();
  warps.clear();
  if (file_exists(filename)) {
    YAML::Node data = YAML::LoadFile(filename);
    if (config["meta"]) meta = config["meta"];
    meta["load_from"] = filename;
    std::chrono::_V2::system_clock::time_point now =
        std::chrono::system_clock::now();

    meta["load_time"] = std::chrono::system_clock::to_time_t(now);  // time_t
    if (data["config"]) {
      config = data["config"];
    } else {
      BUGZ_LOG(fatal) << "YAML Missing config section.";
    }
    if (data["ports"]) {
      const YAML::Node ports = data["ports"];
      for (auto const &port_iter : ports) {
        port p;
        p.sector = port_iter.first.as<int>();
        p.type = port_iter.second["class"].as<int>();
        int x;
        if (p.type == 0) {
          // nothing to load for type = 0, set defaults.
          for (x = 0; x < 3; ++x) {
            p.amount[x] = 0;
            p.percent[x] = 0;
          }
        } else {
          x = 0;
          for (auto const &amount : port_iter.second["amount"]) {
            p.amount[x] = amount.as<int>();
            ++x;
          }
          x = 0;
          for (auto const &pct : port_iter.second["pct"]) {
            p.percent[x] = pct.as<int>();
            ++x;
          }
        }
        add_port(p);
      }
    } else {
      BUGZ_LOG(fatal) << "YAML Missing ports section.";
    }
    if (data["warps"]) {
      const YAML::Node &warps = data["warps"];
      // if (warps.IsMap()) {
      for (auto const warp_iter : warps) {
        sector_warps sw;
        sw.sector = warp_iter.first.as<int>();
        for (auto const sector_iter : warp_iter.second) {
          sw.add(sector_iter.as<int>());
        }
        // BUGZ_LOG(trace) << "YAML warp: " << sw;
        add_warp(sw);
      }
      // }
    } else {
      BUGZ_LOG(fatal) << "YAML Missing warps section.";
    }

    BUGZ_LOG(fatal) << "YAML: config keys: " << config.size();
    BUGZ_LOG(fatal) << "YAML:   warp keys: " << warps.size();
    BUGZ_LOG(fatal) << "YAML:   port keys: " << ports.size();

  } else {
    BUGZ_LOG(fatal) << "Missing YAML: " << filename;
  }
}

void Galaxy::save(void) {
  std::string filename =
      str(boost::format("galaxy-%1%-%2%.yaml") % game % username);
  YAML::Node data;
  // add some information to meta before saving.
  meta["save_to"] = filename;
  std::chrono::_V2::system_clock::time_point now =
      std::chrono::system_clock::now();
  meta["save_time"] = std::chrono::system_clock::to_time_t(now);  // time_t

  data["meta"] = meta;
  // data["meta"].SetStyle(YAML::EmitterStyle::Flow);
  BUGZ_LOG(fatal) << "YAML config: " << config.size();
  data["config"] = config;
  data["config"].SetStyle(YAML::EmitterStyle::Flow);

  /*
  for (auto const &config_iter : config) {
    data["config"][config_iter.first] = config_iter.second;
  }
  */
  BUGZ_LOG(fatal) << "YAML warps: " << warps.size();
  for (auto const &warp : warps) {
    for (auto const &sector : warp.second.warps) {
      data["warps"][warp.first].push_back(sector);
    }
    data["warps"][warp.first].SetStyle(YAML::EmitterStyle::Flow);
    /*
    for (int x = 0; x < MAX_WARPS; ++x) {
      if (warp.second.warps[x] == 0) break;
      data["warps"][warp.first].push_back(warp.second.warps[x]);
    }
    */
  }
  BUGZ_LOG(fatal) << "YAML ports: " << ports.size();
  /*
  When saving to yaml, my sector_type is like char.  So, it wants
  to save the values as a character.  Cast to int.
   */
  for (auto const &port : ports) {
    data["ports"][port.second.sector]["class"] = (int)port.second.type;
    if (port.second.type == 0) {
      data["ports"][port.second.sector].SetStyle(YAML::EmitterStyle::Flow);
    } else {
      // nothing to save for type = 0
      for (int x = 0; x < 3; x++) {
        data["ports"][port.second.sector]["amount"].push_back(
            (int)port.second.amount[x]);
        data["ports"][port.second.sector]["pct"].push_back(
            (int)port.second.percent[x]);
      }
      data["ports"][port.second.sector]["amount"].SetStyle(
          YAML::EmitterStyle::Flow);
      data["ports"][port.second.sector]["pct"].SetStyle(
          YAML::EmitterStyle::Flow);
      data["ports"][port.second.sector].SetStyle(YAML::EmitterStyle::Flow);
    }
  }

  std::ofstream fout(filename);
  fout << data << std::endl;
  BUGZ_LOG(fatal) << "YAML: " << filename;
}

std::vector<port_pair_type> Galaxy::find_trades(sector_type sector,
                                                bool highest) {
  std::vector<port_pair_type> pptv;

  // Does this sector have a port?
  auto port = ports.find(sector);
  if (port == ports.end()) return pptv;

  auto port_warps = warps.find(sector);
  // warps not found for port sector?
  if (port_warps == warps.end()) return pptv;

  for (auto const &s : port_warps->second.warps) {
    // only count the ports > our sector number -- so we don't have dups
    if (highest && (s < sector)) continue;

    // verify we have a way back
    {
      auto warpback = warps.find(s);
      // can we find that warp?
      if (warpback == warps.end()) continue;
      // does it link back to the port's sector?
      if (warpback->second.warps.find(sector) == warpback->second.warps.end())
        continue;
    }

    // Does this sector have a port?
    auto possible_port = ports.find(s);
    if (possible_port == ports.end()) continue;

    // calculate trade type
    // int t = trade_type(port->second.type, possible_port->second.type);
    trade_type_result ttr =
        trade_type_info(port->second.type, possible_port->second.type);
    if ((ttr.type == 0) || (ttr.type == 4)) continue;

    bool burnt = false;
    for (int x = 0; x < 3; ++x) {
      if (ttr.trades.foe[x]) {
        // if port isn't unknown, check to see if it's burnt out.
        if (!possible_port->second.unknown())
          if (possible_port->second.percent[x] < burnt_percent) burnt = true;
        if (!port->second.unknown())
          if (port->second.percent[x] < burnt_percent) burnt = true;
      }
    }
    if (burnt) continue;

    pptv.push_back(port_pair_type{ttr.type, sector, s});
    BUGZ_LOG(trace) << "sector: " << sector << " and " << s
                    << " tt:" << ttr.type;
  }
  return pptv;
}

bool compare_port_pair(const port_pair_type &ppt1, const port_pair_type &ppt2) {
  if (ppt1.type == ppt2.type) {
    return ppt1.s1 < ppt2.s1;
  } else {
    return (ppt1.type < ppt2.type);
  }
}

void Galaxy::sort_port_pair_type(std::vector<port_pair_type> &pptv) {
  sort(pptv.begin(), pptv.end(), compare_port_pair);
}

std::vector<port_pair_type> Galaxy::find_best_trades(void) {
  std::vector<port_pair_type> pptv;

  burnt_percent = config["burnt_percent"].as<int>();
  if (burnt_percent > 90) {
    burnt_percent = 90;
    config["burnt_percent"] = 90;
  }
  if (burnt_percent < 10) {
    burnt_percent = 10;
    config["burnt_percent"] = 10;
  }

  for (auto const &pi : ports) {
    if (pi.second.type == 0) continue;
    sector_type sector = pi.second.sector;
    auto port_warps = warps.find(sector);
    if (port_warps == warps.end()) continue;

    std::vector<port_pair_type> ppt_sector = find_trades(sector, true);
    if (ppt_sector.empty()) continue;
    pptv.insert(pptv.end(), ppt_sector.begin(), ppt_sector.end());
  }
  return pptv;
}