#include "charman.h"
#include "images.h"
#include "lastseen.h"
#include "logs_utils.h"
#include "render.h"
#include "terminal.h"
#include "utils.h"
#include "zf_log.h"
#include <ctype.h>
#include <iomanip>
#include <regex>
#include <sstream>
#include <string.h>
#include <string>
#include <unistd.h> // write
#include <vector>

extern struct console_details console;
extern std::string username;
extern std::string fullname;

#define BSIZE 512

void a_or_an(std::string &word) {
  /* If it starts with a vowel "an" */
  char ch = toupper(word[0]);
  if ((ch == 'A') || (ch == 'E') || (ch == 'I') || (ch == 'O') || (ch == 'U')) {
    word.insert(0, " an ");
  } else {
    word.insert(0, " a ");
  }
}

/*
 * harry_idle_event(fd)
 *
 * User is idle, let's let them know we're here.  HAHAHA!
 *
 */
void harry_idle_event(int fd) {
  // Make something happen
  std::ostringstream buffer;
  int r;
  int level = harry_level();
  if (!level)
    return;

  // If the username is something, we have their info!
  bool have_userinfo = !username.empty();
  int xpos = console.posx;

  // This is no where near finished, BUT!
  // Do not put any ^ codes in these -- the strlen() would be wrong.
  const char *phrases[] = {
      "Hahaha",
      "Snicker, snicker",
      "Boo!",
      "Arrooo!",
      "Ahh-wooo!",
      "Aaaooo!",
      "Sorry, I forgot!",
      "The Matrix has you...",
      "Follow the white rabbit.",
  };
  const int total_phrases = sizeof(phrases) / sizeof(char *);

  const char *user_phrases[] = {
      "Is USER really here?",
      "Knock, Knock NICK.",
      "What is a NICK?",
      "Wake up, NICK...",
      "Here lies USER, rest in peace.",
      "Don't be scared, NICK.",
  };
  const int total_user_phrases = sizeof(user_phrases) / sizeof(char *);

  // I'd like to use total_possible, but ... it isn't possible. (NNY)
  static LastSeen last_seen_harry_event(LastSeen::best_guess(total_phrases));

  int total_possible =
      have_userinfo ? total_phrases + total_user_phrases : total_phrases;

  do {
    r = randint(total_possible);
  } while (last_seen_harry_event.seen_before(r));

  ZF_LOGD("have %d picked %d total %d console @ %d,%d", have_userinfo, r,
          total_possible, console.posx, console.posy);

  std::string selected;
  if (r >= total_phrases) {
    selected = user_phrases[r - total_phrases];
    std::string temp = username;
    a_or_an(temp);
    replace(selected, " a NICK", temp);
    replace(selected, "USER", fullname);
    replace(selected, "NICK", username);
  } else
    selected = phrases[r];

  ZF_LOGD("Selected: %s", selected.c_str());
  if (selected.size() + xpos > 78) {
    ZF_LOGD("Sorry, too long (%d)", (int)selected.size() + xpos);
  } else {
    int color = randint(15) + 1;
    int pause = 2;
    if (selected.size() > 20) {
      pause = 5;
    }

    // %02d = std::setfill('0') << std::setw(2) << (int)
    /*
    buffer << "^CS^S2^C" << std::setfill('0') << std::setw(2) << color
           << phrases[r] << "^P2^CR^D" << std::setw(2) << strlen(phrases[r]);
    */
    buffer << TRIGGER "CS" TRIGGER "S2" TRIGGER "C" << std::setfill('0')
           << std::setw(2) << color
           << selected
           // Reset SPEED and RENDER
           << TRIGGER "S0" TRIGGER "R0" TRIGGER "P" << pause
           << TRIGGER "CR" TRIGGER "D" << std::setw(2) << selected.size();

    std::string str = buffer.str();
    ZF_LOGD("harry_event: render(%d, \"%s\")", fd, str.c_str());
    render(fd, str);
  }
}

void init_harry() {
  // init_have_seen(last_seen_harry_event, MAX_HARRY_EVENT_DUPS);
  // ZF_LOGD("init => %d %d", last_seen_harry_event[0],
  // last_seen_harry_event[1]);
  console_init(&console);
  reset_render();
}

// char words[] = "[a-zA-Z]+( [a-zA-Z]+)+";

int mangle_clrscr(std::string &buffer, std::string &work, size_t pos) {
  static int ANSI_CLS_count = 0;
  ZF_LOGI("seen: ANSI_CLS");
  ANSI_CLS_count++;

  int level = harry_level();
  if (!level)
    return 0;

  // Don't activate on the first ANSI_CLS (that's the BBS Version display/login)

  if (ANSI_CLS_count > 1) {

    // if (random_activate((level + 1) / 2)) {
    if (random_activate(level * 5)) {
      std::ostringstream display;
      int needs_cls = 0;

      struct image {
        const char **lines;
        int size;
        int cls;
        int width; // height = size
      } images[] = {{ghost, sizeof(ghost) / sizeof(char *), 1, 0},
                    {ghead, sizeof(ghead) / sizeof(char *), 1, 0},
                    {wolf, sizeof(wolf) / sizeof(char *), 1, 0},
                    {panther, sizeof(panther) / sizeof(char *), 1, 0},
                    {bat, sizeof(bat) / sizeof(char *), 1, 0},
                    {icu, sizeof(icu) / sizeof(char *), 0, 20},
                    {skull, sizeof(skull) / sizeof(char *), 0, 19},
                    {skullblink, sizeof(skullblink) / sizeof(char *), 0, 19},
                    // {specter, sizeof(specter) / sizeof(char *), 1, 0},
                    {owl, sizeof(owl) / sizeof(char *), 0, 70}

      };
      const int total_images = sizeof(images) / sizeof(image);
      static LastSeen last_files(LastSeen::best_guess(total_images));
      int r;

      do {
        r = randint(total_images);
      } while (last_files.seen_before(r));

      std::string fgoto;

      if (!images[r].cls) {
        int x = 0, y = 0;

        x = randint(79 - images[r].width) + 1;
        y = randint(24 - images[r].size) + 1;

        display << TRIGGER "CS" TRIGGER "f" << std::setfill('0') << std::setw(2)
                << x << std::setw(2) << y << "\x1b[1;1H";
        fgoto = display.str();
        // reset display
        display.str(std::string());
        display.clear();
      } else {
        if (images[r].lines == specter) {
          // specter!
          fgoto.assign(TRIGGER "S1" TRIGGER "CS" TRIGGER "F");
        } else
          fgoto.assign(TRIGGER "CS" TRIGGER "F");
      }

      needs_cls = images[r].cls;

      // I get what's happening.  Mystic moves cursor to home, CLS, cursor
      // home. When we get here, we're ALWAYS at the top of the screen...
      // Hence our bat isn't displayed at the end of the screen.

      // This is before the actual CLS, so we CLS before displaying our files.
      // I tried a ^P2 before doing this .. but I'd rather have the picture up
      // right away I think.

      // Ok, yes, there's no filename being sent.  :P
      render_image(images[r].lines, images[r].size);

      display << (needs_cls ? "\x1b[2J" : "") << fgoto
              << TRIGGER "S0" TRIGGER "CR" TRIGGER "P3";

      std::string display_output = display.str();

      ZF_LOGI("mangle(ANSI_CLS): %d file inserted %s", r,
              repr(display_output.c_str()));

      // Move the buffer so there's room for the display string.
      buffer.insert(pos, display_output);
      work.insert(pos, std::string(display_output.size(), ' '));
      return 1; // need_render = 1;
    } else {
      // if (random_activate((level + 1) / 2)) {
      if (random_activate(level * 5)) {
        int r;
        std::ostringstream display;
        // If the username is something, we have their info!
        bool have_userinfo = !username.empty();

        const char *phrasing[] = {
            TRIGGER "R1Haha" TRIGGER "P1ha" TRIGGER "P1ha", "Poof!",
            "Anyone there?", TRIGGER "R1Knock, " TRIGGER "P1Knock",
            /*
            This picks random color and position -- then
            homes cursor and changes to another color.  (This can be seen.)
             */
            TRIGGER "G0101" TRIGGER "C07" TRIGGER "S0"
                    "Segmentation fault" TRIGGER "P1"
                    " (core dumped)" TRIGGER "P2",
            TRIGGER
            "G0101" TRIGGER "C07" TRIGGER "S0"
            "/usr/include/c++/7/bits/basic_string.h:1057: "
            "std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::reference "
            "std::__cxx11::basic_string<_CharT, _Traits, "
            "_Alloc>::operator[](std::__cxx11::basic_string<_CharT, _Traits, "
            "_Alloc>::size_type) [with _CharT = char; _Traits = "
            "std::char_traits<char>; _Alloc = std::allocator<char>; "
            "std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::reference = "
            "char&; std::__cxx11::basic_string<_CharT, _Traits, "
            "_Alloc>::size_type = long unsigned int]: Assertion '__pos <= "
            "size()' failed." TRIGGER "P1"
            "\r\nAborted (core dumped)" TRIGGER "P2" TRIGGER "S0",
            TRIGGER "G0101" TRIGGER "C07" TRIGGER "S0"
                    "/usr/include/c++/7/debug/vector:417:\r\n"
                    "Error: attempt to subscript container with out-of-bounds "
                    "index 13, but\r\n"
                    "container only holds 0 elements.\r\n"
                    "\r\n"
                    "Objects involved in the operation:\r\n"
                    "    sequence \"this\" @ 0x0x7fff43fa94a0 {\r\n"
                    "      type = "
                    "std::__debug::vector<std::__cxx11::basic_string<char, "
                    "std::char_traits<char>, std::allocator<char> >, "
                    "std::allocator<std::__cxx11::basic_string<char, "
                    "std::char_traits<char>, std::allocator<char> > > >;\r\n"
                    "    }\r\n"
                    "Aborted (core dumped)" TRIGGER "P2" TRIGGER "S0"

        };

        // the Vector error could use some randomness in the element accessed,
        // as well as how many elements and the memory location.

        const int max_phrases = sizeof(phrasing) / sizeof(char *);

        const char *user_phrasing[] = {
            "We've got you now, NICK!",
            "Are you there, USER?",
            TRIGGER "R1Knock, " TRIGGER "P1Knock " TRIGGER "P1NICK",
            "I see you," TRIGGER "P1" TRIGGER "S1" TRIGGER "R1 NICK" TRIGGER
            "S0" TRIGGER "R0",
            "I see NICK hiding."
        };

        const int max_user_phrases = sizeof(user_phrasing) / sizeof(char *);

        static LastSeen last_phrasing(LastSeen::best_guess(max_phrases));

        int total_possible =
            have_userinfo ? max_phrases + max_user_phrases : max_phrases;

        ZF_LOGI("mangle(ANSI_CLS)");

        do {
          r = randint(total_possible);
        } while (last_phrasing.seen_before(r));

        std::string selected;
        if (r >= max_phrases) {
          selected = user_phrasing[r - max_phrases];
          std::string temp = username;
          a_or_an(temp);
          replace(selected, " a NICK", temp);
          replace(selected, "USER", fullname);
          replace(selected, "NICK", username);
        } else
          selected = phrasing[r];

        int color = randint(14) + 1;
        int x = randint(30) + 1;
        int y = randint(15) + 1;

        /*
        Don't have it pause there before moving the cursor.

        Move the cursor, get the color changed, THEN pause.
        Then act all crazy.

        NOTE:  Make sure if you use any ^R Render effects, turn them off
        before trying to display the restore_color.  :P   ^R0 Also, make
        sure you re-home the cursor ^G0101 because that's where they are
        expecting the cursor to be!  (At least it's how Mystic does it.)

        HOME, CLS, HOME, ...  Not sure what others do there.  We'll see.
        */

        if (strncmp(selected.c_str(), TRIGGER "G", 2) == 0) {
          display << TRIGGER "CS" TRIGGER "S3" TRIGGER "P1" << selected
                  << TRIGGER "S0" TRIGGER "R0" TRIGGER "CR" TRIGGER "P1" TRIGGER
                             "G0101";
          // This starts with a GOTO, so don't use our random position
        } else {
          display << TRIGGER "CS" TRIGGER "G" << std::setw(2)
                  << std::setfill('0') << x << std::setw(2) << y
                  << TRIGGER "S3" TRIGGER "C" << std::setw(2) << color
                  << TRIGGER "P1" << selected
                  << TRIGGER "S0" TRIGGER "R0" TRIGGER "CR" TRIGGER "P1" TRIGGER
                             "G0101";
        };

        std::string display_output = display.str();

        // Added debug statement so we can identify what was sent... color,
        // number picked and what that is
        ZF_LOGD("mangle(ANSI_CLS): Inserted color=%02d r=%d phrase='%s'", color,
                r, phrasing[r]);
        ZF_LOGI("mangle(ANSI_CLS): %d %s", r, repr(display_output.c_str()));

        // Move the buffer so there's room for the display string.
        buffer.insert(pos, display_output);
        work.insert(pos, std::string(display_output.size(), ' '));
        return 1; // need_render = 1;
      }
    }
  }
  return 0;
}

int mangle(int fd, std::string &buffer) {
  // a simple default for now.
  // ZF_LOGV("mangle [%s]", logrepr(buffer.c_str()));
  ZF_LOGV_LR("mangle:", buffer);
  /*
  ZF_LOGV_MEM(buffer.data(), buffer.size(), "mangle(%d): %lu bytes", fd,
              buffer.size());
  */

  int need_render = 0;
  static std::string work;
  static size_t work_size = 0;

  work.assign(buffer);

  // This should allow us to monitor any memory allocations
  if (work.capacity() != work_size) {
    ZF_LOGD("work cap %lu -> %lu", work_size, work.capacity());
    work_size = work.capacity();
  }

  int level = harry_level();
  std::ostringstream new_buffer;
  std::smatch match;

  if (level) {
    // Strings are good, but Regex is better
    // Mystic BBS v1.12 A43 for Linux Node 1
    // Mystic BBS Version 1.12 A45
    static int bbs_match = 0;

    if (!bbs_match) {
      std::regex bbs_what(
          "(?:Mystic BBS Version [0-9.]+ A[0-9]+)|(?:Mystic BBS "
          "v[0-9.]+ A[0-9]+ for Linux Node [0-9]+)");
      // std::regex bbs_what("Mystic BBS Version [0-9.]+ A[0-9]+");
      // std::regex_constants::ECMAScript);

      // Mystic BBS v[0-9.]+ A[0-9]+ for Linux Node [0-9]+

      if (std::regex_search(buffer, match, bbs_what)) {
        // We have a match
        ZF_LOGD("bbs_seen");
        bbs_match = 1;
        std::string old_string =
            buffer.substr(match.position(0), match.length(0));

        // Build a new and better string
        std::string new_string;
        const char *bbs_systems[] = {
            "Haunted BBS",   "Harry's BBS", "Scary BBS Software",
            "Screaming BBS", "Fright BBS",  "Gravestone BBS",
        };
        const char *operating_systems[] = {
            "OS/360",   "CP/M",       "OS/9",
            "Xenix",    "MS-DOS",     "PC-DOS",
            "DR-DOS",   "QNX",        "Novell Netware",
            "AmigaOS",  "Windows NT", "Windows CE",
            "AIX",      "OS/2",       "OS/400",
            "NeXTSTEP", "MINIX",      "Solaris",
            "Plan 9",   "FreeBSD",    "Windows 95",
            "Palm OS",  "Mac OS X",   "Windows XP",
            "DESQview", "EMACS",
            // EMACS *should* be an OS!
        };

        int r = randint(sizeof(bbs_systems) / sizeof(char *));
        new_buffer << bbs_systems[r] << " v" << randint(10) << "."
                   << randint(80);
        new_buffer << " for ";

        r = randint(sizeof(operating_systems) / sizeof(char *));
        new_buffer << operating_systems[r] << " Node " << randint(150 * level);
        new_string = new_buffer.str();
        // reset buffer
        new_buffer.str(std::string());
        new_buffer.clear();

        replace(buffer, old_string, new_string);
        replace(work, old_string, new_string);
        level = 0; // temp turn off the manglers!  ;)
      }
    }

    static int author_match = 0;

    if (!author_match) {
      static std::regex author("Copyright \\(C\\) [0-9-]+ By James Coyle");

      if (std::regex_search(buffer, match, author)) {
        // We have a match
        ZF_LOGD("author seen");
        author_match = 1;
        std::string old_author =
            buffer.substr(match.position(0), match.length(0));

        // Build a new and better string
        const char *coder_names[] = {"Horrible Harry", "Ghost Writer",
                                     "Sands of Time", "Spector Software",
                                     "Creepy Coder"};
        if (randint(10) < 5)
          new_buffer << "Copyfright ";
        else
          new_buffer << "Copyright ";

        new_buffer << "(C) " << 1000 + randint(999) << "-" << randint(24000)
                   << " By ";
        int r = randint(sizeof(coder_names) / sizeof(char *));
        new_buffer << coder_names[r];
        std::string new_author = new_buffer.str();

        replace(buffer, old_author, new_author);
        replace(work, old_author, new_author);
        level = 0;
      }
    }
  }

  const char *ANSI_CLS = "\x1b[2J";
  size_t pos = buffer.find(ANSI_CLS);

  if (pos != std::string::npos) {
    if (level)
      if (mangle_clrscr(buffer, work, pos)) {
        need_render = 1;
      }
  }

  static std::string text;
  static std::vector<int> text_offsets;
  size_t stri;

  text.clear();
  text_offsets.clear();

  // Don't use console, this for just spliting text and ansi into different
  // buffers.
  console_details chew = console;

  for (stri = 0; stri < buffer.size(); ++stri) {
    termchar tc = console_char(&chew, work[stri]);

    if (tc.in_ansi) {
      if (tc.ansi != START) {
        // Ok, this is something.  What is it?
        // ZF_LOGV("ANSI type %d at %lu", tc.ansi, stri);
        switch (tc.ansi) {
        case CURSOR:
        case CLEAR:
        case OTHER:
          text.append(1, '.');
          text_offsets.push_back(-1);
          break;
        case START:
        case COLOR:
          // text.append(1, ' ');
          // text_offsets.push_back(-1);
          // color changes show as nothing in the text string.
          break;
        }
      }
    } else {
      // These should never get out of sync ...
      if (text.size() != text_offsets.size()) {
        ZF_LOGE("Error: text != text_offsets %lu != %lu", text.size(),
                text_offsets.size());
      }
      text.append(1, work[stri]);
      text_offsets.push_back(stri);
    }
  }

  // Control buffer debugging output.
#ifndef NO_BUFFER_DEBUG
  // ZF_LOGV_LR("mangle:", buffer);
  ZF_LOGV_LR("Buffer:", buffer);
  ZF_LOGV_LR("Work:", work);
  ZF_LOGV_LR("Text:", text);
  // ZF_LOGV("Buffer: %s", logrepr(buffer.c_str()));
  // ZF_LOGV("Work: %s", logrepr(work.c_str()));
  // ZF_LOGV("Text: %s", logrepr(text.c_str()));

  // ZF_LOGV_MEM(buffer.data(), buffer.size(), "Buffer:");
  // ZF_LOGV_MEM(work.data(), work.size(), "Work:");
  // ZF_LOGV_MEM(text.data(), text.size(), "Text Buffer:");

  // Output vector contents
  std::ostringstream oss;
  int comma = 0;
  for (auto it = std::begin(text_offsets); it != std::end(text_offsets); ++it) {
    if (comma) {
      oss << ", ";
    };
    comma++;
    oss << *it;
    if (comma == 30) {
      std::string temp_output = oss.str();
      ZF_LOGV("Vector: %s", temp_output.c_str());
      // reset ostringstream
      oss.str(std::string());
      oss.clear();
      comma = 0;
    }
  }
  std::string vector_output = oss.str();
  ZF_LOGV("Vector: %s", vector_output.c_str());

  // reset oss (if we need it)
  oss.str(std::string());
  oss.clear();
#endif

  // Begin the mangle process 2.0
  if (level) {
    ZF_LOGD("CharMan");
    CharMan cm(buffer, work, text, text_offsets);
    ZF_LOGD("CharMan %d, %d chars, render %d", cm.mangle_count, cm.mangle_chars,
            cm.need_render);
    if (cm.need_render)
      need_render = 1;
  };

  if (need_render) {
    render(fd, buffer);
  } else {
    write(fd, buffer.data(), buffer.size());
    console_receive(&console, buffer);
  }
  return need_render;
}