# Copyright (C) 2004-2025  Sutou Kouhei <kou@cozmixng.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

require "forwardable"
require "digest/sha2"

require "gdk_pixbuf2"

require_relative "../image-data-loader"
require_relative "../properties"
require_relative "../utils"

module Rabbit
  module ImageManipulable
    class Base
      extend ModuleLoader

      class << self
        def delegate
          extend Forwardable

          def_delegators(:@delegated_loader,
                         :draw,
                         :ensure_resize,
                         :height,
                         :internal_pixbuf,
                         :keep_ratio,
                         :keep_ratio=,
                         :keep_ratio?,
                         :original_height,
                         :original_width,
                         :pixbuf,
                         :resize,
                         :update_size,
                         :width)
        end
      end

      attr_reader :filename
      attr_reader :properties
      attr_reader :original_width
      attr_reader :original_height
      attr_reader :animation

      def initialize(filename, props, canvas: nil)
        @filename = filename
        @properties = Properties.new(props)
        @canvas = canvas
        initialize_keep_ratio
        @animation = nil
        @animation_iterator = nil
        @animation_timeout = nil
        update_size
        @original_width = @width
        @original_height = @height
      end

      def [](key)
        @properties[key]
      end

      def []=(key, value)
        @properties[key] = value
      end

      def keep_ratio?
        @properties.keep_ratio
      end
      # For backward compatibility
      alias_method :keep_ratio, :keep_ratio?

      def keep_ratio=(value)
        @properties.keep_ratio = value
      end

      def pixbuf
        @pixbuf
      end

      def width
        (relative_clip_width&.resolve(@width) || @width) -
          (relative_clip_x&.resolve(@width) || 0)
      end

      def height
        (relative_clip_height&.resolve(@height) || @height) -
          (relative_clip_y&.resolve(@height) || 0)
      end

      def relative_clip_x
        @properties.get_relative_size("relative_clip_x", @filename)
      end

      def relative_clip_y
        @properties.get_relative_size("relative_clip_y", @filename)
      end

      def relative_clip_width
        @properties.get_relative_size("relative_clip_width", @filename)
      end

      def relative_clip_height
        @properties.get_relative_size("relative_clip_height", @filename)
      end

      def resize(w, h)
        return if w.nil? and h.nil?

        if keep_ratio?
          if w and h.nil?
            h = (original_height * w.to_f / original_width).ceil
          elsif w.nil? and h
            w = (original_width * h.to_f / original_height).ceil
          end
        else
          w ||= width
          h ||= height
        end
        w = w.ceil if w
        h = h.ceil if h
        if w and w > 0 and h and h > 0 and [w, h] != [width, height]
          @width = w
          @height = h
        end
      end

      def draw(canvas, x, y, params={})
        default_params = default_draw_params(x, y)
        target_pixbuf = pixbuf
        if @animation_iterator
          @animation_iterator.advance
          target_pixbuf = @animation_iterator.pixbuf
          update_animation_timeout(canvas)
        end
        canvas.draw_pixbuf(target_pixbuf, x, y, default_params.merge(params))
      end

      private
      def initialize_keep_ratio
        return unless @properties["keep_ratio"].nil?
        # For backward compatibility
        keep_scale = @properties["keep_scale"]
        if keep_scale.nil?
          @properties["keep_ratio"] = true
        else
          @properties["keep_ratio"] = keep_scale
        end
      end

      def load_data(data)
        loader = ImageDataLoader.new(data)
        begin
          loader.load
        rescue ImageLoadError
          raise ImageLoadError.new("#{@filename}: #{$!.message}")
        end

        @width = loader.width
        @height = loader.height
        @pixbuf = loader.pixbuf
        @animation = loader.animation
        if @animation and not @animation.static_image?
          @animation_iterator = @animation.get_iter
        else
          @animation_iterator = nil
        end
        if @animation_timeout
          GLib::Source.remove(@animation_timeout)
          @animation_timeout = nil
        end
      end

      def update_animation_timeout(canvas)
        delay_time = @animation_iterator.delay_time
        if delay_time > 0 and @animation_timeout.nil?
          @animation_timeout = GLib::Timeout.add(delay_time) do
            canvas.redraw
            @animation_timeout = nil
            # update_animation_timeout(canvas)
            GLib::Source::REMOVE
          end
        end
      end

      def default_draw_params(x, y)
        _relative_clip_x = relative_clip_x
        _relative_clip_y = relative_clip_y
        _relative_clip_width = relative_clip_width
        _relative_clip_height = relative_clip_height
        if _relative_clip_x or
           _relative_clip_y or
           _relative_clip_width or
           _relative_clip_height
          clip_x = _relative_clip_x&.resolve(@width) || 0
          clip_y = _relative_clip_y&.resolve(@height) || 0
          clip_width = _relative_clip_width&.resolve(@width) || @width
          clip_height = _relative_clip_height&.resolve(@height) || @height
          uncliped_width = width - (clip_width - clip_x) + @width
          uncliped_height = height - (clip_height - clip_y) + @height
          {
            width: uncliped_width,
            height: uncliped_height,
            clip_x: x + clip_x,
            clip_y: y + clip_y,
            clip_width: clip_width,
            clip_height: clip_height,
          }
        else
          {
            width: width,
            height: height,
          }
        end
      end

      # TODO: Move to more suitable location
      def cache_processed_data(canvas, input, extension)
        tmp_dir_name = canvas&.tmp_dir_name
        return yield unless tmp_dir_name
        hash = compute_hash(input)
        cached_path = File.join(tmp_dir_name, "#{hash}.#{extension}")
        unless File.exist?(cached_path)
          processed_path = yield
          FileUtils.cp(processed_path, cached_path)
        end
        cached_path
      end

      def compute_hash(input)
        digest = Digest::SHA2.new
        add_data = lambda do |data|
          case data
          when Array
            data.each do |element|
              add_data.call(element)
            end
          when Hash
            data.each do |key, value|
              add_data.call(key)
              add_data.call(value)
            end
          when String
            digest << data
          when IO
            loop do
              chunk = data.read(4096)
              break if chunk.nil?
              digest << chunk
            end
          end
        end
        add_data.call(input)
        digest.hexdigest
      end
    end
  end
end
