begin
  require 'posixlock'
  HAVE_POSIXLOCK = true
rescue LoadError => e
  HAVE_POSIXLOCK = false
  Redwood::log "No posixlock module...fcntl locking is unavailable."
end

require 'lockfile'

module Redwood

  class LockConfigError < StandardError; end
  class LockError < StandardError; end

  class LockManager
    include Singleton

    ## true enables logging, false is quiet
    DEBUG = true

    attr_reader :read_methods, :write_methods

    def initialize rlocks, wlocks
      @read_methods = (rlocks.nil? ? Redwood::DEF_RLOCKS : rlocks)
      @write_methods = (wlocks.nil? ? Redwood::DEF_WLOCKS : wlocks)

      Redwood::log "LockManager initialized with r:#{@read_methods.join(',')}, w:#{@write_methods.join(',')}" if DEBUG

      validate_methods

      @held_locks = Hash.new({ }) #filehandle and dotlock reference

      ## used to protect the file_mutexes hash
      @mutex = Mutex.new
      ## use a mutex per file to hopefully keep more threads runnable
      @file_mutexes = Hash.new { |hash,key| hash[key] = Mutex.new }

      self.class.i_am_the_instance self
    end

    def dolocked(filename, lmode, &block)
      mode = (lmode.eql?(:write) ? File::LOCK_EX : File::LOCK_SH)

      ##a quick wrapper around the potential creation of a new mutex
      ##variable will hopefully make threaded access to multiple files
      ##more expedient since multiple files can be going through the
      ##gauntlet with their own mutex
      fm = @mutex.synchronize do
        @file_mutexes[filename]
      end

      fm.synchronize do
        @held_locks[filename][:handle] = File.open(filename, rw_flag(mode))

        self.instance_variable_get("@#{lmode}_methods".to_sym).each do |meth|
          self.send lockfuncname(meth).to_sym, filename, mode
        end

        yield @held_locks[filename][:handle]

        self.instance_variable_get("@#{lmode}_methods".to_sym).each do |meth|
          self.send lockfuncname(meth, true).to_sym, filename, File::LOCK_UN
        end
      end

    end

    private

    def lock_dot filename, mode
      Redwood::log "Acquiring dot lock on #{filename}" if DEBUG
      (@held_locks[filename][:dot] = Lockfile.new(filename + '.lock')).lock
    end

    def unlock_dot filename, mode
      Redwood::log "Relinquishing dot lock on #{filename}" if DEBUG
      @held_locks[filename].delete(:dot).unlock
    end

    if HAVE_POSIXLOCK
      def lock_fcntl filename, mode
        Redwood::log "Acquiring fcntl lock on #{filename}" if DEBUG
        @held_locks[filename][:handle].posixlock mode
      end

      def unlock_fcntl filename, mode
        Redwood::log "Relinquishing fcntl lock on #{filename}" if DEBUG
        @held_locks[filename][:handle].posixlock mode
      end
    end

    def lock_flock filename, mode
      Redwood::log "Acquiring flock lock on #{filename}" if DEBUG
      @held_locks[filename][:handle].flock mode
    end

    def unlock_flock filename, mode
      Redwood::log "Relinquishing flock lock on #{filename}" if DEBUG
      @held_locks[filename][:handle].flock mode
    end

    ## some utility methods
    def validate_methods
      (@read_methods + @write_methods).uniq.each do |m|
        raise LockConfigError.new, "Invalid locking method '#{m}' in configuration." unless LockManager.private_instance_methods.include? lockfuncname(m).to_s
      end
    end

    def lockfuncname meth, unlock = false
      (unlock ? "unlock" : "lock") + "_#{meth}"
    end

    def rw_flag mode
      case mode
      when File::LOCK_EX then 'a+'
      when File::LOCK_SH then 'r'
      end
    end
  end
end
