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


  class Lockfile
#{{{
    VERSION = '0.0.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

    HOSTNAME = Socket.gethostname
    RETRIES = 16
    MAX_AGE = nil 
    SLEEP_INC = 4
    MAX_SLEEP = 64
    SUSPEND = 16 
    TIMEOUT = 512
    DEBUG = ENV['LOCKFILE_DEBUG']
    REFRESH = nil 
    DONT_CLEAN = false

    class << self
#{{{
      attr :retries, true
      attr :max_age, true
      attr :sleep_inc, true
      attr :max_sleep, true
      attr :suspend, true
      attr :timeout, true
      attr :refresh, true
      attr :debug, true
      attr :dont_clean, true
      def init
#{{{
        @retries = RETRIES
        @max_age = MAX_AGE
        @sleep_inc = SLEEP_INC
        @max_sleep = MAX_SLEEP
        @suspend = SUSPEND
        @timeout = TIMEOUT
        @refresh = REFRESH
        @debug = DEBUG 
        @dont_clean = DONT_CLEAN
#}}}
      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 :max_sleep, true
    attr :suspend, true
    attr :refresh, true
    attr :timeout, true
    attr :debug, true
    attr :dont_clean, true

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

    def initialize path, opts = {}
#{{{
      @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
      @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
      @clean = @dont_clean ? nil : lambda{File.unlink @path rescue nil}
      @dirname = File.dirname @path
      @basename = File.basename @path
      @thief = false
      @locked = false
#}}}
    end
    def lock
#{{{
      raise StackingLockError, "<#{ @path }> is locked!" if @locked

      ret = nil 

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

              begin
                trace{ "attempting to lock <#{ @path }>" }
                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
                n_retries += 1
                trace{ "n_retries <#{ n_retries }>" }
                raise MaxTriesLockError, "surpased retries <#{ @retries }>" if 
                  @retries > 0 and n_retries >= @retries 

                valid = validlock? @path

                case valid
                  when true
                    trace{ "valid lock" }
                    trace{ "sleep <#{ sleeptime }>..." }
                    sleep sleeptime
                    sleeptime += @sleep_inc if @max_sleep.nil? or sleeptime < @max_sleep
                    sleeptime = @max_sleep if @max_sleep and sleeptime > @max_sleep
                  when false
                    trace{ "invalid lock" }
                    begin
                      File.unlink @path
                      @thief = true
                      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 = nil 
          begin
            refresher = new_refresher
            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
        raise NFSLockError, "nfs failure"
      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 = Thread.current
#{{{
      if @refresh
        Thread.new(thread, @path, @refresh, @max_age) do |thread, path, refresh, max_age|
          loop do 
            unless File.exist? path
              thread.raise StolenLockError
              Thread.exit
            end
            touch path
            trace{"touched <#{ path }> @ <#{ Time.now.to_f }>"}
            sleep refresh
          end
        end
      else
        nil
      end
#}}}
    end
    def validlock? path
#{{{
      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|
          f.puts lock_id
          f.flush
          yield f
        end
      ensure
        begin; File.unlink tmplock; rescue Errno::ENOENT; end if tmplock
      end
#}}}
    end
    def lock_id
#{{{
      time = Time.now
      "%s:\n  pid: %d\n  ppid: %d\n  time: %s.%d" % 
        [HOSTNAME, Process.pid, Process.ppid, time.strftime('%Y-%m-%d %H:%M:%S'), time.usec]
#}}}
    end
    def tmpnam dir, seed = File.basename($0)
#{{{
      time = Time.now
      sec = time.to_i
      usec = time.usec
      pid = Process.pid
      "%s%s.%s_%d_%s_%d%d%d.lk" % 
        [dir, File::SEPARATOR, HOSTNAME, pid, seed, (sec & 0xf), (usec & 0xf), 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 file 
#{{{
      is_a_file = File === file
      path = (is_a_file ? file.path : file.to_s) 
      unless test ?e, path
        open(path,'w'){|f|}
      else
        now = Time.now
        File.utime now, now, path
      end
#}}}
    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
#}}}
  end
  $__lockfile__ == __FILE__ 
end
