#include <fstream>
#include <iostream>
#include <thread>                   // for sleep_for
#include <filesystem>
#include <fcntl.h>
#include <locale>
#include <codecvt>
#include <vector>
#include <nlohmann/json.hpp>
#include <ft2build.h>
#include <SFML/Graphics/Font.hpp>
#include FT_FREETYPE_H
#include FT_TRUETYPE_TABLES_H
#include FT_TRUETYPE_IDS_H
#include "dbc.hpp"
#include "constants.hpp"
#include <fmt/core.h>

using namespace nlohmann;
using fmt::format, fmt::println, fmt::print;
namespace fs = std::filesystem;
using std::string, std::wstring, std::vector;

const size_t CLEAR_CACHE_POINT=100;

struct FontIndex {
  size_t cm_index; // charmap index
  string glyph;
};

struct FontExtractor {
  size_t $clear_count = 1;
  wchar_t ui_base_char = L'█';
  vector<FontIndex> $index;
  std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> $converter;
  vector<string> $charmap;
  vector<wchar_t> $wcharmap;
  sf::Font $font;
  sf::FloatRect $grid_bounds;
  int $font_size;
  fs::path $font_path;
  json $results;

  FontExtractor(fs::path font_path) :
    $font_path(font_path)
  {
    bool good = $font.loadFromFile($font_path.string());
    dbc::check(good, format("failed to load font {}", $font_path.string()));
    $font.setSmooth(false);

    for(int i = 100; i < 200; i++) {
      auto glyph = $font.getGlyph(ui_base_char, i, false);
      if(glyph.bounds.width > 0 && glyph.bounds.height > 0) {
        $grid_bounds = glyph.bounds;
        $font_size = i;
        break;
      }
    }

    dbc::check($grid_bounds.width > 0 && $grid_bounds.height > 0, "couldn't find a valid font size");

    println("!!!!!!!!!!!!!!!!!!!!! FONT SIZE {}", $font_size);
  }

  void configure_font() {
    FT_ULong   charcode;
    FT_UInt    gindex;
    FT_String  buf[32] = {0};
    FT_Library library;
    FT_Face face;

    auto error = FT_Init_FreeType(&library);
    dbc::check(!error, "Failed to initialize freetype library.");

    error = FT_New_Face(library, FONT_FILE_NAME, 0, &face);

    dbc::check(face->num_faces == 1, format("Font {} has {} but I only support 1.", FONT_FILE_NAME, face->num_faces));
    dbc::check(face->charmap, "Font doesn't have a charmap.");

    println("Font charmaps {}", face->num_charmaps);
    auto active = FT_Get_Charmap_Index(face->charmap);

    for(int i = 0; i < face->num_charmaps; i++) {
      auto format = FT_Get_CMap_Format(face->charmaps[i]);
      auto lang_id = FT_Get_CMap_Language_ID(face->charmaps[i]);

      println("{}: {} {}, active: {}, platform: {}, encoding: {}, ms_unicode: {}, language: {}",
          i,
          format >= 0 ? "format" : "synthetic",
          format,
          i == active,
          face->charmaps[i]->platform_id,
          face->charmaps[i]->encoding_id,
          TT_MS_ID_UNICODE_CS,
          lang_id);

      // BUG: this is windows only
      if(face->charmaps[i]->encoding_id == TT_MS_ID_UNICODE_CS) {
        FT_Set_Charmap(face, face->charmaps[i]);
      }
    }

    bool has_names = FT_HAS_GLYPH_NAMES(face);

    // get the first char of the map
    charcode = FT_Get_First_Char(face, &gindex);

    // go through every char and pre-convert
    for(int i = 0; gindex; i++) {
      if(has_names) FT_Get_Glyph_Name(face, gindex, buf, 32 );

      // store it in my pre-convert charmap for later
      wstring wcodestr{(wchar_t)charcode};
      string codestr = from_unicode(wcodestr);
      $charmap.emplace_back(codestr);
      $wcharmap.push_back(charcode);

      // keep going
      charcode = FT_Get_Next_Char(face, charcode, &gindex );
    }

    FT_Done_Face(face);
    FT_Done_FreeType(library);

    println("FreeType extracted {} glyphs.", $charmap.size());
  }

  size_t next_valid_char(size_t cur_char)  {
    for(size_t i = cur_char+1; i < $wcharmap.size(); i++) {
      wchar_t test_char = $wcharmap[i];
      if($font.hasGlyph(test_char)) {
        auto glyph = $font.getGlyph(test_char, $font_size, false);
        auto bounds = glyph.bounds;

        // skip bad chars
        if(bounds.width <= 0 || bounds.height <= 0) continue;

        if(bounds.width <= $grid_bounds.width &&
            bounds.height <= $grid_bounds.height) {
          return i;
        }
      }

      clear_font_cache();
    }

    return 0;
  }

  void extract_valid() {
    size_t cur_char = next_valid_char(1);

    while(cur_char != 0) {
      string out = from_unicode(wstring_at(cur_char));
      $results.push_back(out);
      cur_char = next_valid_char(cur_char + 1);
    }
  }

  string from_unicode(wstring input) {
    try {
      return $converter.to_bytes(input);
    } catch(...) {
      return $converter.to_bytes(L"?");
    }
  }

  string at(size_t x) {
    return $index[x].glyph;
  }

  wstring wstring_at(size_t cm_index) {
    return wstring{$wcharmap[cm_index]};
  }

  void clear_font_cache() {
    if($clear_count % CLEAR_CACHE_POINT == 0) {
      bool good = $font.loadFromFile($font_path.string());
      dbc::check(good, format("failed to load font {}", $font_path.string()));
      $font.setSmooth(false);

      $clear_count++;
    }
  }
};

int main(int argc, char *argv[]) {
  if(argc != 3) {
    println("USAGE: fontextractor <input> <output>");
    return 1;
  }

  fs::path font_path = argv[1];
  fs::path out_path = argv[2];

  dbc::check(fs::exists(font_path), "ERROR: input file doesn't exist");

  FontExtractor fex(font_path);
  fex.configure_font();
  fex.extract_valid();

  std::ofstream json_out(out_path, std::ios::binary);
  json_out << fex.$results.dump(2) << std::endl;

  println("Wrote {} chars to {}.", fex.$results.size(), out_path.string());
}
