# Example:
# 
# job = Job.enqueue!(MyWorker, :my_method, "my_arg_1", "my_arg_2")
class Job < ActiveRecord::Base

  cattr_accessor :states
  self.states = %w(pending running finished failed)

  serialize :args, Array
  serialize :result

  before_create :setup_state, :setup_priority, :setup_start_at
  validates_presence_of :worker_class, :worker_method
  
  attr_readonly :worker_class, :worker_method, :args
  
  def self.enqueue!(worker_class, worker_method, *args)
    job = create!(
      :worker_class  => worker_class.to_s,
      :worker_method => worker_method.to_s,
      :args          => args
    )

    logger.info("BackgroundFu: Job enqueued. Job(id: #{job.id}, worker: #{worker_class}, method: #{worker_method}, argc: #{args.size}).")
    
    job
  end

  # Invoked by a background daemon.
  def get_done!
    initialize_worker
    invoke_worker
  rescue Exception => e
    rescue_worker(e)
  ensure
    ensure_worker
  end
  
  # Restart a failed job.
  def restart!
    if failed? 
      update_attributes!(
        :result     => nil, 
        :progress   => nil, 
        :started_at => nil, 
        :state      => "pending"
      )
      logger.info("BackgroundFu: Job restarted. Job(id: #{id}).")
    end
  end
  
  def initialize_worker
    update_attributes!(:started_at => Time.now, :state => "running")
    @worker = worker_class.constantize.new
    logger.info("BackgroundFu: Job initialized. Job(id: #{id}).")
  end
  
  def invoke_worker
    self.result = @worker.send(worker_method, *args)
    self.state  = "finished"
    logger.info("BackgroundFu: Job finished. Job(id: #{id}).")
  end
  
  def rescue_worker(exception)
    self.result = [exception.message, exception.backtrace.join("\n")].join("\n\n")
    self.state  = "failed"
    logger.info("BackgroundFu: Job failed. Job(id: #{id}).")
  end
  
  def ensure_worker
    self.progress = @worker.instance_variable_get("@progress")
    save!
  rescue StaleObjectError
    # Ignore this exception as its only purpose is
    # not allowing multiple daemons execute the same job.
    logger.info("BackgroundFu: Race condition handled (It's OK). Job(id: #{id}).")
  end

  # Delete finished jobs that are more than a week old.
  def self.cleanup_finished_jobs
    logger.info "BackgroundFu: Cleaning up finished jobs."
    Job.destroy_all(["state='finished' and updated_at < ?", 1.week.ago])
  end
  
  def self.generate_state_helpers
    states.each do |state_name|
      define_method("#{state_name}?") do
        state == state_name
      end

      # Job.running => array of running jobs, etc.
      self.class.send(:define_method, state_name) do
        find_all_by_state(state_name, :order => "id desc")
      end
    end
  end
  generate_state_helpers

  def setup_state
    return unless state.blank?

    self.state = "pending" 
  end
  
  # Default priority is 0. Jobs will be executed in descending priority order (negative priorities allowed).
  def setup_priority
    return unless priority.blank?
    
    self.priority = 0
  end
  
  # Job will be executed after this timestamp.
  def setup_start_at
    return unless start_at.blank?
    
    self.start_at = Time.now
  end

end  
