#include "render.hpp"
#include "ansi_parser.hpp"
#include <cmath>
#include <fmt/core.h>
#include <array>
#include "map.hpp"
#include <iostream>
#include "color.hpp"

#if defined(_WIN64) || defined(_WIN32)
 #include <stdio.h>
 #include <fcntl.h>
 #include <io.h>
#endif

using namespace fmt;

SFMLRender::SFMLRender(sf::RenderWindow &window) :
  $window(window),
  $map_font_size(0),
  $line_spacing(0),
  $default_fg(ColorValue::LIGHT_MID),
  $default_bg(ColorValue::BLACK),
  $bg_sprite($font_texture),
  $font(FONT_FILE_NAME),
  $ui_text($font),
  $ansi($default_fg, $default_bg)
{
  // force true color, but maybe I want to support different color sets
  $font.setSmooth(false);
  $ui_text.setPosition({0,0});
  $ui_text.setCharacterSize($config.ui_font_size);
  $ui_text.setFillColor(ColorValue::LIGHT_MID);
  sf::Glyph glyph = $font.getGlyph($config.ui_base_char, $config.ui_font_size, false);
  $text_bounds = glyph.bounds;
  $cells_w = std::ceil($config.video_x / $text_bounds.size.x);
  $cells_h = std::ceil($config.video_y / $text_bounds.size.y);
}

sf::Sprite &SFMLRender::get_text_sprite(wchar_t tile) {
  if(!$sprites.contains(tile)) {
    sf::Glyph glyph = $font.getGlyph(tile, $map_font_size, false);
    // WARNING! we actually have to do this here because SFML caches
    // the glyphs on the font texture, so this gets loaded each time
    // we get a new glyph from the font.
    $font_texture = $font.getTexture($map_font_size);
    $sprites.try_emplace(tile, $font_texture, glyph.textureRect);
  }

  return $sprites.at(tile);
}

void SFMLRender::clear_cache() {
  $sprites.clear();
  bool good = $font.openFromFile(FONT_FILE_NAME);
  dbc::check(good, "Failed to load the font.");
  $font.setSmooth(false);
  $ui_text.setFont($font);
}

void SFMLRender::center_panel(Panel &panel) {
  int cell_center_x = ($cells_w - panel.width) / 2;
  int cell_center_y = ($cells_h - panel.height) / 2;

  panel.x = cell_center_x * $text_bounds.size.x;
  panel.y = cell_center_y * $text_bounds.size.y;
}

void SFMLRender::resize_grid(int new_size, Panel &panel_out) {
  auto glyph = $font.getGlyph($config.bg_tile, new_size, false);
  int view_x = std::ceil(($config.video_x - panel_out.x) / glyph.bounds.size.x);
  int view_y = std::ceil(($config.video_y - panel_out.y) / glyph.bounds.size.y);

  // looks good, set 'em all
  $base_glyph = glyph;
  $map_font_size = new_size;
  $sprites.clear(); // need to reset the sprites for the new size
  $line_spacing = $font.getLineSpacing($map_font_size);
  $bg_sprite = get_text_sprite($config.bg_tile);
  $grid_bounds = $bg_sprite.getLocalBounds();
  panel_out.resize(view_x, view_y);
}

inline void configure_tile(const sf::Sprite &sprite, sf::FloatRect &sp_bounds, sf::FloatRect grid_bounds, float &width_delta, float &height_delta) {
  // BUG: I think I could create a struct that kept this info for all sprites loaded
  // should look into caching all this instead of calcing it each time
  sp_bounds = sprite.getLocalBounds();

  // calculate where to center the sprite, but only if it's smaller
  width_delta = grid_bounds.size.x > sp_bounds.size.x ? (grid_bounds.size.x - sp_bounds.size.x) / 2 : 0;
  height_delta = grid_bounds.size.y > sp_bounds.size.x ? (grid_bounds.size.y - sp_bounds.size.y) / 2 : 0;
}

void SFMLRender::render_grid(const std::wstring &text, sf::Color default_fg, sf::Color default_bg, float x, float y) {
  wchar_t last_tile = $config.bg_tile;
  sf::FloatRect sp_bounds;
  float width_delta = 0;
  float height_delta = 0;
  sf::Sprite &sprite = get_text_sprite(last_tile);
  const float start_x = x;
  sf::Color cur_fg = default_fg;
  sf::Color cur_bg = default_bg;

  $ansi.parse(text, [&](auto fg, auto bg) {
      cur_fg = fg;
      cur_bg = bg;
    },

    [&](wchar_t tile) {
      switch(tile) {
        case '\r': break; // ignore it
        case '\n': {
            // don't bother processing newlines, just skip
            y += $line_spacing;
            x = start_x;
          }
          break;
        default: {
          // only get a new sprite if the tile changed
          if(last_tile != tile) {
            sprite = get_text_sprite(tile);
            configure_tile(sprite, sp_bounds, $grid_bounds, width_delta, height_delta);
            last_tile = tile; // update last tile seen
          }

          sprite.setPosition({x+width_delta, y+height_delta});
          sprite.setColor(cur_fg);

          // only draw background char if it's different from default
          if(cur_bg != default_bg) {
            $bg_sprite.setPosition({x, y});
            $bg_sprite.setColor(cur_bg);
            $window.draw($bg_sprite);
          }

          $window.draw(sprite);
          // next cell
          x += $base_glyph.advance;
        }
      }
  });
}

inline sf::FloatRect draw_chunk(sf::RenderWindow& window,
    sf::FloatRect text_bounds, sf::Text& text, sf::Color default_bg,
    sf::Color bgcolor, int bg_box_offset, float x, float y, std::wstring &out)
{
  text.setString(out);
  text.setPosition({x, y});
  // get a base character for the cell size
  sf::FloatRect bounds({x, y}, {text_bounds.size.x * out.size(), text_bounds.size.y});

  if(default_bg != bgcolor) {
    sf::RectangleShape backing(bounds.size);
    backing.setFillColor(bgcolor);
    backing.setPosition({bounds.position.x, bounds.position.y + bg_box_offset});
    window.draw(backing);
  }

  window.draw(text);
  out.clear();
  return bounds;
}

void SFMLRender::render_text(const std::wstring &text, sf::Color default_fg, sf::Color default_bg, float start_x, float start_y) {
  std::wstring out;
  float x = start_x;
  float y = start_y;
  sf::Color cur_bg = default_bg;

  // start with the default_fg until it's changed
  $ui_text.setFillColor(default_fg);

  $ansi.parse(text,
    [&](auto fg, auto bg) {
        if(out.size() > 0 ) {
          auto bounds = draw_chunk($window,
              $text_bounds, $ui_text,
              default_bg, cur_bg, $config.bg_box_offset, x, y, out);
          x += bounds.size.x;
        }
        cur_bg = bg;
        $ui_text.setFillColor(fg);
    },
    [&](wchar_t tile) {
      switch(tile) {
        case '\r': break; // ignore it
        case '\n': {
          sf::FloatRect bounds;

          if(out.size() > 0) {
            bounds = draw_chunk($window, $text_bounds,
                $ui_text, default_bg, cur_bg, $config.bg_box_offset, x, y, out);
          } else {
            bounds = $ui_text.getLocalBounds();
          }

          y += bounds.size.y;
          x = start_x; // reset to the original position
        }
        break;
        default:
          out += tile;
          break;
      }
    }
  );

  if(out.size() > 0) {
    draw_chunk($window, $text_bounds, $ui_text, default_bg, cur_bg, $config.bg_box_offset, x, y, out);
  }
}

void SFMLRender::draw_sprite(sf::Sprite &sprite, sf::Shader *shader) {
  $window.draw(sprite, shader);
}

/*
 * Does not render the panel, you have to do that so you can control
 * when things render.
 */
void SFMLRender::draw(Panel &panel, float x_offset, float y_offset) {
  const std::wstring &panelout = panel.to_string();

  auto bounds = panel.grid ? $grid_bounds : $text_bounds;

  sf::RectangleShape backing(
      sf::Vector2f(bounds.size.x * panel.width + panel.border_px,
        bounds.size.y * panel.height + panel.border_px));

  backing.setFillColor(panel.default_bg);

  if(panel.has_border) {
    backing.setOutlineColor(panel.border_color);
    backing.setOutlineThickness(panel.border_px);
  }

  backing.setPosition({panel.x + x_offset, panel.y + y_offset});
  $window.draw(backing);

  if(panel.grid) {
    render_grid(panelout, panel.default_fg, panel.default_bg, panel.x + x_offset, panel.y + y_offset);
  } else {
    render_text(panelout, panel.default_fg, panel.default_bg, panel.x + x_offset, panel.y + y_offset);
  }
}

bool SFMLRender::mouse_position(Panel &panel, Point &out) {
  // yes, you have to do this in sfml
  sf::Vector2f pos = $window.mapPixelToCoords(sf::Mouse::getPosition($window));

  auto bounds = panel.grid ? $grid_bounds : $text_bounds;

  if(pos.x >= panel.x && pos.y >= panel.y
      && pos.x <= (panel.x + panel.width * bounds.size.x)
      && pos.y <= (panel.y + panel.height * bounds.size.y))
  {
      out = {
        size_t((pos.x - panel.x) / bounds.size.x),
        size_t((pos.y - panel.y) / bounds.size.y)
      };

      return true;
  }

  return false;
}

void SFMLRender::init_terminal() {
#if defined(_WIN64) || defined(_WIN32)
  _setmode(_fileno(stdout), _O_U16TEXT);
#endif

  ftxui::Terminal::SetColorSupport(ftxui::Terminal::Color::TrueColor);
}