require 'boson/save'
require 'boson/namespace'
require 'boson/more_util'
# order of library subclasses matters
%w{module file gem require local_file}.each {|e| require "boson/libraries/#{e}_library" }

module Boson
  # == Naming a Library Module
  # Although you can name a library module almost anything, here's the fine print:
  # * A module can have any name if it's the only module in a library.
  # * If there are multiple modules in a file library, the module's name must
  #   be a camelized version of the file's basename
  #   i.e. ~/.boson/commands/ruby_core.rb -> RubyCore.
  # * Although modules are evaluated under the Boson::Commands namespace, Boson
  #   will warn you about creating modules whose name is the same as a top level
  #   class/module. The warning is to encourage users to stay away from
  #   error-prone libraries. Once you introduce such a module, _all_ libraries
  #   assume the nested module over the top level module and the top level
  #   module has to be prefixed with '::' _everywhere_.
  #
  # == Configuration
  # Libraries and their commands can be configured in different ways in this order:
  # * If library is a FileLibrary, commands be configured with a config method
  #   attribute (see Inspector).
  # * If a library has a module, you can set library + command attributes via
  #   the config() callback (see Loader).
  # === Module Callbacks
  # For libraries that have a module i.e. RunnerLibrary, the following class methods
  # are invoked in the order below when loading a library:
  #
  # [*:config*] This method returns a library's hash of attributes as explained by Library.new. This is useful
  #             for distributing libraries with a default configuration. The library attributes specified here
  #             are overridden by ones a user has in their config file except for the :commands attribute, which
  #             is recursively merged together.
  # [*:append_features*] In addition to its normal behavior, this method's return value determines if a
  #                      library is loaded in the current environment. This is useful for libraries that you
  #                      want loaded by default but not in some environments i.e. different ruby versions or
  #                      in irb but not in script/console. Remember to use super when returning true.
  # [*:included*] In addition to its normal behavior, this method should be used to require external libraries.
  #               Although requiring dependencies could be done anywhere in a module, putting dependencies here
  #               are encouraged. By not having dependencies hardcoded in a module, it's possible to analyze
  #               and view a library's commands without having to install and load its dependencies.
  #               If creating commands here, note that conflicts with existing commands won't be detected.
  # [*:after_included*] This method is called after included() to initialize functionality. This is useful for
  #                     libraries that are primarily executing ruby code i.e. defining ruby extensions or
  #                     setting irb features. This method isn't called when indexing a library.
  class Library
    ATTRIBUTES << :gems

    module Libraries
      attr_reader :gems
      def local?
        is_a?(LocalFileLibrary) ||
          (Boson.local_repo && Boson.local_repo.dir == repo_dir)
      end
    end
    include Libraries

    # [*:object_methods*] Boolean which detects any Object/Kernel methods created when loading a library and automatically
    #                     adds them to a library's commands. Default is true.
    module LibrariesLoader
      def detect_additions(options={}, &block)
        options[:object_methods] = @object_methods if !@object_methods.nil?
        super(options, &block).tap do |detected|
          if detected[:gems]
            @gems ||= []
            @gems.concat detected[:gems]
          end
        end
      end

      def module_callbacks
        set_config(@module.config) if @module.respond_to?(:config)
        if @module.respond_to?(:append_features)
          raise AppendFeaturesFalseError unless @module.append_features(Module.new)
        end
        super
      end

      def before_load_commands
        raise(LoaderError, "No module for library #{@name}") unless @module
        if (conflict = Util.top_level_class_conflict(Boson::Commands, @module.to_s))
          warn "Library module '#{@module}' may conflict with top level class/module '#{conflict}' references in"+
            " your libraries. Rename your module to avoid this warning."
        end
        super
      end

      def after_include
        @module.after_included if @module.respond_to?(:after_included) && !@index
        super
      end
    end
    include LibrariesLoader
  end

  # Raised when a library's append_features returns false.
  class AppendFeaturesFalseError < StandardError; end

  class Manager
    module Libraries
      def handle_load_action_error(library, load_method, err)
        if err.is_a? Boson::AppendFeaturesFalseError
          warn "DEBUG: Library #{library} didn't load due to append_features" if Boson.debug
        else
          super
        end
      end

      def before_create_commands(lib)
        super
        if lib.is_a?(FileLibrary) && lib.module
          Inspector.add_method_data_to_library(lib)
        end
      end

      def add_failed_library(library)
        FileLibrary.reset_file_cache(library.to_s)
        super
      end

      def check_for_uncreated_aliases(lib, commands)
        return if lib.is_a?(GemLibrary)
        super
      end
    end
    include Libraries
  end

  class Command
    # hack: have to override
    # Array of array args with optional defaults. Scraped with MethodInspector
    def args(lib=library)
      @args = !@args.nil? ? @args : begin
        if lib
          file_string, meth = file_string_and_method_for_args(lib)
          (file_string && meth && (@file_parsed_args = true) &&
            MethodInspector.scrape_arguments(file_string, meth))
        end || false
      end
    end

    module Libraries
      def file_string_and_method_for_args(lib)
        if !lib.is_a?(ModuleLibrary) && (klass_method = (lib.class_commands || {})[@name])
          klass, meth = klass_method.split(NAMESPACE, 2)
          if (meth_locations = MethodInspector.find_class_method_locations(klass, meth))
            file_string = File.read meth_locations[0]
          end
        elsif File.exists?(lib.library_file || '')
          file_string, meth = FileLibrary.read_library_file(lib.library_file), @name
        end
        [file_string, meth]
      end
    end
    include Libraries
  end

  class MethodInspector
    module Libraries
      def find_class_method_locations(klass, meth)
        if (klass = Util.any_const_get(klass)) && (meth_location = klass.method(meth).source_location) &&
          meth_location[0]
          meth_location
        end
      end
    end
    extend Libraries
  end

  module Scientist
    module Libraries
      def help_options
        @global_options[:verbose] ? ['--verbose'] : []
      end

      def run_help_option(cmd)
        Boson.invoke :usage, cmd.full_name + " " + help_options.join(' ')
      end
    end
    extend Libraries
  end
end