#include "media/trial_watermark.hpp"

#include "core/preview_frame.hpp"

#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <string_view>

#ifdef SMARTCUT_HAS_FFMPEG
extern "C" {
#include <libavutil/frame.h>
#include <libavutil/pixdesc.h>
}
#endif

namespace smartcut::media {
namespace {

using Glyph = std::array<std::uint8_t, 7>;

Glyph glyph(char value) {
  switch (value) {
    case 'A': return {0x0e, 0x11, 0x11, 0x1f, 0x11, 0x11, 0x11};
    case 'C': return {0x0e, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0e};
    case 'E': return {0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x1f};
    case 'I': return {0x1f, 0x04, 0x04, 0x04, 0x04, 0x04, 0x1f};
    case 'L': return {0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1f};
    case 'M': return {0x11, 0x1b, 0x15, 0x15, 0x11, 0x11, 0x11};
    case 'N': return {0x11, 0x19, 0x15, 0x13, 0x11, 0x11, 0x11};
    case 'O': return {0x0e, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e};
    case 'R': return {0x1e, 0x11, 0x11, 0x1e, 0x14, 0x12, 0x11};
    case 'S': return {0x0f, 0x10, 0x10, 0x0e, 0x01, 0x01, 0x1e};
    case 'T': return {0x1f, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04};
    case 'U': return {0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e};
    case 'V': return {0x11, 0x11, 0x11, 0x11, 0x11, 0x0a, 0x04};
    case '4': return {0x02, 0x06, 0x0a, 0x12, 0x1f, 0x02, 0x02};
    case '6': return {0x0e, 0x10, 0x10, 0x1e, 0x11, 0x11, 0x0e};
    default: return {};
  }
}

#ifdef SMARTCUT_HAS_FFMPEG

std::uint64_t component_mask(const AVComponentDescriptor& component) {
  return component.depth >= 63 ? ~std::uint64_t{} : ((std::uint64_t{1} << component.depth) - 1U);
}

bool blend_component(AVFrame* frame, const AVPixFmtDescriptor* descriptor, int component_index,
                     int x, int y, int target_8bit, int opacity_8bit) {
  const auto& component = descriptor->comp[component_index];
  if (component.plane < 0 || component.plane >= AV_NUM_DATA_POINTERS || !frame->data[component.plane] ||
      component.step <= 0 || component.offset < 0 || component.depth <= 0 || component.depth > 16 ||
      component.shift < 0 || component.shift + component.depth > 32)
    return false;
  const int bytes = (component.shift + component.depth + 7) / 8;
  if (bytes <= 0 || bytes > 4 || component.offset + bytes > component.step) return false;
  auto* pixel = frame->data[component.plane] + static_cast<std::ptrdiff_t>(y) * frame->linesize[component.plane] +
                static_cast<std::ptrdiff_t>(x) * component.step + component.offset;
  std::uint64_t raw = 0;
  if (descriptor->flags & AV_PIX_FMT_FLAG_BE) {
    for (int byte = 0; byte < bytes; ++byte) raw = (raw << 8U) | pixel[byte];
  } else {
    for (int byte = 0; byte < bytes; ++byte) raw |= std::uint64_t{pixel[byte]} << (byte * 8U);
  }
  const auto mask = component_mask(component);
  const auto current = (raw >> component.shift) & mask;
  const auto target = static_cast<std::uint64_t>(std::clamp(target_8bit, 0, 255)) * mask / 255U;
  const auto opacity = static_cast<std::uint64_t>(std::clamp(opacity_8bit, 0, 255));
  const auto blended = (current * (255U - opacity) + target * opacity + 127U) / 255U;
  raw = (raw & ~(mask << component.shift)) | ((blended & mask) << component.shift);
  if (descriptor->flags & AV_PIX_FMT_FLAG_BE) {
    for (int byte = bytes - 1; byte >= 0; --byte) {
      pixel[byte] = static_cast<std::uint8_t>(raw & 0xffU);
      raw >>= 8U;
    }
  } else {
    for (int byte = 0; byte < bytes; ++byte) {
      pixel[byte] = static_cast<std::uint8_t>((raw >> (byte * 8U)) & 0xffU);
    }
  }
  return true;
}

bool blend_pixel(AVFrame* frame, const AVPixFmtDescriptor* descriptor, int x, int y,
                 int brightness, int opacity) {
  if (x < 0 || y < 0 || x >= frame->width || y >= frame->height) return true;
  if (descriptor->flags & AV_PIX_FMT_FLAG_RGB) {
    if (descriptor->nb_components < 3) return false;
    for (int component = 0; component < 3; ++component)
      if (!blend_component(frame, descriptor, component, x, y, brightness, opacity)) return false;
    return true;
  }
  // Luma-only drawing remains readable on YUV, greyscale and HDR material and
  // avoids tinting chroma-subsampled formats.
  return descriptor->nb_components >= 1 &&
         blend_component(frame, descriptor, 0, x, y, brightness, opacity);
}

bool fill_rectangle(AVFrame* frame, const AVPixFmtDescriptor* descriptor, int left, int top,
                    int width, int height, int brightness, int opacity) {
  const int right = std::min(frame->width, left + width);
  const int bottom = std::min(frame->height, top + height);
  left = std::max(0, left);
  top = std::max(0, top);
  for (int y = top; y < bottom; ++y)
    for (int x = left; x < right; ++x)
      if (!blend_pixel(frame, descriptor, x, y, brightness, opacity)) return false;
  return true;
}

bool draw_text(AVFrame* frame, const AVPixFmtDescriptor* descriptor, std::string_view text,
               int left, int top, int scale) {
  int cursor = left;
  for (const char character : text) {
    const auto pattern = glyph(character);
    if (character != ' ') {
      for (int row = 0; row < 7; ++row) {
        for (int column = 0; column < 5; ++column) {
          if ((pattern[row] & (1U << (4 - column))) == 0) continue;
          if (!fill_rectangle(frame, descriptor, cursor + column * scale + scale,
                              top + row * scale + scale, scale, scale, 8, 190) ||
              !fill_rectangle(frame, descriptor, cursor + column * scale,
                              top + row * scale, scale, scale, 245, 245))
            return false;
        }
      }
    }
    cursor += 6 * scale;
  }
  return true;
}

bool draw_banner(AVFrame* frame, const AVPixFmtDescriptor* descriptor, int top,
                 std::string_view text) {
  int scale = std::max(1, std::min(frame->width / static_cast<int>(text.size() * 6 + 10),
                                   frame->height / 28));
  scale = std::min(scale, 8);
  const int text_width = static_cast<int>(text.size() * 6 - 1) * scale;
  const int text_height = 7 * scale;
  const int padding = std::max(3, 3 * scale);
  const int left = std::max(0, (frame->width - text_width) / 2);
  top = std::clamp(top, 0, std::max(0, frame->height - text_height - 2 * padding));
  if (!fill_rectangle(frame, descriptor, left - padding, top - padding,
                      text_width + 2 * padding, text_height + 2 * padding, 12, 185) ||
      !draw_text(frame, descriptor, text, left, top, scale))
    return false;
  return true;
}
#endif

bool blend_bgra_pixel(core::PreviewFrame* frame, int x, int y, int brightness,
                      int opacity) {
  if (x < 0 || y < 0 || x >= frame->width || y >= frame->height) return true;
  const auto offset = static_cast<std::size_t>(y) * static_cast<std::size_t>(frame->stride) +
                      static_cast<std::size_t>(x) * 4U;
  if (offset + 3U >= frame->pixels.size()) return false;
  const auto target = static_cast<unsigned>(std::clamp(brightness, 0, 255));
  const auto alpha = static_cast<unsigned>(std::clamp(opacity, 0, 255));
  for (std::size_t component = 0; component < 3; ++component) {
    const auto current = static_cast<unsigned>(frame->pixels[offset + component]);
    frame->pixels[offset + component] = static_cast<std::uint8_t>(
        (current * (255U - alpha) + target * alpha + 127U) / 255U);
  }
  return true;
}

bool fill_rectangle(core::PreviewFrame* frame, int left, int top, int width,
                    int height, int brightness, int opacity) {
  const int right = std::min(frame->width, left + width);
  const int bottom = std::min(frame->height, top + height);
  left = std::max(0, left);
  top = std::max(0, top);
  for (int y = top; y < bottom; ++y)
    for (int x = left; x < right; ++x)
      if (!blend_bgra_pixel(frame, x, y, brightness, opacity)) return false;
  return true;
}

bool draw_text(core::PreviewFrame* frame, std::string_view text, int left,
               int top, int scale) {
  int cursor = left;
  for (const char character : text) {
    const auto pattern = glyph(character);
    if (character != ' ') {
      for (int row = 0; row < 7; ++row) {
        for (int column = 0; column < 5; ++column) {
          if ((pattern[row] & (1U << (4 - column))) == 0) continue;
          if (!fill_rectangle(frame, cursor + column * scale + scale,
                              top + row * scale + scale, scale, scale, 8, 190) ||
              !fill_rectangle(frame, cursor + column * scale,
                              top + row * scale, scale, scale, 245, 245))
            return false;
        }
      }
    }
    cursor += 6 * scale;
  }
  return true;
}

bool draw_banner(core::PreviewFrame* frame, int top, std::string_view text) {
  int scale = std::max(1, std::min(frame->width / static_cast<int>(text.size() * 6 + 10),
                                   frame->height / 28));
  scale = std::min(scale, 8);
  const int text_width = static_cast<int>(text.size() * 6 - 1) * scale;
  const int text_height = 7 * scale;
  const int padding = std::max(3, 3 * scale);
  const int left = std::max(0, (frame->width - text_width) / 2);
  top = std::clamp(top, 0, std::max(0, frame->height - text_height - 2 * padding));
  return fill_rectangle(frame, left - padding, top - padding,
                        text_width + 2 * padding, text_height + 2 * padding, 12, 185) &&
         draw_text(frame, text, left, top, scale);
}

}  // namespace

bool apply_trial_watermark(AVFrame* frame) {
#ifndef SMARTCUT_HAS_FFMPEG
  (void)frame;
  return false;
#else
  if (!frame || frame->width < 32 || frame->height < 24 || av_frame_make_writable(frame) < 0)
    return false;
  const auto* descriptor = av_pix_fmt_desc_get(static_cast<AVPixelFormat>(frame->format));
  if (!descriptor || descriptor->nb_components < 1 ||
      (descriptor->flags & (AV_PIX_FMT_FLAG_HWACCEL | AV_PIX_FMT_FLAG_BITSTREAM |
                            AV_PIX_FMT_FLAG_PAL | AV_PIX_FMT_FLAG_FLOAT)))
    return false;
  const bool compact = frame->width < 256 || frame->height < 120;
  const std::string_view text = compact ? std::string_view{"EVAL"}
                                        : std::string_view{"SMARTCUT64 EVALUATION"};
  const int scale = std::max(1, std::min(frame->width / (compact ? 36 : 140),
                                         frame->height / (compact ? 36 : 70)));
  const int banner_height = 7 * std::min(scale, 8) + 8;
  return draw_banner(frame, descriptor, std::max(4, frame->height / 12), text) &&
         draw_banner(frame, descriptor,
                     std::max(4, frame->height - banner_height - frame->height / 12), text);
#endif
}

bool apply_trial_watermark(core::PreviewFrame* frame) {
  if (!frame || frame->width < 32 || frame->height < 24 || frame->stride < frame->width * 4)
    return false;
  const auto required = static_cast<std::size_t>(frame->stride) *
                        static_cast<std::size_t>(frame->height);
  if (frame->stride <= 0 || frame->height <= 0 || frame->pixels.size() < required)
    return false;
  const bool compact = frame->width < 256 || frame->height < 120;
  const std::string_view text = compact ? std::string_view{"EVAL"}
                                        : std::string_view{"SMARTCUT64 EVALUATION"};
  const int scale = std::max(1, std::min(frame->width / (compact ? 36 : 140),
                                         frame->height / (compact ? 36 : 70)));
  const int banner_height = 7 * std::min(scale, 8) + 8;
  return draw_banner(frame, std::max(4, frame->height / 12), text) &&
         draw_banner(frame,
                     std::max(4, frame->height - banner_height - frame->height / 12), text);
}

}  // namespace smartcut::media
