require "danbooru_image_resizer/danbooru_image_resizer"
require "tmpdir"

class Upload < ApplicationRecord
  class Error < Exception ; end

  attr_accessor :file, :image_width, :image_height, :file_ext, :md5, 
    :file_size, :as_pending, :artist_commentary_title, 
    :artist_commentary_desc, :include_artist_commentary,
    :referer_url, :downloaded_source, :replaced_post
  belongs_to :uploader, :class_name => "User"
  belongs_to :post
  before_validation :initialize_uploader, :on => :create
  before_validation :initialize_status, :on => :create
  before_create :convert_cgi_file
  after_destroy :delete_temp_file
  validate :uploader_is_not_limited, :on => :create
  validate :file_or_source_is_present, :on => :create
  validate :rating_given
  attr_accessible :file, :image_width, :image_height, :file_ext, :md5, 
    :file_size, :as_pending, :source, :file_path, :content_type, :rating, 
    :tag_string, :status, :backtrace, :post_id, :md5_confirmation, 
    :parent_id, :server, :artist_commentary_title,
    :artist_commentary_desc, :include_artist_commentary,
    :referer_url, :replaced_post

  module ValidationMethods
    def uploader_is_not_limited
      if !uploader.can_upload?
        self.errors.add(:uploader, uploader.upload_limited_reason)
        return false
      else
        return true
      end
    end

    def file_or_source_is_present
      if file.blank? && source.blank?
        self.errors.add(:base, "Must choose file or specify source")
        return false
      else
        return true
      end
    end

    # Because uploads are processed serially, there's no race condition here.
    def validate_md5_uniqueness
      md5_post = Post.find_by_md5(md5)

      if md5_post && replaced_post
        raise "duplicate: #{md5_post.id}" if replaced_post != md5_post
      elsif md5_post
        raise "duplicate: #{md5_post.id}"
      end
    end

    def validate_file_exists
      unless file_path && File.exists?(file_path)
        raise "file does not exist"
      end
    end

    def validate_file_content_type
      unless is_valid_content_type?
        raise "invalid content type (only JPEG, PNG, GIF, SWF, MP4, and WebM files are allowed)"
      end

      if is_ugoira? && ugoira_service.empty?
        raise "missing frame data for ugoira"
      end
    end

    def validate_md5_confirmation
      if !md5_confirmation.blank? && md5_confirmation != md5
        raise "md5 mismatch"
      end
    end

    def validate_md5_confirmation_after_move
      if !md5_confirmation.blank? && md5_confirmation != Digest::MD5.file(md5_file_path).hexdigest
        raise "md5 mismatch"
      end
    end

    def rating_given
      if rating.present?
        return true
      elsif tag_string =~ /(?:\s|^)rating:([qse])/i
        self.rating = $1.downcase
        return true
      else
        self.errors.add(:base, "Must specify a rating")
        return false
      end
    end

    def tag_audio
      if is_video? && video.audio_channels.present?
        self.tag_string = "#{tag_string} video_with_sound"
      end
    end

    def validate_video_duration
      if is_video? && video.duration > 120
        raise "video must not be longer than 2 minutes"
      end
    end
  end

  module ConversionMethods
    def process_upload
      CurrentUser.scoped(uploader, uploader_ip_addr) do
        update_attribute(:status, "processing")
        self.source = strip_source
        if is_downloadable?
          self.downloaded_source, self.source = download_from_source(temp_file_path)
        end
        validate_file_exists
        self.content_type = file_header_to_content_type(file_path)
        self.file_ext = content_type_to_file_ext(content_type)
        validate_file_content_type
        calculate_hash(file_path)
        validate_md5_uniqueness
        validate_md5_confirmation
        tag_audio
        validate_video_duration
        calculate_file_size(file_path)
        if has_dimensions?
          calculate_dimensions(file_path)
        end
        generate_resizes(file_path)
        move_file
        validate_md5_confirmation_after_move
        save
      end
    end

    def create_post_from_upload
      post = convert_to_post
      post.distribute_files
      if post.save
        create_artist_commentary(post) if include_artist_commentary?
        ugoira_service.save_frame_data(post) if is_ugoira?
        notify_cropper(post)
        update_attributes(:status => "completed", :post_id => post.id)
      else
        update_attribute(:status, "error: " + post.errors.full_messages.join(", "))
      end
    end

    def process!(force = false)
      @tries ||= 0
      return if !force && status =~ /processing|completed|error/

      process_upload
      create_post_from_upload

    rescue Timeout::Error, Net::HTTP::Persistent::Error => x
      if @tries > 3
        update_attributes(:status => "error: #{x.class} - #{x.message}", :backtrace => x.backtrace.join("\n"))
      else
        @tries += 1
        retry
      end

    rescue Exception => x
      update_attributes(:status => "error: #{x.class} - #{x.message}", :backtrace => x.backtrace.join("\n"))
      
    ensure
      delete_temp_file
    end

    def async_conversion?
      is_ugoira?
    end

    def ugoira_service
      @ugoira_service ||= PixivUgoiraService.new
    end

    def convert_to_post
      Post.new.tap do |p|
        p.tag_string = tag_string
        p.md5 = md5
        p.file_ext = file_ext
        p.image_width = image_width
        p.image_height = image_height
        p.rating = rating
        p.source = source
        p.file_size = file_size
        p.uploader_id = uploader_id
        p.uploader_ip_addr = uploader_ip_addr
        p.parent_id = parent_id

        if !uploader.can_upload_free? || upload_as_pending?
          p.is_pending = true
        end
      end
    end

    def notify_cropper(post)
      if ImageCropper.enabled?
        # ImageCropper.notify(post)
      end
    end
  end

  module FileMethods
    def delete_temp_file(path = nil)
      FileUtils.rm_f(path || temp_file_path)
    end

    def move_file
      FileUtils.mv(file_path, md5_file_path)
    end

    def calculate_file_size(source_path)
      self.file_size = File.size(source_path)
    end

    # Calculates the MD5 based on whatever is in temp_file_path
    def calculate_hash(source_path)
      self.md5 = Digest::MD5.file(source_path).hexdigest
    end

    def is_image?
      %w(jpg gif png).include?(file_ext)
    end

    def is_flash?
      %w(swf).include?(file_ext)
    end

    def is_video?
      %w(webm mp4).include?(file_ext)
    end

    def is_ugoira?
      %w(zip).include?(file_ext)
    end
  end

  module ResizerMethods
    def generate_resizes(source_path)
      generate_resize_for(Danbooru.config.small_image_width, Danbooru.config.small_image_width, source_path, 85)

      if is_image? && image_width > Danbooru.config.large_image_width
        generate_resize_for(Danbooru.config.large_image_width, nil, source_path)
      end
    end

    def generate_video_preview_for(width, height, output_path)
      dimension_ratio = image_width.to_f / image_height
      if dimension_ratio > 1
        height = (width / dimension_ratio).to_i
      else
        width = (height * dimension_ratio).to_i
      end
      video.screenshot(output_path, {:seek_time => 0, :resolution => "#{width}x#{height}"})
      FileUtils.chmod(0664, output_path)
    end

    def generate_resize_for(width, height, source_path, quality = 90)
      unless File.exists?(source_path)
        raise Error.new("file not found")
      end

      output_path = resized_file_path_for(width)
      if is_image?
        Danbooru.resize(source_path, output_path, width, height, quality)
      elsif is_ugoira?
        if Delayed::Worker.delay_jobs
          # by the time this runs we'll have moved source_path to md5_file_path
          ugoira_service.generate_resizes(md5_file_path, resized_file_path_for(Danbooru.config.large_image_width), resized_file_path_for(Danbooru.config.small_image_width))
        else
          ugoira_service.generate_resizes(source_path, resized_file_path_for(Danbooru.config.large_image_width), resized_file_path_for(Danbooru.config.small_image_width), false)
        end
      elsif is_video?
        generate_video_preview_for(width, height, output_path)
      end
    end
  end

  module DimensionMethods
    # Figures out the dimensions of the image.
    def calculate_dimensions(file_path)
      if is_video?
        self.image_width = video.width
        self.image_height = video.height
      elsif is_ugoira?
        ugoira_service.calculate_dimensions(file_path)
        self.image_width = ugoira_service.width
        self.image_height = ugoira_service.height
      else
        File.open(file_path, "rb") do |file|
          image_size = ImageSpec.new(file)
          self.image_width = image_size.width
          self.image_height = image_size.height
        end
      end
    end

    # Does this file have image dimensions?
    def has_dimensions?
      %w(jpg gif png swf webm mp4 zip).include?(file_ext)
    end
  end

  module ContentTypeMethods
    def is_valid_content_type?
      file_ext =~ /jpg|gif|png|swf|webm|mp4|zip/
    end

    def content_type_to_file_ext(content_type)
      case content_type
      when "image/jpeg"
        "jpg"

      when "image/gif"
        "gif"

      when "image/png"
        "png"

      when "application/x-shockwave-flash"
        "swf"

      when "video/webm"
        "webm"

      when "video/mp4"
        "mp4"

      when "application/zip"
        "zip"

      else
        "bin"
      end
    end

    def file_header_to_content_type(source_path)
      case File.read(source_path, 16)
      when /^\xff\xd8/n
        "image/jpeg"

      when /^GIF87a/, /^GIF89a/
        "image/gif"

      when /^\x89PNG\r\n\x1a\n/n
        "image/png"

      when /^CWS/, /^FWS/, /^ZWS/
        "application/x-shockwave-flash"

      when /^\x1a\x45\xdf\xa3/n
        "video/webm"

      when /^....ftyp(?:isom|3gp5|mp42|MSNV|avc1)/
        "video/mp4"

      when /^PK\x03\x04/
        "application/zip"

      else
        "application/octet-stream"
      end
    end
  end

  module FilePathMethods
    def md5_file_path
      prefix = Rails.env == "test" ? "test." : ""
      "#{Rails.root}/public/data/#{prefix}#{md5}.#{file_ext}"
    end

    def resized_file_path_for(width)
      prefix = Rails.env == "test" ? "test." : ""

      case width
      when Danbooru.config.small_image_width
        "#{Rails.root}/public/data/preview/#{prefix}#{md5}.jpg"

      when Danbooru.config.large_image_width
        "#{Rails.root}/public/data/sample/#{prefix}#{Danbooru.config.large_image_prefix}#{md5}.#{large_file_ext}"
      end
    end

    def large_file_ext
      if is_ugoira?
        "webm"
      else
        "jpg"
      end
    end

    def temp_file_path
      @temp_file_path ||= File.join(Rails.root, "tmp", "upload_#{Time.now.to_f}.#{Process.pid}")
    end
  end

  module DownloaderMethods
    def strip_source
      source.to_s.strip
    end

    # Determines whether the source is downloadable
    def is_downloadable?
      source =~ /^https?:\/\// && file_path.blank?
    end

    # Downloads the file to destination_path
    def download_from_source(destination_path)
      self.file_path = destination_path
      download = Downloads::File.new(source, destination_path, :referer_url => referer_url)
      download.download!
      ugoira_service.load(download.data)
      [download.downloaded_source, download.source]
    end
  end

  module CgiFileMethods
    def convert_cgi_file
      return if file.blank? || file.size == 0

      self.file_path = temp_file_path

      if file.respond_to?(:tempfile) && file.tempfile
        FileUtils.cp(file.tempfile.path, file_path)
      else
        File.open(file_path, 'wb') do |out|
          out.write(file.read)
        end
      end
      FileUtils.chmod(0664, file_path)
    end
  end

  module StatusMethods
    def initialize_status
      self.status = "pending"
    end

    def is_pending?
      status == "pending"
    end

    def is_processing?
      status == "processing"
    end

    def is_completed?
      status == "completed"
    end

    def is_duplicate?
      status =~ /duplicate/
    end

    def duplicate_post_id
      @duplicate_post_id ||= status[/duplicate: (\d+)/, 1]
    end
  end

  module UploaderMethods
    def initialize_uploader
      self.uploader_id = CurrentUser.user.id
      self.uploader_ip_addr = CurrentUser.ip_addr
    end

    def uploader_name
      User.id_to_name(uploader_id)
    end
  end

  module VideoMethods
    def video
      @video ||= FFMPEG::Movie.new(file_path)
    end
  end

  module SearchMethods
    def uploaded_by(user_id)
      where("uploader_id = ?", user_id)
    end

    def pending
      where(:status => "pending")
    end

    def search(params)
      q = where("true")
      return q if params.blank?

      if params[:uploader_id].present?
        q = q.uploaded_by(params[:uploader_id].to_i)
      end

      if params[:uploader_name].present?
        q = q.where("uploader_id = (select _.id from users _ where lower(_.name) = ?)", params[:uploader_name].mb_chars.downcase)
      end

      if params[:source].present?
        q = q.where("source = ?", params[:source])
      end

      q
    end
  end

  module ApiMethods
    def method_attributes
      super + [:uploader_name]
    end
  end

  module ArtistCommentaryMethods
    def create_artist_commentary(post)
      post.create_artist_commentary(
        :original_title => artist_commentary_title,
        :original_description => artist_commentary_desc
      )
    end
  end

  include ConversionMethods
  include ValidationMethods
  include FileMethods
  include ResizerMethods
  include DimensionMethods
  include ContentTypeMethods
  include DownloaderMethods
  include FilePathMethods
  include CgiFileMethods
  include StatusMethods
  include UploaderMethods
  include VideoMethods
  extend SearchMethods
  include ApiMethods
  include ArtistCommentaryMethods

  def presenter
    @presenter ||= UploadPresenter.new(self)
  end

  def upload_as_pending?
    as_pending == "1"
  end

  def include_artist_commentary?
    include_artist_commentary == "1"
  end
end
