require_dependency "file_helper"
require_dependency "url_helper"
require_dependency "db_helper"
require_dependency "file_store/local_store"

class OptimizedImage < ActiveRecord::Base
  belongs_to :upload

  # BUMP UP if optimized image algorithm changes
  VERSION = 1

  def self.lock(upload_id, width, height)
    @hostname ||= `hostname`.strip rescue "unknown"
    # note, the extra lock here ensures we only optimize one image per machine
    # this can very easily lead to runaway CPU so slowing it down is beneficial
    DistributedMutex.synchronize("optimized_image_host_#{@hostname}") do
      DistributedMutex.synchronize("optimized_image_#{upload_id}_#{width}_#{height}") do
        yield
      end
    end
  end

  def self.create_for(upload, width, height, opts = {})
    return unless width > 0 && height > 0
    return if upload.try(:sha1).blank?

    lock(upload.id, width, height) do
      # do we already have that thumbnail?
      thumbnail = find_by(upload_id: upload.id, width: width, height: height)

      # make sure we have an url
      if thumbnail && thumbnail.url.blank?
        thumbnail.destroy
        thumbnail = nil
      end

      # return the previous thumbnail if any
      return thumbnail unless thumbnail.nil?

      # create the thumbnail otherwise
      original_path = Discourse.store.path_for(upload)
      if original_path.blank?
        external_copy = Discourse.store.download(upload) rescue nil
        original_path = external_copy.try(:path)
      end

      if original_path.blank?
        Rails.logger.error("Could not find file in the store located at url: #{upload.url}")
      else
        # create a temp file with the same extension as the original
        extension = File.extname(original_path)
        temp_file = Tempfile.new(["discourse-thumbnail", extension])
        temp_path = temp_file.path

        if extension =~ /\.svg$/i
          FileUtils.cp(original_path, temp_path)
          resized = true
        elsif opts[:crop]
          resized = crop(original_path, temp_path, width, height, opts)
        else
          resized = resize(original_path, temp_path, width, height, opts)
        end

        if resized
          thumbnail = OptimizedImage.create!(
            upload_id: upload.id,
            sha1: Upload.generate_digest(temp_path),
            extension: extension,
            width: width,
            height: height,
            url: "",
          )
          # store the optimized image and update its url
          File.open(temp_path) do |file|
            url = Discourse.store.store_optimized_image(file, thumbnail)
            if url.present?
              thumbnail.url = url
              thumbnail.save
            end
          end
        end

        # close && remove temp file
        temp_file.close!
      end

      # make sure we remove the cached copy from external stores
      if Discourse.store.external?
        external_copy&.close
      end

      thumbnail
    end
  end

  def destroy
    OptimizedImage.transaction do
      Discourse.store.remove_optimized_image(self)
      super
    end
  end

  def local?
    !(url =~ /^(https?:)?\/\//)
  end

  def self.safe_path?(path)
    # this matches instructions which call #to_s
    path = path.to_s
    return false if path != File.expand_path(path)
    return false if path !~ /\A[\w\-\.\/]+\z/m
    true
  end

  def self.ensure_safe_paths!(*paths)
    paths.each do |path|
      raise Discourse::InvalidAccess unless safe_path?(path)
    end
  end

  def self.thumbnail_or_resize
    SiteSetting.strip_image_metadata ? "thumbnail" : "resize"
  end

  def self.resize_instructions(from, to, dimensions, opts = {})
    ensure_safe_paths!(from, to)

    # NOTE: ORDER is important!
    %W{
      convert
      #{from}[0]
      -auto-orient
      -gravity center
      -background transparent
      -#{thumbnail_or_resize} #{dimensions}^
      -extent #{dimensions}
      -interpolate bicubic
      -unsharp 2x0.5+0.7+0
      -interlace none
      -quality 98
      -profile #{File.join(Rails.root, 'vendor', 'data', 'RT_sRGB.icm')}
      #{to}
    }
  end

  def self.resize_instructions_animated(from, to, dimensions, opts = {})
    ensure_safe_paths!(from, to)

    %W{
      gifsicle
      --colors=256
      --resize-fit #{dimensions}
      --optimize=3
      --output #{to}
      #{from}
    }
  end

  def self.crop_instructions(from, to, dimensions, opts = {})
    ensure_safe_paths!(from, to)

    %W{
      convert
      #{from}[0]
      -auto-orient
      -gravity north
      -background transparent
      -#{thumbnail_or_resize} #{opts[:width]}
      -crop #{dimensions}+0+0
      -unsharp 2x0.5+0.7+0
      -interlace none
      -quality 98
      -profile #{File.join(Rails.root, 'vendor', 'data', 'RT_sRGB.icm')}
      #{to}
    }
  end

  def self.crop_instructions_animated(from, to, dimensions, opts = {})
    ensure_safe_paths!(from, to)

    %W{
      gifsicle
      --crop 0,0+#{dimensions}
      --colors=256
      --optimize=3
      --output #{to}
      #{from}
    }
  end

  def self.downsize_instructions(from, to, dimensions, opts = {})
    ensure_safe_paths!(from, to)

    %W{
      convert
      #{from}[0]
      -auto-orient
      -gravity center
      -background transparent
      -interlace none
      -resize #{dimensions}
      -profile #{File.join(Rails.root, 'vendor', 'data', 'RT_sRGB.icm')}
      #{to}
    }
  end

  def self.downsize_instructions_animated(from, to, dimensions, opts = {})
    resize_instructions_animated(from, to, dimensions, opts)
  end

  def self.resize(from, to, width, height, opts = {})
    optimize("resize", from, to, "#{width}x#{height}", opts)
  end

  def self.crop(from, to, width, height, opts = {})
    opts[:width] = width
    optimize("crop", from, to, "#{width}x#{height}", opts)
  end

  def self.downsize(from, to, dimensions, opts = {})
    optimize("downsize", from, to, dimensions, opts)
  end

  def self.optimize(operation, from, to, dimensions, opts = {})
    method_name = "#{operation}_instructions"
    if !!opts[:allow_animation] && (from =~ /\.GIF$/i || opts[:filename] =~ /\.GIF$/i)
      method_name += "_animated"
    end
    instructions = self.send(method_name.to_sym, from, to, dimensions, opts)
    convert_with(instructions, to)
  end

  def self.convert_with(instructions, to)
    begin
      Discourse::Utils.execute_command(*instructions)
    rescue
      return false
    end

    FileHelper.optimize_image!(to)
    true
  rescue
    Rails.logger.error("Could not optimize image: #{to}")
    false
  end

  def self.migrate_to_new_scheme(limit = nil)
    problems = []

    if SiteSetting.migrate_to_new_scheme
      max_file_size_kb = SiteSetting.max_image_size_kb.kilobytes
      local_store = FileStore::LocalStore.new

      scope = OptimizedImage.includes(:upload)
        .where("url NOT LIKE '%/optimized/_X/%'")
        .order(id: :desc)

      scope.limit(limit) if limit

      scope.each do |optimized_image|
        begin
          # keep track of the url
          previous_url = optimized_image.url.dup
          # where is the file currently stored?
          external = previous_url =~ /^\/\//
          # download if external
          if external
            url = SiteSetting.scheme + ":" + previous_url
            file = FileHelper.download(
              url,
              max_file_size: max_file_size_kb,
              tmp_file_name: "discourse",
              follow_redirect: true
            ) rescue nil
            path = file.path
          else
            path = local_store.path_for(optimized_image)
            file = File.open(path)
          end
          # compute SHA if missing
          if optimized_image.sha1.blank?
            optimized_image.sha1 = Upload.generate_digest(path)
          end
          # optimize if image
          FileHelper.optimize_image!(path)
          # store to new location & update the filesize
          File.open(path) do |f|
            optimized_image.url = Discourse.store.store_optimized_image(f, optimized_image)
            optimized_image.save
          end
          # remap the URLs
          DbHelper.remap(UrlHelper.absolute(previous_url), optimized_image.url) unless external
          DbHelper.remap(previous_url, optimized_image.url)
          # remove the old file (when local)
          unless external
            FileUtils.rm(path, force: true)
          end
        rescue => e
          problems << { optimized_image: optimized_image, ex: e }
          # just ditch the optimized image if there was any errors
          optimized_image.destroy
        ensure
          file&.unlink
          file&.close
        end
      end
    end

    problems
  end

end

# == Schema Information
#
# Table name: optimized_images
#
#  id        :integer          not null, primary key
#  sha1      :string(40)       not null
#  extension :string(10)       not null
#  width     :integer          not null
#  height    :integer          not null
#  upload_id :integer          not null
#  url       :string           not null
#
# Indexes
#
#  index_optimized_images_on_upload_id                       (upload_id)
#  index_optimized_images_on_upload_id_and_width_and_height  (upload_id,width,height) UNIQUE
#