//+----------------------------------------------------------------------------+
//| Description:  Magic Set Editor - Program to make Magic (tm) cards          |
//| Copyright:    (C) Twan van Laarhoven and the other MSE developers          |
//| License:      GNU General Public License 2 or later (see file COPYING)     |
//+----------------------------------------------------------------------------+

// ----------------------------------------------------------------------------- : Includes

#include <util/prec.hpp>
#include <render/text/element.hpp>
#include <util/tagged_string.hpp>
#include <data/field/text.hpp>
#include <gfx/color.hpp>

DECLARE_POINTER_TYPE(FontTextElement);

// ----------------------------------------------------------------------------- : TextElements

// Colors for <atom-param> tags
Color param_colors[] =
  { Color(0,170,0)
  , Color(0,0,200)
  , Color(200,0,100)
  , Color(200,200,0)
  , Color(0,170,170)
  , Color(200,0,0)
  };
const size_t param_colors_count = sizeof(param_colors) / sizeof(param_colors[0]);

struct Margins {
  double left, right, top;
};

// Helper class for TextElements::fromString, to allow persistent formating state accross recusive calls
struct TextElementsFromString {
  // What formatting is enabled?
  int bold = 0, italic = 0, symbol = 0;
  int soft = 0, kwpph = 0, param = 0, line = 0, soft_line = 0;
  int code = 0, code_kw = 0, code_string = 0, param_ref = 0;
  int param_id = 0, li = 0;
  vector<Color> colors;
  vector<double> sizes;
  vector<String> fonts;
  vector<Margins> margins;
  vector<Alignment> aligns;

  const TextStyle& style;
  Context& ctx;
  vector<TextParagraph>& paragraphs;
  
  TextElementsFromString(TextElements& out, const String& text, const TextStyle& style, Context& ctx)
    : style(style), ctx(ctx), paragraphs(out.paragraphs)
  {
    out.start = 0;
    out.end = text.size();
    paragraphs.emplace_back();
    paragraphs.back().start = 0;
    fromString(out.children, text, 0, text.size());
    paragraphs.back().end = text.size();
  }

private:
  // read TextElements from a string
  void fromString(vector<TextElementP>& elements, const String& text, size_t start, size_t end) {
    size_t text_start = start;
    // for each character...
    for (size_t pos = start ; pos < end ; ) {
      Char c = text.GetChar(pos);
      if (c == _('<')) {
        if (text_start < pos) {
          // text element before this tag?
          addText(elements, text, text_start, pos);
          addParagraphs(text, text_start, pos);
        }
        // a (formatting) tag
        size_t tag_start = pos;
        pos = skip_tag(text, tag_start);
        if      (is_tag(text, tag_start, _( "<b")))          bold        += 1;
        else if (is_tag(text, tag_start, _("</b")))          bold        -= 1;
        else if (is_tag(text, tag_start, _( "<i")))          italic      += 1;
        else if (is_tag(text, tag_start, _("</i")))          italic      -= 1;
        else if (is_tag(text, tag_start, _( "<sym")))        symbol      += 1;
        else if (is_tag(text, tag_start, _("</sym")))        symbol      -= 1;
        else if (is_tag(text, tag_start, _( "<line")))       line        += 1;
        else if (is_tag(text, tag_start, _("</line")))       line        -= 1;
        else if (is_tag(text, tag_start, _( "<soft-line")))  soft_line   += 1;
        else if (is_tag(text, tag_start, _("</soft-line")))  soft_line   -= 1;
        else if (is_tag(text, tag_start, _( "<sep-soft")))   soft        += 1;
        else if (is_tag(text, tag_start, _("</sep-soft")))   soft        -= 1;
        else if (is_tag(text, tag_start, _( "<soft")))       soft        += 1; // must be after <soft-line
        else if (is_tag(text, tag_start, _("</soft")))       soft        -= 1;
        else if (is_tag(text, tag_start, _( "<li")))         li          += 1;
        else if (is_tag(text, tag_start, _("</li")))         li          -= 1;
        else if (is_tag(text, tag_start, _( "<atom-kwpph"))) kwpph       += 1;
        else if (is_tag(text, tag_start, _("</atom-kwpph"))) kwpph       -= 1;
        else if (is_tag(text, tag_start, _( "<code-kw")))    code_kw     += 1;
        else if (is_tag(text, tag_start, _("</code-kw")))    code_kw     -= 1;
        else if (is_tag(text, tag_start, _( "<code-str")))   code_string += 1;
        else if (is_tag(text, tag_start, _("</code-str")))   code_string -= 1;
        else if (is_tag(text, tag_start, _( "<code")))       code        += 1;
        else if (is_tag(text, tag_start, _("</code")))       code        -= 1;
        else if (is_tag(text, tag_start, _( "<color"))) {
          size_t colon = text.find_first_of(_(">:"), tag_start);
          if (colon < pos - 1 && text.GetChar(colon) == _(':')) {
            auto c = parse_color(text.substr(colon+1, pos-colon-2));
            if (c) {
              colors.push_back(*c);
            } else {
              queue_message(MESSAGE_WARNING, _("Invalid color in tagged string: ") + text.substr(colon + 1, pos - colon - 2));
              colors.push_back(style.font.color);
            }
          }
        } else if (is_tag(text, tag_start, _("</color"))) {
          if (!colors.empty()) colors.pop_back();
        }
        else if (is_tag(text, tag_start, _( "<font"))) {
          size_t colon = text.find_first_of(_(">:"), tag_start);
          if (colon < pos - 1 && text.GetChar(colon) == _(':')) {
            fonts.push_back(text.substr(colon+1, pos-colon-2));
          }
        } else if (is_tag(text, tag_start, _("</font"))) {
          if (!fonts.empty()) fonts.pop_back();
        }
        else if (is_tag(text, tag_start, _( "<size"))) {
          size_t colon = text.find_first_of(_(">:"), tag_start);
          if (colon < pos - 1 && text.GetChar(colon) == _(':')) {
            double size = style.font.size;
            String v = text.substr(colon+1, pos-colon-2);
            v.ToDouble(&size);
            sizes.push_back(size);
          }
        } else if (is_tag(text, tag_start, _("</size"))) {
          if (!sizes.empty()) sizes.pop_back();
        }
        else if (is_tag(text, tag_start, _( "<ref-param"))) {
          // determine the param being referenced
          // from a tag <ref-param123>
          if (pos != String::npos) {
            String ref = text.substr(tag_start + 10, pos - tag_start - 11);
            long ref_n;
            if (ref.ToLong(&ref_n)) {
              param_id = (ref_n - 1)%param_colors_count + param_colors_count;
            }
          }
          param_ref += 1;
        }
        else if (is_tag(text, tag_start, _("</ref-param")))  param_ref   -= 1;
        else if (is_tag(text, tag_start, _( "<atom-param"))) param       += 1;
        else if (is_tag(text, tag_start, _("</atom-param"))) param       -= 1;
        else if (is_tag(text, tag_start, _("<atom"))) {
          // 'atomic' indicator
          #if 0
            // it would be nice if we could have semi-transparent brushes
            Color color = style.font.color; color.a /= 5;
          #else
            Color fg = style.font.color;
            Color color = fg.r+fg.g+fg.b < 255*2 ? Color(210,210,210) : Color(60,60,60);
          #endif
          size_t end_tag = min(end, match_close_tag(text, tag_start));
          intrusive_ptr<AtomTextElement> e = make_intrusive<AtomTextElement>(pos, end_tag, color);
          fromString(e->children, text, pos, end_tag);
          elements.push_back(e);
          pos = skip_tag(text, end_tag);
        } else if (is_tag(text, tag_start, _( "<error"))) {
          // error indicator
          size_t end_tag = min(end, match_close_tag(text, tag_start));
          intrusive_ptr<ErrorTextElement> e = make_intrusive<ErrorTextElement>(pos, end_tag);
          fromString(e->children, text, pos, end_tag);
          elements.push_back(e);
          pos = skip_tag(text, end_tag);
        } else if (is_tag(text, tag_start, _("</bullet"))) {
          // end of bullet point, set margin here
          if (li <= 0) {
            queue_message(MESSAGE_WARNING, _("<bullet> outside <li> tag"));
          }
          paragraphs.back().margin_end_char = pos;
        } else if (is_tag(text, tag_start, _("<margin"))) {
          size_t colon = text.find_first_of(_(">:"), tag_start);
          if (colon < pos - 1 && text.GetChar(colon) == _(':')) {
            size_t colon2 = text.find_first_of(_(">:"), colon + 1);
            size_t colon3 = colon2 < pos-1 ? text.find_first_of(_(">:"), colon2 + 1) : colon2;
            Margins m = {0.,0.,0.};
            text.substr(colon + 1, colon2 - colon - 2).ToDouble(&m.left);
            text.substr(colon2 + 1, colon3 - colon2 - 2).ToDouble(&m.right);
            text.substr(colon3 + 1, pos - colon3 - 2).ToDouble(&m.top);
            if (!margins.empty()) {
              m.left  += margins.back().left;
              m.right += margins.back().right;
              m.top   += margins.back().top;
            }
            margins.emplace_back(m);
            paragraphs.back().margin_left = m.left;
            paragraphs.back().margin_right = m.right;
            paragraphs.back().margin_top = m.top;
          }
        } else if (is_tag(text, tag_start, _("</margin"))) {
          if (!margins.empty()) margins.pop_back();
        } else if (is_tag(text, tag_start, _("<align"))) {
          size_t colon = text.find_first_of(_(">:"), tag_start);
          if (colon < pos - 1 && text.GetChar(colon) == _(':')) {
            Alignment align = alignment_from_string(text.substr(colon+1, pos-colon-2));
            aligns.push_back(align);
            paragraphs.back().alignment = align;
          }
        } else if (is_tag(text, tag_start, _("</align"))) {
          if (!aligns.empty()) aligns.pop_back();
        } else {
          // ignore other tags
        }
        // text starts again after the tag
        text_start = pos;
      } else {
        // normal text
        pos += 1;
      }
    }
    if (text_start < end) {
      // remaining text at the end
      addText(elements, text, text_start, end);
      addParagraphs(text, text_start, end);
    }
  }
  
private:
  /// Create a text element for a piece of text, text[start..end)
  void addText(vector<TextElementP>& elements, const String& text, size_t start, size_t end) {
    String content = untag(text.substr(start, end - start));
    assert(content.size() == end-start);
    // use symbol font?
    if (symbol > 0 && style.symbol_font.valid()) {
      elements.push_back(make_intrusive<SymbolTextElement>(content, start, end, style.symbol_font, &ctx));
    } else {
      // text, possibly mixed with symbols
      DrawWhat what = soft > 0 ? DRAW_ACTIVE : DRAW_NORMAL;
      LineBreak line_break = line > 0 ? LineBreak::LINE :
                             soft_line > 0 ? LineBreak::SOFT : LineBreak::HARD;
      if (kwpph > 0 || param > 0) {
        // bracket the text
        content = String(LEFT_ANGLE_BRACKET) + content + RIGHT_ANGLE_BRACKET;
        start -= 1;
        end   += 1;
      }
      // render as symbols or as text?
      if (style.always_symbol && style.symbol_font.valid()) {
        // mixed symbols/text, autodetected by symbol font
        size_t text_pos = 0;
        size_t pos = 0;
        FontP font;
        while (pos < end-start) {
          if (size_t n = style.symbol_font.font->recognizePrefix(content,pos)) {
            // at 'pos' there are n symbol font characters
            if (text_pos < pos) {
              // text before it?
              if (!font) font = makeFont(style);
              elements.push_back(make_intrusive<FontTextElement>(content.substr(text_pos, pos-text_pos), start+text_pos, start+pos, font, what, line_break));
            }
            elements.push_back(make_intrusive<SymbolTextElement>(content.substr(pos,n), start+pos, start+pos+n, style.symbol_font, &ctx));
            text_pos = pos += n;
          } else {
            ++pos;
          }
        }
        if (text_pos < pos) {
          if (!font) font = makeFont(style);
          elements.push_back(make_intrusive<FontTextElement>(content.substr(text_pos), start+text_pos, end, font, what, line_break));
        }
      } else {
        elements.push_back(make_intrusive<FontTextElement>(content, start, end, makeFont(style), what, line_break));
      }
    }
  }
  // Find paragraph breaks in text
  void addParagraphs(const String& text, size_t start, size_t end) {
    if (line == 0 && soft_line > 0) return;
    for (size_t i = start; i < end; ++i) {
      wxUniChar c = text.GetChar(i);
      if (c == '\n') {
        paragraphs.back().end = i + 1;
        paragraphs.emplace_back();
        paragraphs.back().start = i + 1;
        paragraphs.back().margin_end_char = i + 1;
        if (!margins.empty()) {
          paragraphs.back().margin_left  = margins.back().left;
          paragraphs.back().margin_right = margins.back().right;
          paragraphs.back().margin_top   = margins.back().top;
        }
        if (!aligns.empty()) {
          paragraphs.back().alignment = aligns.back();
        }
      }
    }
  }
  
  FontP makeFont(const TextStyle& style) {
    return style.font.make(
      (bold        > 0 ? FONT_BOLD        : FONT_NORMAL) |
      (italic      > 0 ? FONT_ITALIC      : FONT_NORMAL) |
      (soft        > 0 ? FONT_SOFT        : FONT_NORMAL) |
      (kwpph       > 0 ? FONT_SOFT        : FONT_NORMAL) |
      (code        > 0 ? FONT_CODE        : FONT_NORMAL) |
      (code_kw     > 0 ? FONT_CODE_KW     : FONT_NORMAL) |
      (code_string > 0 ? FONT_CODE_STRING : FONT_NORMAL),
      fonts.empty() ? nullptr : &fonts.back(),
      param > 0 || param_ref > 0
        ? &param_colors[(param_id++) % param_colors_count]
      : !colors.empty()
        ? &colors.back()
        : nullptr,
      !sizes.empty() ? &sizes.back() : nullptr
      );
  }
};

void TextElements::clear() {
  children.clear();
  paragraphs.clear();
}

void TextElements::fromString(const String& text, const TextStyle& style, Context& ctx) {
  clear();
  TextElementsFromString f(*this, text, style, ctx);
}
