#!/usr/bin/env ruby
# frozen_string_literal: true

ENV["RAILS_ENV"] ||= "test"

require_relative "../config/environment"

GlobalSetting.define_singleton_method(:enable_vips_image_processing) { true }

require "base64"
require "chunky_png"
require "fileutils"
require "letter_avatar"
require "optparse"

# Compare representative A-Z renders with Linux baselines and highlight pixel drift.
module LetterAvatarPixelDiff
  LETTERS = ("A".."Z").to_a.freeze
  AVATAR_SIZE = 45
  BASELINE_DIRECTORY = Rails.root.join("spec/fixtures/images/letter_avatar_visual/linux")
  REPORT_DIRECTORY = Rails.root.join("tmp/letter-avatar-pixel-diff")

  class << self
    def run(arguments)
      options = parse_options(arguments)
      generated_paths = {}

      if RbConfig::CONFIG["host_os"].match?(/darwin/i)
        warn "LetterAvatar pixel baselines are Linux-only."
        return 2
      end

      LETTERS.each do |letter|
        generated_paths[letter] = LetterAvatar.generate(letter, AVATAR_SIZE, cache: false)
      end

      if options[:update_baselines]
        update_baselines(generated_paths)
        puts "Updated Linux A-Z baselines in #{BASELINE_DIRECTORY}."
        return 0
      end

      missing_baselines =
        LETTERS.reject { |letter| BASELINE_DIRECTORY.join("#{letter}.png").exist? }
      if missing_baselines.any?
        warn(
          "Missing Linux baselines for #{missing_baselines.join(", ")}. " \
            "Run `script/letter_avatar_pixel_diff --update-baselines`.",
        )
        return 2
      end

      comparisons = compare(generated_paths)
      write_report(comparisons)
      comparisons.each do |comparison|
        changed_pixel_count = comparison[:changed_pixel_count]
        pixel_count = comparison[:pixel_count]
        changed_percentage = pixel_count.zero? ? 0 : (changed_pixel_count.fdiv(pixel_count) * 100)
        puts(
          "#{comparison[:letter]}: #{changed_pixel_count}/#{pixel_count} pixels differ " \
            "(#{format("%.2f", changed_percentage)}%)",
        )
      end

      if comparisons.any? { |comparison| comparison[:changed_pixel_count].positive? }
        warn "LetterAvatar pixels changed. Open #{REPORT_DIRECTORY.join("index.html")}."
        1
      else
        puts "All Linux A-Z pixel baselines match. Report: #{REPORT_DIRECTORY.join("index.html")}"
        0
      end
    rescue OptionParser::ParseError => error
      warn error.message
      2
    ensure
      remove_generated_avatars(generated_paths)
    end

    private

    def parse_options(arguments)
      options = { update_baselines: false }
      OptionParser
        .new do |parser|
          parser.banner = "Usage: script/letter_avatar_pixel_diff [--update-baselines]"
          parser.on("--update-baselines", "Regenerate every committed Linux A-Z baseline.") do
            options[:update_baselines] = true
          end
        end
        .parse!(arguments)
      options
    end

    def update_baselines(generated_paths)
      FileUtils.mkdir_p(BASELINE_DIRECTORY)
      generated_paths.each do |letter, generated_path|
        FileUtils.cp(generated_path, BASELINE_DIRECTORY.join("#{letter}.png"))
      end
    end

    def compare(generated_paths)
      generated_paths.map do |letter, generated_path|
        expected = ChunkyPNG::Image.from_file(BASELINE_DIRECTORY.join("#{letter}.png"))
        actual = ChunkyPNG::Image.from_file(generated_path)
        changed_pixel_count, pixel_count = changed_pixel_count(expected, actual)

        {
          letter: letter,
          expected: expected,
          actual: actual,
          changed_pixel_count: changed_pixel_count,
          pixel_count: pixel_count,
        }
      end
    end

    def changed_pixel_count(expected, actual)
      width = [expected.width, actual.width].max
      height = [expected.height, actual.height].max
      count =
        height.times.sum do |y_position|
          width.times.count do |x_position|
            expected_pixel = expected[x_position, y_position] if x_position < expected.width &&
              y_position < expected.height
            actual_pixel = actual[x_position, y_position] if x_position < actual.width &&
              y_position < actual.height

            expected_pixel != actual_pixel
          end
        end

      [count, width * height]
    end

    def write_report(comparisons)
      FileUtils.mkdir_p(REPORT_DIRECTORY)
      sections = comparisons.map { |comparison| write_comparison(comparison) }
      report = <<~HTML
        <!doctype html>
        <html lang="en">
          <head>
            <meta charset="utf-8">
            <meta name="viewport" content="width=device-width, initial-scale=1">
            <title>LetterAvatar pixel diff</title>
            <style>
              body { color: #222; font: 16px/1.5 sans-serif; margin: 2rem; }
              section { border-top: 1px solid #ccc; margin-top: 2rem; }
              .images { display: flex; flex-wrap: wrap; gap: 2rem; }
              figure { margin: 0; }
              img { image-rendering: pixelated; width: 180px; height: 180px; }
            </style>
          </head>
          <body>
            <h1>LetterAvatar pixel diff</h1>
            <p>Baseline: Linux</p>
            #{sections.join}
          </body>
        </html>
      HTML
      File.write(REPORT_DIRECTORY.join("index.html"), report)
    end

    def write_comparison(comparison)
      letter = comparison[:letter]
      expected = comparison[:expected]
      actual = comparison[:actual]
      diff = build_diff(expected, actual)
      expected_path = REPORT_DIRECTORY.join("#{letter}-expected.png")
      actual_path = REPORT_DIRECTORY.join("#{letter}-actual.png")
      diff_path = REPORT_DIRECTORY.join("#{letter}-diff.png")
      expected.save(expected_path)
      actual.save(actual_path)
      diff.save(diff_path)
      expected_data = Base64.strict_encode64(File.binread(expected_path))
      actual_data = Base64.strict_encode64(File.binread(actual_path))
      diff_data = Base64.strict_encode64(File.binread(diff_path))
      changed_pixel_count = comparison[:changed_pixel_count]
      pixel_count = comparison[:pixel_count]
      changed_percentage = pixel_count.zero? ? 0 : (changed_pixel_count.fdiv(pixel_count) * 100)

      <<~HTML
        <section>
          <h2>#{letter}: #{changed_pixel_count}/#{pixel_count} pixels differ (#{format("%.2f", changed_percentage)}%)</h2>
          <div class="images">
            <figure><figcaption>Expected</figcaption><img src="data:image/png;base64,#{expected_data}" alt="Expected #{letter}"></figure>
            <figure><figcaption>Actual</figcaption><img src="data:image/png;base64,#{actual_data}" alt="Actual #{letter}"></figure>
            <figure><figcaption>Diff (changed pixels are magenta)</figcaption><img src="data:image/png;base64,#{diff_data}" alt="Pixel diff for #{letter}"></figure>
          </div>
        </section>
      HTML
    end

    def build_diff(expected, actual)
      width = [expected.width, actual.width].max
      height = [expected.height, actual.height].max
      diff = ChunkyPNG::Image.new(width, height, ChunkyPNG::Color::TRANSPARENT)

      height.times do |y_position|
        width.times do |x_position|
          expected_pixel = expected[x_position, y_position] if x_position < expected.width &&
            y_position < expected.height
          actual_pixel = actual[x_position, y_position] if x_position < actual.width &&
            y_position < actual.height

          diff[x_position, y_position] = if expected_pixel.nil? && actual_pixel.nil?
            ChunkyPNG::Color::TRANSPARENT
          elsif expected_pixel == actual_pixel
            dim(actual_pixel)
          else
            ChunkyPNG::Color.rgba(255, 0, 255, 255)
          end
        end
      end

      diff
    end

    def dim(pixel)
      ChunkyPNG::Color.rgba(
        ChunkyPNG::Color.r(pixel) / 4,
        ChunkyPNG::Color.g(pixel) / 4,
        ChunkyPNG::Color.b(pixel) / 4,
        ChunkyPNG::Color.a(pixel),
      )
    end

    def remove_generated_avatars(generated_paths)
      generated_paths&.each do |letter, generated_path|
        FileUtils.rm_f(generated_path)
        FileUtils.rm_f(LetterAvatar.fullsize_path(LetterAvatar::Identity.from_username(letter)))
      end
    end
  end
end

exit LetterAvatarPixelDiff.run(ARGV)
