# ==========================================================================
# Project:   Spade - CommonJS Runtime
# Copyright: ©2010 Strobe Inc. All rights reserved.
# License:   Licened under MIT license (see LICENSE)
# ==========================================================================

require 'json'


module Spade

  class Loader

    def initialize(ctx)
      @ctx = ctx
    end

    def discoverRoot(path)
      Spade.discover_root path
    end

    def root(path=nil)
      return @ctx.rootdir if path.nil?
      @ctx.rootdir = path 
      @packages = nil
    end

    # exposed to JS.  Find the JS file on disk and register the module
    def loadFactory(spade, id, formats, done=nil)
      formats = formats.to_a # We may get a V8::Array, we want a normal one

      # load individual files
      if id =~ /^file:\//
        js_path = id[5..-1]
        if File.exists? js_path
          load_module id, js_path, ['js'], js_path
        end
        return nil
      end

      parts = id.split '/'
      package_name = parts.shift
      package_info = packages[package_name]
      skip_module  = false

      return nil if package_info.nil?

      if parts.size==1 && parts[0] == '~package'
        skip_module = true
      elsif parts.size==1 && parts[0] == 'main'
        parts = (package_info[:json]['main'] || 'lib/main').split('/')
        dirs  = [parts[0...-1]]
        parts = [parts[-1]]
      else
        dirs = extract_dirs(parts, package_info)
      end

      # register the package first - also make sure dependencies are 
      # registered since they are needed for loading plugins
      unless package_info[:registered]
        package_info[:registered] = true
        @ctx.eval "spade.register('#{package_name}', #{package_info[:json].to_json});"
        
        deps = package_info[:json]['dependencies'] || [];
        deps.each do |dep_name, ignored|
          dep_package_info = packages[dep_name]
          next unless dep_package_info && !dep_package_info[:registered]
          dep_package_info[:registered] = true
          @ctx.eval "spade.register('#{dep_name}', #{dep_package_info[:json].to_json});"

          # Add new formats if they are specified in our dependencies
          dep_formats = dep_package_info[:json]['plugin:formats']
          formats.unshift(*dep_formats.keys).uniq! if dep_formats
        end
              
      end
      
      unless skip_module
        filename = parts.pop
        base_path = package_info[:path]
        formats << ['js'] if formats.empty?

        dirs.each do |dirname|
          formats.each do |fmt|
            cur_path = File.join(base_path, dirname, parts, filename+'.'+fmt)
            if File.exist? cur_path
              load_module id, cur_path, fmt, cur_path
              return nil
            end
          end
          
          rb_path = File.join(package_info[:path],dirname,parts, filename+'.rb')
          if File.exists? rb_path
            load_ruby id, rb_path
            return nil
          end
        end
      end
      
      return nil
    end

    # exposed to JS.  Determines if the named id exists in the system
    def exists(spade, id, formats)
      
      # individual files
      return File.exists?(id[5..-1]) if id =~ /^file:\//

      parts = id.split '/'
      package_name = parts.shift
      package_info = packages[package_name]
      
      return false if package_info.nil?
      return true if parts.size==1 && parts[0] == '~package'
      
      dirs = extract_dirs(parts, package_info)
      
      
      filename = parts.pop
      base_path = package_info[:path]
      formats = ['js'] if formats.nil?
      dirs.each do |dirname|
        formats.each do |fmt|
          cur_path = File.join(base_path, dirname, parts, filename+'.'+fmt)
          return true if File.exist? cur_path
        end
        
        rb_path = File.join(package_info[:path],dirname,parts, filename+'.rb')
        return File.exists? rb_path
      end
    end
    
    def load_module(id, module_path, format, path)
      module_contents = File.read(module_path).to_json # encode as string
      @ctx.eval("spade.register('#{id}',#{module_contents}, { format: #{format.to_s.to_json}, filename: #{path.to_s.to_json} });")
      nil
    end

    def load_ruby(id, rb_path)

      klass = Spade.exports_for rb_path
      exports = klass.nil? ? {} : klass.new(@ctx)
      @ctx['$__rb_exports__'] = exports

      @ctx.eval(%[(function() { 
        var exp = $__rb_exports__; 
        spade.register('#{id}', function(r,e,m) { m.exports = exp; }); 
      })();])

      @ctx['$__rb_exports__'] = nil
    end

    def packages
      @packages unless @packages.nil?
      @packages = {}

      if defined?(BPM)
        package_paths = Dir.glob(File.join(LibGems.dir, 'gems', '*'))
        package_paths.each{|path| add_package(path) }
      end

      # in reverse order of precedence
      %w[.spade/packages vendor/cache vendor/packages packages].each do |p|
        package_paths = Dir.glob File.join(@ctx.rootdir, p.split('/'), '*')
        package_paths.each { |path| add_package(path) }
      end

      # add self
      add_package @ctx.rootdir

      @packages
    end

    private

    def extract_dirs(parts, package_info)
      dirname = (parts.size > 0 && parts.first.chars.first == '~') ?
                    parts.shift[1..-1] : 'lib'

      dirs = package_info[:directories][dirname]
      raise "Can't require from unknown directory: #{dirname}" unless dirs
      dirs = [dirs] unless dirs.is_a?(Array)

      dirs
    end

    def add_package(path)
      json_package = File.join(path, 'package.json')
      return unless File.exists?(json_package)

      json = JSON.load(File.read(json_package)) rescue nil
      return if json.nil?

      directories = json["directories"] || { "lib" => "lib" }
      json["root"] = "file:/"+File.split(path).join('/')
      @packages[json["name"]] = { 
        :registered => false,
        :path => path, 
        :directories => directories,
        :json => json 
      }

    end

  end


end