require 'rdoc/code_objects'
require 'fileutils'

##
# A set of ri data.
#
# The store manages reading and writing ri data for a project (gem, path,
# etc.) and maintains a cache of methods, classes and ancestors in the
# store.

class RDoc::RI::Store

  ##
  # If true this Store will not write any files

  attr_accessor :dry_run

  ##
  # Path this store reads or writes

  attr_accessor :path

  ##
  # Type of ri datastore this was loaded from.  See RDoc::RI::Driver,
  # RDoc::RI::Paths.

  attr_accessor :type

  ##
  # The contents of the Store

  attr_reader :cache

  ##
  # Creates a new Store of +type+ that will load or save to +path+

  def initialize path, type = nil
    @dry_run = false
    @type    = type
    @path    = path

    @cache = {
      :class_methods    => {},
      :instance_methods => {},
      :attributes       => {},
      :modules          => [],
      :ancestors        => {},
    }
  end

  ##
  # Ancestors cache accessor.  Maps a klass name to an Array of its ancestors
  # in this store.  If Foo in this store inherits from Object, Kernel won't be
  # listed (it will be included from ruby's ri store).

  def ancestors
    @cache[:ancestors]
  end

  ##
  # Attributes cache accessor.  Maps a class to an Array of its attributes.

  def attributes
    @cache[:attributes]
  end

  ##
  # Path to the cache file

  def cache_path
    File.join @path, 'cache.ri'
  end

  ##
  # Path to the ri data for +klass_name+

  def class_file klass_name
    name = klass_name.split('::').last
    File.join class_path(klass_name), "cdesc-#{name}.ri"
  end

  ##
  # Class methods cache accessor.  Maps a class to an Array of its class
  # methods (not full name).

  def class_methods
    @cache[:class_methods]
  end

  ##
  # Path where data for +klass_name+ will be stored (methods or class data)

  def class_path klass_name
    File.join @path, *klass_name.split('::')
  end

  ##
  # Friendly rendition of #path

  def friendly_path
    case type
    when :gem    then
      sep = Regexp.union(*['/', File::ALT_SEPARATOR].compact)
      @path =~ /#{sep}doc#{sep}(.*?)#{sep}ri$/
      "gem #{$1}"
    when :home   then '~/.ri'
    when :site   then 'ruby site'
    when :system then 'ruby core'
    else @path
    end
  end

  def inspect # :nodoc:
    "#<%s:0x%x %s %p>" % [self.class, object_id, @path, modules.sort]
  end

  ##
  # Instance methods cache accessor.  Maps a class to an Array of its
  # instance methods (not full name).

  def instance_methods
    @cache[:instance_methods]
  end

  ##
  # Loads cache file for this store

  def load_cache
    open cache_path, 'rb' do |io|
      @cache = Marshal.load io.read
    end
  rescue Errno::ENOENT
  end

  ##
  # Loads ri data for +klass_name+

  def load_class klass_name
    open class_file(klass_name), 'rb' do |io|
      Marshal.load io.read
    end
  end

  ##
  # Loads ri data for +method_name+ in +klass_name+

  def load_method klass_name, method_name
    open method_file(klass_name, method_name), 'rb' do |io|
      Marshal.load io.read
    end
  end

  ##
  # Path to the ri data for +method_name+ in +klass_name+

  def method_file klass_name, method_name
    method_name = method_name.split('::').last
    method_name =~ /#(.*)/
    method_type = $1 ? 'i' : 'c'
    method_name = $1 if $1

    method_name = if ''.respond_to? :ord then
                    method_name.gsub(/\W/) { "%%%02x" % $&[0].ord }
                  else
                    method_name.gsub(/\W/) { "%%%02x" % $&[0] }
                  end

    File.join class_path(klass_name), "#{method_name}-#{method_type}.ri"
  end

  ##
  # Modules cache accessor.  An Array of all the modules (and classes) in the
  # store.

  def modules
    @cache[:modules]
  end

  ##
  # Writes the cache file for this store

  def save_cache
    # HACK mongrel-1.1.5 documents its files twice
    @cache[:ancestors].       each do |_, m| m.uniq!; m.sort! end
    @cache[:attributes].      each do |_, m| m.uniq!; m.sort! end
    @cache[:class_methods].   each do |_, m| m.uniq!; m.sort! end
    @cache[:instance_methods].each do |_, m| m.uniq!; m.sort! end
    @cache[:modules].uniq!; @cache[:modules].sort!

    return if @dry_run

    open cache_path, 'wb' do |io|
      Marshal.dump @cache, io
    end
  end

  ##
  # Writes the ri data for +klass+

  def save_class klass
    FileUtils.mkdir_p class_path(klass.full_name) unless @dry_run

    @cache[:modules] << klass.full_name

    path = class_file klass.full_name

    begin
      disk_klass = nil

      open path, 'rb' do |io|
        disk_klass = Marshal.load io.read
      end

      klass.merge disk_klass
    rescue Errno::ENOENT
    end

    # BasicObject has no ancestors
    ancestors = klass.ancestors.compact.map do |ancestor|
      # HACK for classes we don't know about (class X < RuntimeError)
      String === ancestor ? ancestor : ancestor.full_name
    end

    @cache[:ancestors][klass.full_name] ||= []
    @cache[:ancestors][klass.full_name].push(*ancestors)

    attributes = klass.attributes.map do |attribute|
      "#{attribute.definition} #{attribute.name}"
    end

    unless attributes.empty? then
      @cache[:attributes][klass.full_name] ||= []
      @cache[:attributes][klass.full_name].push(*attributes)
    end

    return if @dry_run

    open path, 'wb' do |io|
      Marshal.dump klass, io
    end
  end

  ##
  # Writes the ri data for +method+ on +klass+

  def save_method klass, method
    FileUtils.mkdir_p class_path(klass.full_name) unless @dry_run

    cache = if method.singleton then
              @cache[:class_methods]
            else
              @cache[:instance_methods]
            end
    cache[klass.full_name] ||= []
    cache[klass.full_name] << method.name

    return if @dry_run

    open method_file(klass.full_name, method.full_name), 'wb' do |io|
      Marshal.dump method, io
    end
  end

end

