require 'singleton'

# the library class is primarily a namespace for the Library::Version class
# to live in. it contains a few usefull methods used to load libraries which
# have been installed using versioning information.  libraries installed in
# this was will have paths like
#
#   lib/foo.rb.0.0.0
#   lib/foo.rb.2.1.1
# 
# and where
#
#   lib/foo.rb
#
# would be a symbolic link to the latest version of the library.  if one
# wanted only the latest then the normal 'require' or 'load' should suffice,
# but Library.link and Libray.load provide means to specify a particular
# interface needed by your program.

class Library 

  attr_accessor :filenmae, :version

  # not much here presently
  def initialize(filename, version = nil)
    unless version
      @filename, @version = Version.parse(filename)
    else
      @filename, @version = filename, Version.new(version) 
    end
  end

  def inspect
    "#{filename}.#{version}"    
  end
  alias to_s inspect
  alias to_str inspect


  # see README for a detailed explanation of current, revision, and age
  # essentially this class resents a version trio as in '1.2.0'
  class Version
    include Comparable

    @@vpat = %r{(?:(?:[^\.\d]\.)|(?:^\s*))(\d|\d[\.,]\d|\d[\.,]\d[\.,]\d)\s*$}o

    attr_reader :current, :revision, :age

    # versions may be initialized like
    #
    #   Version.new 'foo.rb.1.0.0'
    #   Version.new '1.0.0'
    #   Version.new 1,0,0 
    #
    def initialize current, revision=nil, age=nil
      if String === current
	m = @@vpat.match(current)
	raise "BAD VERSION STRING #{current}" unless m and m[1]
	vs = m[1] 
	@current, @revision, @age = 
	  (vs.split %r{[\.,]}).map{|n| n.to_i}
      elsif Version === current
	@current, @revision, @age = 
	  [:current, :revision, :age].map{|m| current.send m}
      else
	@current, @revision, @age = 
	  [current, revision, age].map{|n| n.to_i}
      end

      @current ||= 0; @revision ||= 0; @age ||= 0

      raise 'REQUIRE : age <= current)' unless 
	@age <= @current
    end

    def inspect
      [current.to_s, revision.to_s, age.to_s].join '.'
    end
    alias to_s inspect
    alias to_str inspect

    # a version is 'larger' than another iff
    # * it's current interface is newer
    # * it's current interface the same, but is a newer revision
    # * it's current interface and revisions are the same, but the age is
    #   larger (eg it supports more interfaces) 
    def <=> version
      v = (String === version ? Version.new(version) : version)
      comp = current <=> v.current
      return comp unless comp == 0 
      comp = revision <=> v.revision
      return comp unless comp == 0 
      comp = age <=> v.age
      return comp unless comp == 0 
      return comp
    end

    # returns wether this version will support another using rules from README
    # eg.
    #     2.1.2 supports 0.1.0
    # since
    #   (2 - 2) <= 0
    def === version 
      v = (String === version ? Version.new(version) : version)
      comp = current <=> v.current
      case comp
	when -1
	  return false
	when 0
	  return true
	when 1
	  return ((current - age) <= v.current)
      end
    end

    alias supports? === 
    alias support? ===

    # uses rules from README to correctly increment version numbers
    # modifying self in place
    def next! implementation_changed = true, 
	      interface_changed = false,  
              backwards_compatible = false

      mod = false

      if interface_changed
	mod = true
	@current += 1
	@revision = 0
	if backwards_compatible
	  @age += 1
	else
	  @age = 0
	end
      else
	if implementation_changed
	  mod = true
	  @revision += 1
	end
      end

      return (mod ? self : nil)
    end

    # uses rules from README to correctly increment version numbers
    def next implementation_changed = true, 
	     interface_changed = false,  
             backwards_compatible = false
      v = Version.new(self)
      v.next!  implementation_changed, 
	       interface_changed,  
	       backwards_compatible
      v
    end

    class << self
      def parse filename
	f = filename.to_s.clone
	v = Version.new(f) 
	f[%r{.#{v}$}] = ''
	[f, v]
      end
    end
  end


  # class which implements logic of searching/loading versioned libraries
  class Loader
    include Singleton

    Entry = Struct.new('Entry', :path, :version)
    
    attr_reader :loaded, :sep, :absolute

    def initialize
      @loaded = {}
      @sep = File::SEPARATOR
      @absolute = %r{^\s*#{sep}}o
    end

    def link filename, interface=nil 
      return false if loaded? (filename, interface)

      self.load filename, interface
    end

    def load filename, interface=nil 
      fn = nil
      v = nil

      if interface
	fn = filename
	v = Version.new(interface)
      else
	fn, v = Version.parse(filename)
      end

      entries = []
      globs(fn).each do |glob|
	Dir[glob].map do |path|
	  entries << Entry.new(path, Version.new(path))
	end
      end

      entries.sort! {|a,b| a.version <=> b.version}
      entries.reverse!

      entries.each do |entry|
	if entry.version === v
	  Kernel::load(entry.path)
	  return (loaded[[filename, interface]] = entry.path)
	end
      end

      msg = "INTERFACE #{interface}\nNOT SUPPORTED BY ANY OF\n"
      entries.each {|e| msg << "\t#{e.path} [#{e.version}\]\n"}
      raise msg 
    end

    def globs filename
      fn = filename.clone
      ext = fn[%r{\.[^\.]+$}o]
      globs = []

      if ext
	globs << "#{fn}.[0-9].[0-9].[0-9]"
      else
	globs << "#{fn}.rb.[0-9].[0-9].[0-9]"
	globs << "#{fn}.so.[0-9].[0-9].[0-9]"
      end

      if fn =~ absolute
	return globs
      else
	return \
	  $LOAD_PATH.collect do |path|
	    globs.map {|glob| path.clone << sep << glob }
	  end.flatten
      end
    end

    def loaded_features
      loaded.values
    end

    def loaded? filename, interface
      loaded[[filename, interface]]
    end
  end

  class << self
    # 'require' a library which supports a certain interface
    # note that, due to a limitation in Kernel::require, this supports only
    # *.rb files for the moment 
    # eg.
    #
    #   Library::link 'yourmodule.rb', 0
    #   Library::link 'yourmodule.rb.0.0.0'
    #   Library::link 'yourmodule.rb', '0.1.0'
    #
    # note that the interface argument, while a version string may be given
    # for, is only used to determine what *interface* is required

    def link filename, interface=nil
      Loader.instance.link (filename, interface)
    end
    alias require link

    # loads a library which supports a certain interface
    # note that, due to a limitation in Kernel::require, this supports only
    # *.rb files for the moment 
    # eg.
    #
    #   Library::load 'yourmodule.rb', 0
    #   Library::load 'yourmodule.rb.0.0.0'
    #   Library::load 'yourmodule.rb', '0.1.0'
    #
    # note that the interface argument, while a version string may be given
    # for, is only used to determine what *interface* is required
    def load filename, interface=nil
      Loader.instance.load (filename, interface)
    end
  end
end
