/*
 *  Copyright (C) 2005-2018 Team Kodi
 *  This file is part of Kodi - https://kodi.tv
 *
 *  SPDX-License-Identifier: GPL-2.0-or-later
 *  See LICENSES/README.md for more information.
 */

#include "GUITextLayout.h"

#include "GUIColorManager.h"
#include "GUIComponent.h"
#include "GUIFont.h"
#include "ServiceBroker.h"
#include "utils/CharsetConverter.h"
#include "utils/StringUtils.h"
#include "utils/TransformMatrix.h"
#include "utils/log.h"
#include "windowing/GraphicContext.h"
#include "windowing/WinSystem.h"

#include <algorithm>
#include <functional>
#include <iterator>
#include <limits>

CGUIString::CGUIString(iString start, iString end, bool carriageReturn)
{
  m_text.assign(start, end);
  m_carriageReturn = carriageReturn;
}

std::string CGUIString::GetAsString() const
{
  std::string text;
  for (unsigned int i = 0; i < m_text.size(); i++)
    text += (char)(m_text[i] & 0xff);
  return text;
}

CGUITextLayout::CGUITextLayout(CGUIFont *font, bool wrap, float fHeight, CGUIFont *borderFont)
{
  m_varFont = m_font = font;
  m_borderFont = borderFont;
  m_textColor = 0;
  m_wrap = wrap;
  m_maxHeight = fHeight;
  m_textWidth = 0;
  m_textHeight = 0;
  m_lastUpdateW = false;
}

void CGUITextLayout::SetWrap(bool bWrap)
{
  m_wrap = bWrap;
}

void CGUITextLayout::SetFont(CGUIFont* font)
{
  const bool usingMonoFont = m_font != nullptr && m_font == m_monoFont;
  m_varFont = font;
  m_font = usingMonoFont ? m_monoFont : font;

  // invalidate the cached text so the next Update()/UpdateW() re-lays out using the new font
  m_lastUtf8Text.clear();
  m_lastText.clear();
}

void CGUITextLayout::Render(float x,
                            float y,
                            float angle,
                            KODI::UTILS::COLOR::Color color,
                            KODI::UTILS::COLOR::Color shadowColor,
                            uint32_t alignment,
                            float maxWidth,
                            bool solid)
{
  if (!m_font)
    return;

  // set the main text color
  if (!m_colors.empty())
    m_colors[0] = color;

  // render the text at the required location, angle, and size
  if (angle)
  {
    static const float degrees_to_radians = 0.01745329252f;
    CServiceBroker::GetWinSystem()->GetGfxContext().AddTransform(TransformMatrix::CreateZRotation(angle * degrees_to_radians, x, y, CServiceBroker::GetWinSystem()->GetGfxContext().GetScalingPixelRatio()));
  }
  // center our text vertically
  if (alignment & XBFONT_CENTER_Y)
  {
    y -= m_font->GetTextHeight(m_lines.size()) * 0.5f;
    alignment &= ~XBFONT_CENTER_Y;
  }
  m_font->Begin();
  for (const auto& string : m_lines)
  {
    uint32_t align = alignment;
    if (align & XBFONT_JUSTIFIED && string.m_carriageReturn)
      align &= ~XBFONT_JUSTIFIED;
    if (solid)
      m_font->DrawText(x, y, m_colors[0], shadowColor, string.m_text, align, maxWidth);
    else
      m_font->DrawText(x, y, m_colors, shadowColor, string.m_text, align, maxWidth);
    y += m_font->GetLineHeight();
  }
  m_font->End();
  if (angle)
    CServiceBroker::GetWinSystem()->GetGfxContext().RemoveTransform();
}

bool CGUITextLayout::UpdateScrollinfo(CScrollInfo &scrollInfo)
{
  if (!m_font)
    return false;
  if (m_lines.empty())
    return false;

  return m_font->UpdateScrollInfo(m_lines[0].m_text, scrollInfo);
}

void CGUITextLayout::RenderScrolling(float x,
                                     float y,
                                     float angle,
                                     KODI::UTILS::COLOR::Color color,
                                     KODI::UTILS::COLOR::Color shadowColor,
                                     uint32_t alignment,
                                     float maxWidth,
                                     const CScrollInfo& scrollInfo)
{
  if (!m_font)
    return;

  // set the main text color
  if (!m_colors.empty())
    m_colors[0] = color;

  // render the text at the required location, angle, and size
  if (angle)
  {
    static const float degrees_to_radians = 0.01745329252f;
    CServiceBroker::GetWinSystem()->GetGfxContext().AddTransform(TransformMatrix::CreateZRotation(angle * degrees_to_radians, x, y, CServiceBroker::GetWinSystem()->GetGfxContext().GetScalingPixelRatio()));
  }
  // center our text vertically
  if (alignment & XBFONT_CENTER_Y)
  {
    y -= m_font->GetTextHeight(m_lines.size()) * 0.5f;
    alignment &= ~XBFONT_CENTER_Y;
  }
  m_font->Begin();
  // NOTE: This workaround is needed as otherwise multi-line text that scrolls
  //       will scroll in proportion to the number of lines.  Ideally we should
  //       do the DrawScrollingText calculation here.  This probably won't make
  //       any difference to the smoothness of scrolling though which will be
  //       jumpy with this sort of thing.  It's not exactly a well used situation
  //       though, so this hack is probably OK.
  for (const auto& string : m_lines)
  {
    m_font->DrawScrollingText(x, y, m_colors, shadowColor, string.m_text, alignment, maxWidth, scrollInfo);
    y += m_font->GetLineHeight();
  }
  m_font->End();
  if (angle)
    CServiceBroker::GetWinSystem()->GetGfxContext().RemoveTransform();
}

void CGUITextLayout::RenderOutline(float x,
                                   float y,
                                   KODI::UTILS::COLOR::Color color,
                                   KODI::UTILS::COLOR::Color outlineColor,
                                   uint32_t alignment,
                                   float maxWidth)
{
  if (!m_font)
    return;

  // center our text vertically
  if (alignment & XBFONT_CENTER_Y)
  {
    y -= m_font->GetTextHeight(m_lines.size()) * 0.5f;
    alignment &= ~XBFONT_CENTER_Y;
  }
  if (m_borderFont)
  {
    // adjust so the baselines of the fonts align
    float by = y + m_font->GetTextBaseLine() - m_borderFont->GetTextBaseLine();
    m_borderFont->Begin();
    for (const auto& string : m_lines)
    {
      uint32_t align = alignment;
      if (align & XBFONT_JUSTIFIED && string.m_carriageReturn)
        align &= ~XBFONT_JUSTIFIED;
      // text centered horizontally must be computed using the original font, not the bordered
      // font, as the bordered font will be wider, and thus will end up uncentered.
      //! @todo We should really have a better way to handle text extent - at the moment we assume
      //!       that text is rendered from a posx, posy, width, and height which isn't enough to
      //!       accurately position text. We need a vertical and horizontal offset of the baseline
      //!       and cursor as well.
      float bx = x;
      if (align & XBFONT_CENTER_X)
      {
        bx -= m_font->GetTextWidth(string.m_text) * 0.5f;
        align &= ~XBFONT_CENTER_X;
      }

      // don't pass maxWidth through to the renderer for the same reason above: it will cause clipping
      // on the left.
      m_borderFont->DrawText(bx, by, outlineColor, 0, string.m_text, align, 0);
      by += m_borderFont->GetLineHeight();
    }
    m_borderFont->End();
  }

  // set the main text color
  if (!m_colors.empty())
    m_colors[0] = color;

  m_font->Begin();
  for (const auto& string : m_lines)
  {
    uint32_t align = alignment;
    if (align & XBFONT_JUSTIFIED && string.m_carriageReturn)
      align &= ~XBFONT_JUSTIFIED;

    // don't pass maxWidth through to the renderer for the reason above.
    m_font->DrawText(x, y, m_colors, 0, string.m_text, align, 0);
    y += m_font->GetLineHeight();
  }
  m_font->End();
}

bool CGUITextLayout::Update(const std::string &text, float maxWidth, bool forceUpdate /*= false*/, bool forceLTRReadingOrder /*= false*/)
{
  if (text == m_lastUtf8Text && !forceUpdate && !m_lastUpdateW)
    return false;

  m_lastUtf8Text = text;
  m_lastUpdateW = false;
  std::wstring utf16;
  g_charsetConverter.utf8ToW(text, utf16, false);
  UpdateCommon(utf16, maxWidth, forceLTRReadingOrder);
  return true;
}

bool CGUITextLayout::UpdateW(const std::wstring &text, float maxWidth /*= 0*/, bool forceUpdate /*= false*/, bool forceLTRReadingOrder /*= false*/)
{
  if (text == m_lastText && !forceUpdate && m_lastUpdateW)
    return false;

  m_lastText = text;
  m_lastUpdateW = true;
  UpdateCommon(text, maxWidth, forceLTRReadingOrder);
  return true;
}

void CGUITextLayout::UpdateCommon(const std::wstring &text, float maxWidth, bool forceLTRReadingOrder)
{
  // parse the text for style information
  vecText parsedText;
  std::vector<KODI::UTILS::COLOR::Color> colors;
  ParseText(text, m_font ? m_font->GetStyle() : 0, m_textColor, colors, parsedText);

  // and update
  UpdateStyled(parsedText, colors, maxWidth, forceLTRReadingOrder);
}

void CGUITextLayout::UpdateStyled(const vecText& text,
                                  const std::vector<KODI::UTILS::COLOR::Color>& colors,
                                  float maxWidth,
                                  bool forceLTRReadingOrder)
{
  // empty out our previous string
  m_lines.clear();
  m_colors = colors;

  // if we need to wrap the text, then do so
  if (m_wrap)
    WrapText(text, maxWidth);
  else
    LineBreakText(text, m_lines);

  // remove any trailing blank lines
  while (!m_lines.empty() && m_lines.back().m_text.empty())
    m_lines.pop_back();

  BidiTransform(m_lines, forceLTRReadingOrder);

  // and cache the width and height for later reading
  CalcTextExtent();
}

// BidiTransform is used to handle RTL text flipping in the string
void CGUITextLayout::BidiTransform(std::vector<CGUIString>& lines, bool forceLTRReadingOrder)
{
  for (unsigned int i = 0; i < lines.size(); i++)
  {
    CGUIString& line = lines[i];
    unsigned int lineLength = line.m_text.size();
    std::wstring logicalText;
    vecText style;

    logicalText.reserve(lineLength);
    style.reserve(lineLength);

    // Separate the text and style for the input styled text
    for (const auto& it : line.m_text)
    {
      logicalText.push_back((wchar_t)(it & 0xffff));
      style.push_back(it & 0xffff0000);
    }

    // Allocate memory for visual to logical map and call bidi
    int* visualToLogicalMap = new (std::nothrow) int[lineLength + 1]();
    std::wstring visualText = BidiFlip(logicalText, forceLTRReadingOrder, visualToLogicalMap);

    vecText styledVisualText;
    styledVisualText.reserve(lineLength);

    // If memory allocation failed, fallback to text with no styling
    if (!visualToLogicalMap)
    {
      for (unsigned int j = 0; j < visualText.size(); j++)
      {
        styledVisualText.push_back(visualText[j]);
      }
    }
    else
    {
      for (unsigned int j = 0; j < visualText.size(); j++)
      {
        styledVisualText.push_back(style[visualToLogicalMap[j]] | visualText[j]);
      }
    }

    delete[] visualToLogicalMap;

    // replace the original line with the processed one
    lines[i] = CGUIString(styledVisualText.begin(), styledVisualText.end(), line.m_carriageReturn);
  }
}

std::wstring CGUITextLayout::BidiFlip(const std::wstring& text,
                                      bool forceLTRReadingOrder,
                                      int* visualToLogicalMap /*= nullptr*/)
{
  std::wstring visualText;
  std::u32string utf32logical;
  std::u32string utf32visual;

  // Convert to utf32, call bidi then convert the result back to utf16
  g_charsetConverter.wToUtf32(text, utf32logical);
  g_charsetConverter.utf32logicalToVisualBiDi(utf32logical, utf32visual, forceLTRReadingOrder,
                                              false,
                                              visualToLogicalMap);
  g_charsetConverter.utf32ToW(utf32visual, visualText);

  return visualText;
}

void CGUITextLayout::Filter(std::string &text)
{
  std::wstring utf16;
  g_charsetConverter.utf8ToW(text, utf16, false);
  std::vector<KODI::UTILS::COLOR::Color> colors;
  vecText parsedText;
  ParseText(utf16, 0, 0xffffffff, colors, parsedText);
  utf16.clear();
  for (unsigned int i = 0; i < parsedText.size(); i++)
    utf16 += (wchar_t)(0xffff & parsedText[i]);
  g_charsetConverter.wToUTF8(utf16, text);
}

void CGUITextLayout::ParseText(const std::wstring& text,
                               uint32_t defaultStyle,
                               KODI::UTILS::COLOR::Color defaultColor,
                               std::vector<KODI::UTILS::COLOR::Color>& colors,
                               vecText& parsedText)
{
  // run through the string, searching for:
  // [B] or [/B] -> toggle bold on and off
  // [I] or [/I] -> toggle italics on and off
  // [COLOR ffab007f] or [/COLOR] -> toggle color on and off
  // [CAPS <option>] or [/CAPS] -> toggle capatilization on and off
  // [TABS] tab amount [/TABS] -> add tabulator space in view

  uint32_t currentStyle = defaultStyle; // start with the default font's style
  KODI::UTILS::COLOR::Color currentColor = 0;

  colors.push_back(defaultColor);
  std::stack<KODI::UTILS::COLOR::Color> colorStack;
  colorStack.push(0);

  // Boolean style tags ([B], [I], [LIGHT], [UPPERCASE], [LOWERCASE], [CAPITALIZE])
  // use per-tag reference counts so that nested identical tags work correctly.
  // This fixes the case where a skin wraps a label in e.g. [B]...[/B] while the
  // addon has already formatted the value with [B]...[/B], producing [B][B]text[/B][/B].
  // With simple |= / &=~ the first [/B] would clear bold for everything that follows.
  // With reference counts the bit stays set until the last matching open tag is closed.
  // Unmatched closing tags are no-ops (count floors at 0).
  int boldDepth = 0;
  int italicDepth = 0;
  int lightDepth = 0;
  int uppercaseDepth = 0;
  int lowercaseDepth = 0;
  int capitalizeDepth = 0;

  int startPos = 0;
  size_t pos = text.find(L'[');
  while (pos != std::wstring::npos && pos + 1 < text.size())
  {
    uint32_t newStyle = 0;
    KODI::UTILS::COLOR::Color newColor = currentColor;
    bool colorTagChange = false;
    bool newLine = false;
    int tabs = 0;
    // have a [ - check if it's an ON or OFF switch
    bool on(true);
    size_t endPos = pos++; // finish of string
    if (text[pos] == L'/')
    {
      on = false;
      pos++;
    }
    // check for each type
    if (text.compare(pos, 2, L"B]") == 0)
    { // bold
      pos += 2;
      if (on)
      {
        if (text.find(L"[/B]", pos) != std::wstring::npos)
          ++boldDepth;
      }
      else if (boldDepth > 0)
        --boldDepth;
      newStyle = FONT_STYLE_BOLD;
    }
    else if (text.compare(pos, 2, L"I]") == 0)
    { // italics
      pos += 2;
      if (on)
      {
        if (text.find(L"[/I]", pos) != std::wstring::npos)
          ++italicDepth;
      }
      else if (italicDepth > 0)
        --italicDepth;
      newStyle = FONT_STYLE_ITALICS;
    }
    else if (text.compare(pos, 10, L"UPPERCASE]") == 0)
    {
      pos += 10;
      if (on)
      {
        if (text.find(L"[/UPPERCASE]", pos) != std::wstring::npos)
          ++uppercaseDepth;
      }
      else if (uppercaseDepth > 0)
        --uppercaseDepth;
      newStyle = FONT_STYLE_UPPERCASE;
    }
    else if (text.compare(pos, 10, L"LOWERCASE]") == 0)
    {
      pos += 10;
      if (on)
      {
        if (text.find(L"[/LOWERCASE]", pos) != std::wstring::npos)
          ++lowercaseDepth;
      }
      else if (lowercaseDepth > 0)
        --lowercaseDepth;
      newStyle = FONT_STYLE_LOWERCASE;
    }
    else if (text.compare(pos, 11, L"CAPITALIZE]") == 0)
    {
      pos += 11;
      if (on)
      {
        if (text.find(L"[/CAPITALIZE]", pos) != std::wstring::npos)
          ++capitalizeDepth;
      }
      else if (capitalizeDepth > 0)
        --capitalizeDepth;
      newStyle = FONT_STYLE_CAPITALIZE;
    }
    else if (text.compare(pos, 6, L"LIGHT]") == 0)
    {
      pos += 6;
      if (on)
      {
        if (text.find(L"[/LIGHT]", pos) != std::wstring::npos)
          ++lightDepth;
      }
      else if (lightDepth > 0)
        --lightDepth;
      newStyle = FONT_STYLE_LIGHT;
    }
    else if (text.compare(pos, 5, L"TABS]") == 0)
    {
      pos += 5;
      if (on)
      {
        const size_t end = text.find(L"[/TABS]", pos);
        if (end != std::wstring::npos)
        {
          std::string t;
          g_charsetConverter.wToUTF8(text.substr(pos, end - pos), t);
          tabs = atoi(t.c_str());
          pos = end + 7;
        }
      }
      // Equivalent of newStyle for other formatting tags to have the tag text discarded.
      if (!tabs)
        tabs = -1;
    }
    else if (text.compare(pos, 3, L"CR]") == 0 && on)
    {
      newLine = true;
      pos += 3;
    }
    else if (text.compare(pos,5, L"COLOR") == 0)
    { // color
      size_t finish = text.find(L']', pos + 5);
      if (on && finish != std::wstring::npos &&
          text.find(L"[/COLOR]", finish) != std::wstring::npos)
      {
        std::string t;
        g_charsetConverter.wToUTF8(text.substr(pos + 5, finish - pos - 5), t);
        KODI::UTILS::COLOR::Color color = CServiceBroker::GetGUI()->GetColorManager().GetColor(t);
        const auto& it = std::ranges::find(colors, color);
        if (it == colors.end())
        { // create new color
          if (colors.size() <= 0xFF)
          {
            newColor = colors.size();
            colors.push_back(color);
          }
          else // we have only 8 bits for color index, fallback to first color if reach max.
            newColor = 0;
        }
        else
          // reuse existing color
          newColor = it - colors.begin();
        colorStack.push(newColor);
        colorTagChange = true;
      }
      else if (!on && finish == pos + 5 && colorStack.size() > 1)
      { // revert to previous color
        colorStack.pop();
        newColor = colorStack.top();
        colorTagChange = true;
      }
      if (finish != std::wstring::npos)
        pos = finish + 1;
    }

    if (newStyle || colorTagChange || newLine || tabs)
    { // we have a new style or a new color, so format up the previous segment
      std::wstring subText = text.substr(startPos, endPos - startPos);
      if (currentStyle & FONT_STYLE_UPPERCASE)
        StringUtils::ToUpper(subText);
      if (currentStyle & FONT_STYLE_LOWERCASE)
        StringUtils::ToLower(subText);
      if (currentStyle & FONT_STYLE_CAPITALIZE)
        StringUtils::ToCapitalize(subText);
      AppendToUTF32(subText, ((currentStyle & FONT_STYLE_MASK) << 24) | (currentColor << 16), parsedText);
      if (newLine)
        parsedText.push_back(L'\n');
      for (int i = 0; i < tabs; ++i)
        parsedText.push_back(L'\t');

      // switch to the new style, deriving it from the current depths
      startPos = pos;
      currentColor = newColor;
      currentStyle = defaultStyle;
      if (boldDepth > 0)
        currentStyle |= FONT_STYLE_BOLD;
      if (italicDepth > 0)
        currentStyle |= FONT_STYLE_ITALICS;
      if (lightDepth > 0)
        currentStyle |= FONT_STYLE_LIGHT;
      if (uppercaseDepth > 0)
        currentStyle |= FONT_STYLE_UPPERCASE;
      if (lowercaseDepth > 0)
        currentStyle |= FONT_STYLE_LOWERCASE;
      if (capitalizeDepth > 0)
        currentStyle |= FONT_STYLE_CAPITALIZE;
    }
    pos = text.find(L'[', pos);
  }
  // now grab the remainder of the string
  std::wstring subText = text.substr(startPos);
  if (currentStyle & FONT_STYLE_UPPERCASE)
    StringUtils::ToUpper(subText);
  if (currentStyle & FONT_STYLE_LOWERCASE)
    StringUtils::ToLower(subText);
  if (currentStyle & FONT_STYLE_CAPITALIZE)
    StringUtils::ToCapitalize(subText);
  AppendToUTF32(subText, ((currentStyle & FONT_STYLE_MASK) << 24) | (currentColor << 16), parsedText);
}

void CGUITextLayout::SetMaxHeight(float fHeight)
{
  m_maxHeight = fHeight;
}

void CGUITextLayout::WrapText(const vecText &text, float maxWidth)
{
  if (!m_font)
    return;

  m_lines.clear();

  if (maxWidth < 0)
  {
    CLog::LogF(LOGWARNING, "Cannot wrap the text due to invalid max width value.");
    return;
  }

  if (maxWidth == 0) // Unlimited max width
  {
    LineBreakText(text, m_lines);
    return;
  }

  std::vector<CGUIString> lines;
  LineBreakText(text, lines);

  size_t nMaxLines;
  if (m_maxHeight > 0 && m_font->GetLineHeight() > 0)
    nMaxLines = static_cast<size_t>(std::ceil(m_maxHeight / m_font->GetLineHeight()));
  else
    nMaxLines = std::numeric_limits<size_t>::max();

  // Split lines that exceed the maximum width,
  // lines can be split by last space char or if there are no spaces by character
  // to mimics the line behavior of word processors
  for (const CGUIString& line : lines)
  {
    if (m_lines.size() >= nMaxLines)
      return;

    if (line.m_text.empty()) // Blank line
    {
      m_lines.emplace_back(line);
      continue;
    }

    auto widthOf = [&](const std::vector<character_t>::const_iterator start,
                       const std::vector<character_t>::const_iterator end)
    {
      if (start == end)
        return 0.0f;
      return m_font->GetTextWidth({start, end});
    };

    auto skipLeadingSpaces = [&](vecText::const_iterator& it)
    {
      it = std::find_if_not(it, line.m_text.end(),
                            std::bind_front(&CGUITextLayout::CanWrapAtLetter, this));
    };

    auto current = line.m_text.begin();
    skipLeadingSpaces(current);

    float currentWidth = 0.0f;
    std::vector<character_t>::const_iterator currentStart = current;
    // track end without trailing spaces
    std::vector<character_t>::const_iterator lastNonSpaceInLine = currentStart;

    while (current != line.m_text.end())
    {
      // Find next candidate wrap position
      auto wordEnd = std::find_if(current, line.m_text.end(),
                                  std::bind_front(&CGUITextLayout::CanWrapAtLetter, this));
      const bool hasSpace = (wordEnd != line.m_text.end());
      const float wordWidth = widthOf(current, wordEnd);

      // Try to include word + trailing space
      if (const float spaceWidth = hasSpace ? widthOf(wordEnd, wordEnd + 1) : 0.0f;
          currentWidth + wordWidth + spaceWidth <= maxWidth)
      {
        currentWidth += wordWidth + spaceWidth;
        lastNonSpaceInLine = wordEnd; // exclude trailing space
        current = wordEnd;
        if (hasSpace)
          ++current;
        continue;
      }

      // Try to include word without trailing space
      if (currentWidth + wordWidth <= maxWidth)
      {
        m_lines.emplace_back(currentStart, wordEnd, false);
        if (m_lines.size() >= nMaxLines)
          return;

        current = wordEnd;
        skipLeadingSpaces(current);
        currentStart = current;
        currentWidth = 0.0f;
        lastNonSpaceInLine = currentStart;
        continue;
      }

      // word itself doesn't fit after existing content: wrap before it (trim trailing spaces)
      if (currentWidth > 0.0f)
      {
        const std::vector<character_t>::const_iterator emitEnd =
            lastNonSpaceInLine > currentStart ? lastNonSpaceInLine : wordEnd;
        m_lines.emplace_back(currentStart, emitEnd, false);
        if (m_lines.size() >= nMaxLines)
          return;

        // Start a new line; keep pos at start of the overflowing word
        skipLeadingSpaces(current);
        currentStart = current;
        currentWidth = 0.0f;
        lastNonSpaceInLine = currentStart;
        continue;
      }

      if (current == wordEnd)
        break;

      // current line is empty and word is too long: split by character using a safe linear scan.
      // Do not assume monotonic width because shaping/kerning can make width shrink or grow non-linearly.
      size_t bestCount = 0;
      for (auto it = std::next(current); it < wordEnd; ++it)
      {
        if (m_font->GetTextWidth({current, it}) <= maxWidth)
          bestCount = std::distance(current, it);
      }
      if (bestCount == 0)
        bestCount = 1; // ensure progress even if a single glyph is wider than maxWidth

      const auto cut = current + static_cast<ptrdiff_t>(bestCount);
      m_lines.emplace_back(current, cut, false);
      if (m_lines.size() >= nMaxLines)
        return;

      current = cut;
      skipLeadingSpaces(current);
      currentStart = current;
      currentWidth = 0.0f;
      lastNonSpaceInLine = currentStart;
    }

    // Add remaining text part of this paragraph line
    if (currentStart < line.m_text.end())
      m_lines.emplace_back(currentStart, line.m_text.end(), false);

    // Restore carriage return marker for the end of paragraph
    if (!m_lines.empty())
      m_lines.back().m_carriageReturn = line.m_carriageReturn;
  }
}

void CGUITextLayout::LineBreakText(const vecText &text, std::vector<CGUIString> &lines)
{
  int nMaxLines = (m_maxHeight > 0 && m_font && m_font->GetLineHeight() > 0)?(int)ceilf(m_maxHeight / m_font->GetLineHeight()):-1;
  vecText::const_iterator lineStart = text.begin();
  vecText::const_iterator pos = text.begin();
  while (pos != text.end() && (nMaxLines <= 0 || lines.size() < (size_t)nMaxLines))
  {
    // Get the current letter in the string
    character_t letter = *pos;

    // Handle the newline character
    if ((letter & 0xffff) == L'\n' )
    { // push back everything up till now
      CGUIString string(lineStart, pos, true);
      lines.push_back(string);
      lineStart = pos + 1;
    }
    ++pos;
  }
  // handle the last line if non-empty
  if (lineStart < text.end() && (nMaxLines <= 0 || lines.size() < (size_t)nMaxLines))
  {
    CGUIString string(lineStart, text.end(), true);
    lines.push_back(string);
  }
}

void CGUITextLayout::GetTextExtent(float &width, float &height) const
{
  width = m_textWidth;
  height = m_textHeight;
}

void CGUITextLayout::CalcTextExtent()
{
  m_textWidth = 0;
  m_textHeight = 0;
  if (!m_font) return;

  for (const auto& string : m_lines)
  {
    float w = m_font->GetTextWidth(string.m_text);
    if (w > m_textWidth)
      m_textWidth = w;
  }
  m_textHeight = m_font->GetTextHeight(m_lines.size());
}

unsigned int CGUITextLayout::GetTextLength() const
{
  unsigned int length = 0;
  for (const auto& string : m_lines)
    length += string.m_text.size();
  return length;
}

void CGUITextLayout::GetFirstText(vecText &text) const
{
  text.clear();
  if (!m_lines.empty())
    text = m_lines[0].m_text;
}

float CGUITextLayout::GetTextWidth(const std::wstring &text) const
{
  // NOTE: Assumes a single line of text
  if (!m_font) return 0;
  vecText utf32;
  AppendToUTF32(text, (m_font->GetStyle() & FONT_STYLE_MASK) << 24, utf32);
  return m_font->GetTextWidth(utf32);
}

float CGUITextLayout::GetTextWidth(const vecText& text) const
{
  return m_font->GetTextWidth(text);
}

std::string CGUITextLayout::GetText() const
{
  if (m_lastUpdateW)
  {
    std::string utf8;
    g_charsetConverter.wToUTF8(m_lastText, utf8);
    return utf8;
  }
  return m_lastUtf8Text;
}

void CGUITextLayout::DrawText(CGUIFont* font,
                              float x,
                              float y,
                              KODI::UTILS::COLOR::Color color,
                              KODI::UTILS::COLOR::Color shadowColor,
                              const std::string& text,
                              uint32_t align)
{
  if (!font) return;
  vecText utf32;
  AppendToUTF32(text, 0, utf32);
  font->DrawText(x, y, color, shadowColor, utf32, align, 0);
}

void CGUITextLayout::AppendToUTF32(const std::wstring &utf16, character_t colStyle, vecText &utf32)
{
  // NOTE: Assumes a single line of text
  utf32.reserve(utf32.size() + utf16.size());
  for (unsigned int i = 0; i < utf16.size(); i++)
    utf32.push_back(utf16[i] | colStyle);
}

void CGUITextLayout::AppendToUTF32(const std::string &utf8, character_t colStyle, vecText &utf32)
{
  std::wstring utf16;
  // no need to bidiflip here - it's done in BidiTransform above
  g_charsetConverter.utf8ToW(utf8, utf16, false);
  AppendToUTF32(utf16, colStyle, utf32);
}

void CGUITextLayout::Reset()
{
  m_lines.clear();
  m_lastText.clear();
  m_lastUtf8Text.clear();
  m_textWidth = m_textHeight = 0;
}
