# typed: strict
class Domain::PostFile::Thumbnail < ReduxApplicationRecord
  extend T::Sig
  self.table_name = "domain_post_file_thumbnails"

  belongs_to :post_file,
             class_name: "::Domain::PostFile",
             inverse_of: :thumbnails,
             touch: true

  enum :thumb_type, { content_container: 0, size_32_32: 1 }
  validates :thumb_type, presence: true
  validates :frame, presence: true
  validate :validate_content_type!

  THUMB_ROOT_DIR = T.let(File.join(BlobFile::ROOT_DIR, "thumbnails"), String)

  THUMB_FILE_PATH_PATTERN = T.let([2, 2, 1].freeze, T::Array[Integer])

  SINGLE_FRAME_CONTENT_TYPES =
    T.let(
      [%r{image/jpeg}, %r{image/jpg}, %r{image/png}, %r{image/bmp}],
      T::Array[Regexp],
    )

  MULTI_FRAME_CONTENT_TYPES =
    T.let(
      [
        %r{image/gif},
        %r{image/apng},
        %r{image/webp},
        %r{video/webm},
        %r{video/mp4},
      ],
      T::Array[Regexp],
    )

  THUMBABLE_CONTENT_TYPES =
    T.let(
      SINGLE_FRAME_CONTENT_TYPES + MULTI_FRAME_CONTENT_TYPES,
      T::Array[Regexp],
    )

  THUMB_TYPE_TO_OPTIONS =
    T.let(
      {
        content_container:
          LoadedMedia::ThumbnailOptions.new(
            width: 768,
            height: 2048,
            quality: 95,
            size: :down,
            interlace: true,
            for_frames: [0],
          ),
        size_32_32:
          LoadedMedia::ThumbnailOptions.new(
            width: 32,
            height: 32,
            quality: 95,
            size: :force,
            interlace: false,
            for_frames: [0.0, 0.1, 0.5, 0.9, 1.0],
          ),
      },
      T::Hash[Symbol, LoadedMedia::ThumbnailOptions],
    )

  sig { returns(T.nilable(String)) }
  def absolute_file_path
    return nil unless (thumb_type = self.thumb_type)
    return nil unless (post_file = self.post_file)
    return nil unless (sha256 = post_file.sha256)
    sha256_hex = HexUtil.bin2hex(sha256)
    [
      THUMB_ROOT_DIR,
      thumb_type.to_s,
      *BlobFile.path_segments(THUMB_FILE_PATH_PATTERN, sha256_hex),
    ].join("/") + "-#{frame.to_i.to_s.rjust(2, "0")}.jpg"
  end

  sig { params(post_file: Domain::PostFile).returns(T::Boolean) }
  def self.can_thumbnail_post_file?(post_file)
    return false unless content_type = post_file.content_type
    can_thumbnail_content_type?(content_type)
  end

  sig { params(content_type: String).returns(T::Boolean) }
  def self.can_thumbnail_content_type?(content_type)
    THUMBABLE_CONTENT_TYPES.any? { |type| content_type.match?(type) }
  end

  sig do
    params(post_file: Domain::PostFile).returns(
      T::Array[Domain::PostFile::Thumbnail],
    )
  end
  def self.create_for_post_file!(post_file)
    logger.tagged("thumbnail", make_arg_tag(post_file)) do
      return [] unless content_type = post_file.content_type
      return [] unless can_thumbnail_content_type?(content_type)
      blob_file_path = post_file.blob&.absolute_file_path
      return [] unless blob_file_path

      media = LoadedMedia.from_file(content_type, blob_file_path)
      return [] unless media

      num_frames = media.num_frames
      logger.info(format_tags(make_tag("num_frames", num_frames)))
      return [] if num_frames.zero?

      existing_thumb_types = post_file.thumbnails.to_a.map(&:thumb_type).uniq
      logger.info(
        format_tags(make_tag("existing_thumb_types", existing_thumb_types)),
      )

      FileUtils.mkdir_p(BlobFile::TMP_DIR)
      thumbnails = []
      THUMB_TYPE_TO_OPTIONS.each do |thumb_type, options|
        thumb_type = thumb_type.to_s
        logger.tagged(make_tag("thumb_type", thumb_type)) do
          next if existing_thumb_types.include?(thumb_type)
          logger.info(format_tags("creating thumbnail"))

          # get the number of frames in the post file
          frames_to_thumbnail =
            options
              .for_frames
              .map { |frame_fraction| (frame_fraction * (num_frames - 1)).to_i }
              .uniq
              .sort

          frames_to_thumbnail.each do |frame|
            logger.tagged(make_tag("frame", frame)) do
              thumbnail = post_file.thumbnails.build(thumb_type:, frame:)
              unless thumb_file_path = thumbnail.absolute_file_path
                logger.info(format_tags("unable to compute thumbnail path"))
                next
              end

              if File.exist?(thumb_file_path)
                logger.info(format_tags("thumbnail file already exists"))
              else
                media.write_frame_thumbnail(frame, thumb_file_path, options)
              end

              thumbnail.save!
              thumbnails << thumbnail
            end
          end
        end
      end
      thumbnails
    end
  end

  private

  sig { void }
  def validate_content_type!
    return unless (post_file = self.post_file)
    content_type = post_file.content_type
    if content_type.nil?
      errors.add(:post_file, "must have a content type")
      return
    end

    unless self.class.can_thumbnail_content_type?(content_type)
      errors.add(:post_file, "must be a thumbnailable content type")
    end
  end
end
