// Aseprite UI Library
// Copyright (C) 2019-2024  Igara Studio S.A.
// Copyright (C) 2001-2018  David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "ui/graphics.h"

#include "base/string.h"
#include "gfx/clip.h"
#include "gfx/matrix.h"
#include "gfx/path.h"
#include "gfx/point.h"
#include "gfx/rect.h"
#include "gfx/region.h"
#include "gfx/size.h"
#include "os/draw_text.h"
#include "os/font.h"
#include "os/sampling.h"
#include "os/surface.h"
#include "os/system.h"
#include "os/window.h"
#include "ui/display.h"
#include "ui/scale.h"
#include "ui/theme.h"

#include <algorithm>
#include <cctype>
#include <string>

namespace ui {

Graphics::Graphics(Display* display, const os::SurfaceRef& surface, int dx, int dy)
  : m_display(display)
  , m_surface(surface)
  , m_dx(dx)
  , m_dy(dy)
{
}

Graphics::~Graphics()
{
  // If we were drawing in the screen surface, we mark these regions
  // as dirty for the final flip.
  if (m_display)
    m_display->dirtyRect(m_dirtyBounds);
}

int Graphics::width() const
{
  return m_surface->width();
}

int Graphics::height() const
{
  return m_surface->height();
}

int Graphics::getSaveCount() const
{
  return m_surface->getSaveCount();
}

gfx::Rect Graphics::getClipBounds() const
{
  return m_surface->getClipBounds().offset(-m_dx, -m_dy);
}

void Graphics::saveClip()
{
  m_surface->saveClip();
}

void Graphics::restoreClip()
{
  m_surface->restoreClip();
}

bool Graphics::clipRect(const gfx::Rect& rc)
{
  return m_surface->clipRect(gfx::Rect(rc).offset(m_dx, m_dy));
}

void Graphics::save()
{
  m_surface->save();
}

void Graphics::concat(const gfx::Matrix& matrix)
{
  m_surface->concat(matrix);
}

void Graphics::setMatrix(const gfx::Matrix& matrix)
{
  m_surface->setMatrix(matrix);
}

void Graphics::resetMatrix()
{
  m_surface->resetMatrix();
}

void Graphics::restore()
{
  m_surface->restore();
}

gfx::Matrix Graphics::matrix() const
{
  return m_surface->matrix();
}

gfx::Color Graphics::getPixel(int x, int y)
{
  os::SurfaceLock lock(m_surface.get());
  return m_surface->getPixel(m_dx+x, m_dy+y);
}

void Graphics::putPixel(gfx::Color color, int x, int y)
{
  dirty(gfx::Rect(m_dx+x, m_dy+y, 1, 1));

  os::SurfaceLock lock(m_surface.get());
  m_surface->putPixel(color, m_dx+x, m_dy+y);
}

void Graphics::drawHLine(int x, int y, int w, const Paint& paint)
{
  dirty(gfx::Rect(m_dx+x, m_dy+y, w, 1));

  os::SurfaceLock lock(m_surface.get());
  m_surface->drawRect(gfx::Rect(m_dx+x, m_dy+y, w, 1), paint);
}

void Graphics::drawHLine(gfx::Color color, int x, int y, int w)
{
  dirty(gfx::Rect(m_dx+x, m_dy+y, w, 1));

  os::SurfaceLock lock(m_surface.get());
  os::Paint paint;
  paint.color(color);
  m_surface->drawRect(gfx::Rect(m_dx+x, m_dy+y, w, 1), paint);
}

void Graphics::drawVLine(int x, int y, int h, const Paint& paint)
{
  dirty(gfx::Rect(m_dx+x, m_dy+y, 1, h));

  os::SurfaceLock lock(m_surface.get());
  m_surface->drawRect(gfx::Rect(m_dx+x, m_dy+y, 1, h), paint);
}

void Graphics::drawVLine(gfx::Color color, int x, int y, int h)
{
  dirty(gfx::Rect(m_dx+x, m_dy+y, 1, h));

  os::SurfaceLock lock(m_surface.get());
  os::Paint paint;
  paint.color(color);
  m_surface->drawRect(gfx::Rect(m_dx+x, m_dy+y, 1, h), paint);
}

void Graphics::drawLine(gfx::Color color, const gfx::Point& _a, const gfx::Point& _b)
{
  gfx::Point a(m_dx+_a.x, m_dy+_a.y);
  gfx::Point b(m_dx+_b.x, m_dy+_b.y);
  dirty(gfx::Rect(a, b));

  os::SurfaceLock lock(m_surface.get());
  os::Paint paint;
  paint.color(color);
  m_surface->drawLine(a, b, paint);
}

void Graphics::drawPath(gfx::Path& path, const Paint& paint)
{
  os::SurfaceLock lock(m_surface.get());

  auto m = matrix();
  save();
  setMatrix(gfx::Matrix::MakeTrans(m_dx, m_dy));
  concat(m);

  m_surface->drawPath(path, paint);

  dirty(matrix().mapRect(path.bounds()).inflate(1, 1));
  restore();
}

void Graphics::drawRect(const gfx::Rect& rcOrig, const Paint& paint)
{
  gfx::Rect rc(rcOrig);
  rc.offset(m_dx, m_dy);
  dirty(rc);

  os::SurfaceLock lock(m_surface.get());
  m_surface->drawRect(rc, paint);
}

void Graphics::drawRect(gfx::Color color, const gfx::Rect& rcOrig)
{
  gfx::Rect rc(rcOrig);
  rc.offset(m_dx, m_dy);
  dirty(rc);

  os::SurfaceLock lock(m_surface.get());
  os::Paint paint;
  paint.color(color);
  paint.style(os::Paint::Stroke);
  m_surface->drawRect(rc, paint);
}

void Graphics::fillRect(gfx::Color color, const gfx::Rect& rcOrig)
{
  gfx::Rect rc(rcOrig);
  rc.offset(m_dx, m_dy);
  dirty(rc);

  os::SurfaceLock lock(m_surface.get());
  os::Paint paint;
  paint.color(color);
  paint.style(os::Paint::Fill);
  m_surface->drawRect(rc, paint);
}

void Graphics::fillRegion(gfx::Color color, const gfx::Region& rgn)
{
  for (gfx::Region::iterator it=rgn.begin(), end=rgn.end(); it!=end; ++it)
    fillRect(color, *it);
}

void Graphics::fillAreaBetweenRects(gfx::Color color,
  const gfx::Rect& outer, const gfx::Rect& inner)
{
  if (!outer.intersects(inner))
    fillRect(color, outer);
  else {
    gfx::Region rgn(outer);
    rgn.createSubtraction(rgn, gfx::Region(inner));
    fillRegion(color, rgn);
  }
}

void Graphics::drawSurface(os::Surface* surface, int x, int y)
{
  dirty(gfx::Rect(m_dx+x, m_dy+y, surface->width(), surface->height()));

  os::SurfaceLock lockSrc(surface);
  os::SurfaceLock lockDst(m_surface.get());
  m_surface->drawSurface(surface, m_dx+x, m_dy+y);
}

void Graphics::drawSurface(os::Surface* surface,
                           const gfx::Rect& srcRect,
                           const gfx::Rect& dstRect,
                           const os::Sampling& sampling,
                           const ui::Paint* paint)
{
  dirty(gfx::Rect(m_dx+dstRect.x, m_dy+dstRect.y,
                  dstRect.w, dstRect.h));

  os::SurfaceLock lockSrc(surface);
  os::SurfaceLock lockDst(m_surface.get());
  m_surface->drawSurface(
    surface,
    srcRect,
    gfx::Rect(dstRect).offset(m_dx, m_dy),
    sampling,
    paint);
}

void Graphics::drawRgbaSurface(os::Surface* surface, int x, int y)
{
  dirty(gfx::Rect(m_dx+x, m_dy+y, surface->width(), surface->height()));

  os::SurfaceLock lockSrc(surface);
  os::SurfaceLock lockDst(m_surface.get());
  m_surface->drawRgbaSurface(surface, m_dx+x, m_dy+y);
}

void Graphics::drawRgbaSurface(os::Surface* surface, int srcx, int srcy, int dstx, int dsty, int w, int h)
{
  dirty(gfx::Rect(m_dx+dstx, m_dy+dsty, w, h));

  os::SurfaceLock lockSrc(surface);
  os::SurfaceLock lockDst(m_surface.get());
  m_surface->drawRgbaSurface(surface, srcx, srcy, m_dx+dstx, m_dy+dsty, w, h);
}

void Graphics::drawColoredRgbaSurface(os::Surface* surface, gfx::Color color, int x, int y)
{
  dirty(gfx::Rect(m_dx+x, m_dy+y, surface->width(), surface->height()));

  os::SurfaceLock lockSrc(surface);
  os::SurfaceLock lockDst(m_surface.get());
  m_surface->drawColoredRgbaSurface(surface, color, gfx::ColorNone,
    gfx::Clip(m_dx+x, m_dy+y, 0, 0, surface->width(), surface->height()));
}

void Graphics::drawColoredRgbaSurface(os::Surface* surface, gfx::Color color,
                                      int srcx, int srcy, int dstx, int dsty, int w, int h)
{
  dirty(gfx::Rect(m_dx+dstx, m_dy+dsty, w, h));

  os::SurfaceLock lockSrc(surface);
  os::SurfaceLock lockDst(m_surface.get());
  m_surface->drawColoredRgbaSurface(surface, color, gfx::ColorNone,
    gfx::Clip(m_dx+dstx, m_dy+dsty, srcx, srcy, w, h));
}

void Graphics::drawSurfaceNine(os::Surface* surface,
                               const gfx::Rect& src,
                               const gfx::Rect& center,
                               const gfx::Rect& dst,
                               const bool drawCenter,
                               const ui::Paint* paint)
{
  gfx::Rect displacedDst(m_dx+dst.x, m_dy+dst.y, dst.w, dst.h);
  dirty(displacedDst);

  os::SurfaceLock lockSrc(surface);
  os::SurfaceLock lockDst(m_surface.get());
  m_surface->drawSurfaceNine(surface, src, center, displacedDst, drawCenter, paint);
}

void Graphics::setFont(const os::FontRef& font)
{
  m_font = font;
}

void Graphics::drawText(const std::string& str,
                        gfx::Color fg, gfx::Color bg,
                        const gfx::Point& origPt,
                        os::DrawTextDelegate* delegate)
{
  gfx::Point pt(m_dx+origPt.x, m_dy+origPt.y);

  os::SurfaceLock lock(m_surface.get());
  gfx::Rect textBounds =
    os::draw_text(m_surface.get(), m_font.get(), str, fg, bg, pt.x, pt.y, delegate);

  dirty(gfx::Rect(pt.x, pt.y, textBounds.w, textBounds.h));
}

namespace {

class DrawUITextDelegate : public os::DrawTextDelegate {
public:
  DrawUITextDelegate(os::Surface* surface,
                     os::Font* font, const int mnemonic)
    : m_surface(surface)
    , m_font(font)
    , m_mnemonic(std::tolower(mnemonic))
    , m_underscoreColor(gfx::ColorNone) {
  }

  gfx::Rect bounds() const { return m_bounds; }

  void preProcessChar(const int index,
                      const int codepoint,
                      gfx::Color& fg,
                      gfx::Color& bg,
                      const gfx::Rect& charBounds) override {
    if (m_surface) {
      if (m_mnemonic &&
          // TODO use ICU library to lower unicode chars
          std::tolower(codepoint) == m_mnemonic) {
        m_underscoreColor = fg;
        m_mnemonic = 0;         // Just one time
      }
      else {
        m_underscoreColor = gfx::ColorNone;
      }
    }
  }

  bool preDrawChar(const gfx::Rect& charBounds) override {
    m_bounds |= charBounds;
    return true;
  }

  void postDrawChar(const gfx::Rect& charBounds) override {
    if (!gfx::is_transparent(m_underscoreColor)) {
      // TODO underscore height = guiscale() should be configurable from ui::Theme
      int dy = 0;
      if (m_font->type() == os::FontType::FreeType) // TODO use other method to locate the underline
        dy += guiscale();
      gfx::Rect underscoreBounds(charBounds.x, charBounds.y+charBounds.h+dy,
                                 charBounds.w, guiscale());

      os::Paint paint;
      paint.color(m_underscoreColor);
      paint.style(os::Paint::Fill);
      m_surface->drawRect(underscoreBounds, paint);

      m_bounds |= underscoreBounds;
    }
  }

private:
  os::Surface* m_surface;
  os::Font* m_font;
  int m_mnemonic;
  gfx::Color m_underscoreColor;
  gfx::Rect m_bounds;
};

}

void Graphics::drawUIText(const std::string& str, gfx::Color fg, gfx::Color bg,
                          const gfx::Point& pt, const int mnemonic)
{
  os::SurfaceLock lock(m_surface.get());
  int x = m_dx+pt.x;
  int y = m_dy+pt.y;

  DrawUITextDelegate delegate(m_surface.get(), m_font.get(), mnemonic);
  os::draw_text(m_surface.get(), m_font.get(), str,
                fg, bg, x, y, &delegate);

  dirty(delegate.bounds());
}

void Graphics::drawAlignedUIText(const std::string& str, gfx::Color fg, gfx::Color bg,
                                 const gfx::Rect& rc, const int align)
{
  doUIStringAlgorithm(str, fg, bg, rc, align, true);
}

gfx::Size Graphics::measureUIText(const std::string& str)
{
  return gfx::Size(
    Graphics::measureUITextLength(str, m_font.get()),
    m_font->height());
}

// static
int Graphics::measureUITextLength(const std::string& str, os::Font* font)
{
  DrawUITextDelegate delegate(nullptr, font, 0);
  os::draw_text(nullptr, font, str,
                gfx::ColorNone, gfx::ColorNone, 0, 0,
                &delegate);
  return delegate.bounds().w;
}

gfx::Size Graphics::fitString(const std::string& str, int maxWidth, int align)
{
  return doUIStringAlgorithm(str, gfx::ColorNone, gfx::ColorNone,
                             gfx::Rect(0, 0, maxWidth, 0), align, false);
}

gfx::Size Graphics::doUIStringAlgorithm(const std::string& str, gfx::Color fg, gfx::Color bg, const gfx::Rect& rc, int align, bool draw)
{
  gfx::Point pt(0, rc.y);

  if ((align & (MIDDLE | BOTTOM)) != 0) {
    gfx::Size preSize = doUIStringAlgorithm(str, gfx::ColorNone, gfx::ColorNone, rc, 0, false);
    if (align & MIDDLE)
      pt.y = rc.y + rc.h/2 - preSize.h/2;
    else if (align & BOTTOM)
      pt.y = rc.y + rc.h - preSize.h;
  }

  gfx::Size calculatedSize(0, 0);
  std::size_t beg, end, newBeg;
  std::string line;
  int lineSeparation = 2*guiscale();

  // Draw line-by-line
  for (beg=end=0; end != std::string::npos && beg<str.size(); ) {
    pt.x = rc.x;

    // Without word-wrap
    if ((align & (WORDWRAP | CHARWRAP)) == 0) {
      end = str.find('\n', beg);
      if (end != std::string::npos)
        newBeg = end+1;
      else
        newBeg = std::string::npos;
    }
    // With char-wrap
    else if ((align & CHARWRAP) == CHARWRAP) {
      for (end=beg+1; end<str.size(); ++end) {
        // If we are out of the available width (rc.w) using the new "end"
        if ((rc.w > 0) &&
            (m_font->textLength(str.substr(beg, end-beg).c_str()) > rc.w)) {
          if (end > beg+1)
            --end;
          break;
        }
      }
      newBeg = end;
    }
    // With word-wrap
    else {
      std::size_t old_end = std::string::npos;
      for (std::size_t new_word_beg=beg;;) {
        end = str.find_first_of(" \n", new_word_beg);

        // If we have already a word to print (old_end != npos), and
        // we are out of the available width (rc.w) using the new "end"
        if ((old_end != std::string::npos) &&
            (rc.w > 0) &&
            (pt.x+m_font->textLength(str.substr(beg, end-beg).c_str()) > rc.w)) {
          // We go back to the "old_end" and paint from "beg" to "end"
          end = old_end;
          break;
        }
        // If we have more words to print...
        else if (end != std::string::npos) {
          // Force line break, now we have to paint from "beg" to "end"
          if (str[end] == '\n')
            break;

          // White-space, this is a beginning of a new word.
          new_word_beg = end+1;
        }
        // We are in the end of text
        else
          break;

        old_end = end;
      }
      newBeg = end+1;
    }

    // Get the entire line to be painted
    if (end != std::string::npos)
      line = str.substr(beg, end-beg);
    else
      line = str.substr(beg);

    gfx::Size lineSize(
      m_font->textLength(line.c_str()),
      m_font->height()+lineSeparation);
    calculatedSize.w = std::max(calculatedSize.w, lineSize.w);

    // Render the text
    if (draw) {
      int xout;
      if ((align & CENTER) == CENTER)
        xout = pt.x + rc.w/2 - lineSize.w/2;
      else if ((align & RIGHT) == RIGHT)
        xout = pt.x + rc.w - lineSize.w;
      else
        xout = pt.x;

      drawText(line, fg, bg, gfx::Point(xout, pt.y));

      if (!gfx::is_transparent(bg))
        fillAreaBetweenRects(bg,
          gfx::Rect(rc.x, pt.y, rc.w, lineSize.h),
          gfx::Rect(xout, pt.y, lineSize.w, lineSize.h));
    }

    pt.y += lineSize.h;
    calculatedSize.h += lineSize.h;
    beg = newBeg;

    if (pt.y+lineSize.h >= rc.y2())
      break;
  }

  if (calculatedSize.h > 0)
    calculatedSize.h -= lineSeparation;

  // Fill bottom area
  if (draw && !gfx::is_transparent(bg)) {
    if (pt.y < rc.y+rc.h)
      fillRect(bg, gfx::Rect(rc.x, pt.y, rc.w, rc.y+rc.h-pt.y));
  }

  return calculatedSize;
}

void Graphics::invalidate(const gfx::Rect& bounds)
{
  dirty(gfx::Rect(bounds).offset(m_dx, m_dy));
}

void Graphics::dirty(const gfx::Rect& bounds)
{
  gfx::Rect rc = m_surface->getClipBounds();
  rc = rc.createIntersection(bounds);
  if (!rc.isEmpty())
    m_dirtyBounds |= rc;
}

//////////////////////////////////////////////////////////////////////
// ScreenGraphics

ScreenGraphics::ScreenGraphics(Display* display)
  : Graphics(display, AddRef(display->surface()), 0, 0)
{
  setFont(AddRef(get_theme()->getDefaultFont()));
}

ScreenGraphics::~ScreenGraphics()
{
}

} // namespace ui
