unless defined? $__lockfile__
  require 'socket'
  require 'timeout'
  require 'fileutils'

  class Lockfile
#{{{
    VERSION = '1.1.0'

    class LockError < StandardError; end
    class StolenLockError < LockError; end
    class StackingLockError < LockError; end
    class StatLockError < LockError; end
    class MaxTriesLockError < LockError; end
    class TimeoutLockError < LockError; end
    class NFSLockError < LockError; end
    class UnLockError < LockError; end

    class SleepCycle < Array
#{{{    
      attr :min
      attr :max
      attr :range
      attr :inc
      def initialize min, max, inc
#{{{
        @min, @max, @inc = Float(min), Float(max), Float(inc)
        @range = @max - @min
        raise RangeError, "max < min" if @max < @min
        raise RangeError, "inc > range" if @inc > @range
        s = @min
        push(s) and s += @inc while(s <= @max)
        self[-1] = @max if self[-1] < @max
        reset
#}}}
      end
      def next
#{{{
        ret = self[@idx]
        @idx = ((@idx + 1) % self.size)
        ret
#}}}
      end
      def reset
#{{{
        @idx = 0
#}}}
      end
#}}}      
    end

    HOSTNAME = Socket::gethostname

    RETRIES        = nil    # maximum number of attempts
    TIMEOUT        = nil    # the longest we will try
    MAX_AGE        = 1024   # lockfiles older than this are stale
    SLEEP_INC      = 2      # sleep cycle is this much longer each time
    MIN_SLEEP      = 2      # shortest sleep time
    MAX_SLEEP      = 32     # longest sleep time
    SUSPEND        = 64     # iff we steal a lock wait this long before we go on
    REFRESH        = 8      # how often we touch/validate the lock
    DONT_CLEAN     = false  # iff we leave lock files lying around
    POLL_RETRIES   = 16     # this many polls makes one 'try'
    POLL_MAX_SLEEP = 0.08   # the longest we'll sleep between polls

    DEBUG = ENV['LOCKFILE_DEBUG'] || false

    class << self
#{{{
      attr :retries, true
      attr :max_age, true
      attr :sleep_inc, true
      attr :min_sleep, true
      attr :max_sleep, true
      attr :suspend, true
      attr :timeout, true
      attr :refresh, true
      attr :debug, true
      attr :dont_clean, true
      attr :poll_retries, true
      attr :poll_max_sleep, true
      def init
#{{{
        @retries = RETRIES
        @max_age = MAX_AGE
        @sleep_inc = SLEEP_INC
        @min_sleep = MIN_SLEEP
        @max_sleep = MAX_SLEEP
        @suspend = SUSPEND
        @timeout = TIMEOUT
        @refresh = REFRESH
        @debug = DEBUG 
        @dont_clean = DONT_CLEAN
        @poll_retries = POLL_RETRIES
        @poll_max_sleep = POLL_MAX_SLEEP

        STDOUT.sync = true if @debug
        STDERR.sync = true if @debug
#}}}
      end
#}}}
    end
    self.init

    attr :klass
    attr :path
    attr :opts
    attr :locked
    attr :thief
    attr :dirname
    attr :basename
    attr :clean
    attr :retries, true
    attr :max_age, true
    attr :sleep_inc, true
    attr :min_sleep, true
    attr :max_sleep, true
    attr :suspend, true
    attr :refresh, true
    attr :timeout, true
    attr :debug, true
    attr :dont_clean, true
    attr :poll_retries, true
    attr :poll_max_sleep, true

    alias thief? thief
    alias locked? locked
    alias debug? debug

    def initialize(path, opts = {}, &block)
#{{{
      @klass = self.class
      @path = path
      @opts = opts

      @retries = getopt('retries') || @klass.retries
      @max_age = getopt('max_age') || @klass.max_age
      @sleep_inc = getopt('sleep_inc') || @klass.sleep_inc
      @min_sleep = getopt('min_sleep') || @klass.min_sleep
      @max_sleep = getopt('max_sleep') || @klass.max_sleep
      @suspend = getopt('suspend') || @klass.suspend
      @timeout = getopt('timeout') || @klass.timeout
      @refresh = getopt('refresh') || @klass.refresh
      @debug = getopt('debug') || @klass.debug
      @dont_clean = getopt('dont_clean') || @klass.dont_clean
      @poll_retries = getopt('poll_retries') || @klass.poll_retries
      @poll_max_sleep = getopt('poll_max_sleep') || @klass.poll_max_sleep

      @sleep_cycle = SleepCycle.new @min_sleep, @max_sleep, @sleep_inc 

      @clean = @dont_clean ? nil : lambda{File.unlink @path rescue nil}
      @dirname = File.dirname @path
      @basename = File.basename @path
      @thief = false
      @locked = false
      
      lock(&block) if block
#}}}
    end
    def lock
#{{{
      raise StackingLockError, "<#{ @path }> is locked!" if @locked

      ret = nil 

      begin
        @sleep_cycle.reset
        create_tmplock do |f|
          begin
            Timeout::timeout(@timeout) do
              tmp_path = f.path
              tmp_stat = f.lstat
              n_retries = 0
              #sleeptime = @sleep_inc 

              trace{ "attempting to lock <#{ @path }>..." }
              begin
                i = 0
                begin
                  trace{ "polling attempt <#{ i }>..." }
                  File.link tmp_path, @path
                  lock_stat = File.lstat @path
                  raise StatLockError, "stat's do not agree" unless
                    tmp_stat.rdev == lock_stat.rdev and tmp_stat.ino == lock_stat.ino 
                  trace{ "aquired lock <#{ @path }>" }
                  @locked = true
              rescue => e
                i += 1
                unless i >= @poll_retries 
                  t = [rand(@poll_max_sleep), @poll_max_sleep].min
                  trace{ "poll sleep <#{ t }>..." }
                  sleep t
                  retry
                end
                raise
              end

              rescue => e
                n_retries += 1
                trace{ "n_retries <#{ n_retries }>" }
                raise MaxTriesLockError, "surpased retries <#{ @retries }>" if 
                  @retries and n_retries >= @retries 

                valid = validlock?

                case valid
                  when true
                    trace{ "found valid lock" }
                    sleeptime = @sleep_cycle.next 
                    trace{ "sleep <#{ sleeptime }>..." }
                    sleep sleeptime
                  when false
                    trace{ "found invalid lock and removing" }
                    begin
                      File.unlink @path
                      @thief = true
                      warn "<#{ @path }> stolen by <#{ Process.pid }> at <#{ timestamp }>"
                      trace{ "i am a thief!" }
                      trace{ "suspending <#{ @suspend }>" }
                      sleep @suspend
                    rescue Errno::ENOENT
                    end
                  when nil
                    # nothing
                end

                retry
              end # begin
            end # timeout 
          rescue Timeout::Error
            raise TimeoutLockError, "surpassed timeout <#{ @timeout }>"
          end # begin
        end # create_tmplock

        if block_given?
          stolen = false
          refresher = (@refresh ? new_refresher : nil) 
          begin
            begin
              ret = yield @path
            rescue StolenLockError
              stolen = true
              raise
            end
          ensure
            begin
              refresher.kill if refresher and refresher.status
            ensure
              unlock unless stolen
            end
          end
        else
          ObjectSpace.define_finalizer self, @clean if @clean
          ret = self
        end
      rescue Errno::ESTALE, Errno::EIO => e
        raise(NFSLockError, errmsg(e)) 
      end

      return ret
#}}}
    end
    def unlock
#{{{
      raise UnLockError, "<#{ @path }> is not locked!" unless @locked
      begin
        File.unlink @path
        @locked = false
        ObjectSpace.undefine_finalizer self if @clean
      rescue Errno::ENOENT
        @locked = false
        ObjectSpace.undefine_finalizer self if @clean
        raise StolenLockError, @path
      end
#}}}
    end
    def new_refresher
#{{{
      Thread.new(Thread.current, @path, @refresh) do |thread, path, refresh|
        loop do 
          touch path
          trace{"touched <#{ path }> @ <#{ Time.now.to_f }>"}
          begin
            loaded = load_lock_id(IO.read(path))
            trace{"loaded <\n#{ loaded.inspect }\n>"}
            raise unless loaded == @lock_id 
          rescue => e
            trace{errmsg e}
            thread.raise StolenLockError
            Thread.exit
          end
          sleep refresh
        end
      end
#}}}
    end
    def validlock?
#{{{
      if @max_age
        uncache @path rescue nil
        begin
          return((Time.now - File.stat(@path).mtime) < @max_age)
        rescue Errno::ENOENT
          return nil 
        end
      else
        exist = File.exist?(@path)
        return(exist ? true : nil)
      end
#}}}
    end
    def uncache file 
#{{{
      refresh = nil
      begin
        is_a_file = File === file
        path = (is_a_file ? file.path : file.to_s) 
        stat = (is_a_file ? file.stat : File.stat(file.to_s)) 
        refresh = tmpnam(File.dirname(path))
        File.link path, refresh
        File.chmod stat.mode, path
        File.utime stat.atime, stat.mtime, path
      ensure 
        begin
          File.unlink refresh if refresh
        rescue Errno::ENOENT
        end
      end
#}}}
    end
    def create_tmplock
#{{{
      tmplock = tmpnam @dirname
      begin
        create(tmplock) do |f|
          @lock_id = gen_lock_id
          dumped = dump_lock_id
          trace{"lock_id <\n#{ @lock_id.inspect }\n>"}
          f.write dumped 
          f.flush
          yield f
        end
      ensure
        begin; File.unlink tmplock; rescue Errno::ENOENT; end if tmplock
      end
#}}}
    end
    def gen_lock_id
#{{{
      Hash[
        'host' => "#{ HOSTNAME }",
        'pid' => "#{ Process.pid }",
        'ppid' => "#{ Process.ppid }",
        'time' => timestamp, 
      ]
#}}}
    end
    def timestamp
#{{{
      time = Time.now
      usec = time.usec.to_s
      usec << '0' while usec.size < 6
      "#{ time.strftime('%Y-%m-%d %H:%M:%S') }.#{ usec }"
#}}}
    end
    def dump_lock_id lock_id = @lock_id
#{{{
      "host: %s\npid: %s\nppid: %s\ntime: %s\n" %
        lock_id.values_at('host','pid','ppid','time')
#}}}
    end
    def load_lock_id buf 
#{{{
      lock_id = {}
      kv = %r/([^:]+):(.*)/o
      buf.each do |line|
        m = kv.match line
        k, v = m[1], m[2]
        next unless m and k and v 
        lock_id[k.strip] = v.strip
      end
      lock_id
#}}}
    end
    def tmpnam dir, seed = File.basename($0)
#{{{
      pid = Process.pid
      time = Time.now
      sec = time.to_i
      usec = time.usec
      "%s%s.%s_%d_%s_%d_%d_%d" % 
        [dir, File::SEPARATOR, HOSTNAME, pid, seed, sec, usec, rand(sec)]
#}}}
    end
    def create path
#{{{
      umask = nil 
      f = nil
      begin
        umask = File.umask 022
        f = open path, File::WRONLY|File::CREAT|File::EXCL, 0644
      ensure
        File.umask umask if umask
      end
      return(block_given? ? begin; yield f; ensure; f.close; end : f)
#}}}
    end
    def touch path 
#{{{
      FileUtils.touch path
#}}}
    end
    def getopt key
#{{{
      @opts[key] || @opts[key.to_s] || @opts[key.to_s.intern]
#}}}
    end
    def to_str
#{{{
      @path
#}}}
    end
    alias to_s to_str
    def trace s = nil 
#{{{
      STDERR.puts((s ? s : yield)) if @debug
#}}}
    end
    def errmsg e
#{{{
      "%s: %s\n%s\n" % [e.class, e.message, e.backtrace.join("\n")]
#}}}
    end
#}}}
  end
  $__lockfile__ == __FILE__ 
end
