require 'librarian/spec'

module Librarian
  class Dsl
    class Target

      class SourceShortcutDefinitionReceiver
        def initialize(target)
          singleton_class = class << self; self end
          singleton_class.class_eval do
            define_method(:source) do |options|
              target.source_from_options(options)
            end
            target.source_types.each do |source_type|
              name = source_type[0]
              define_method(name) do |*args|
                args.push({}) unless Hash === args.last
                target.source_from_params(name, *args)
              end
            end
          end
        end
      end

      SCOPABLES = [:source, :sources]

      attr_accessor :dsl
      private :dsl=

      attr_reader :dependency_name, :dependency_type
      attr_reader :source_types, :source_types_map, :source_types_reverse_map, :source_type_names, :source_shortcuts
      attr_reader :dependencies, :source_cache, *SCOPABLES

      def initialize(dsl)
        self.dsl = dsl
        @dependency_name = dsl.dependency_name
        @dependency_type = dsl.dependency_type
        @source_types = dsl.source_types
        @source_types_map = Hash[source_types]
        @source_types_reverse_map = Hash[source_types.map{|pair| a, b = pair ; [b, a]}]
        @source_type_names = source_types.map{|t| t[0]}
        @source_cache = {}
        @source_shortcuts = {}
        @dependencies = []
        @exclusions = []
        SCOPABLES.each do |scopable|
          instance_variable_set(:"@#{scopable}", [])
        end
        dsl.source_shortcuts.each do |name, param|
          define_source_shortcut(name, param)
        end
      end

      def to_spec
        Spec.new(@sources, @dependencies, @exclusions)
      end

      def dependency(name, *args)
        options = args.last.is_a?(Hash) ? args.pop : {}
        source = source_from_options(options) || @source
        dep = dependency_type.new(name, args, source)
        @dependencies << dep
      end

      def exclusion(name)
        @exclusions << name
      end

      def source(name, param = nil, options = nil, &block)
        if !(Hash === name) && [Array, Hash, Proc].any?{|c| c === param} && !options && !block
          define_source_shortcut(name, param)
        elsif !(Hash === name) && !param && !options
          source = source_shortcuts[name]
          scope_or_directive(block) do
            @source = source
            @sources = @sources.dup << source
          end
        else
          name, param, options = *normalize_source_options(name, param, options || {})
          source = source_from_params(name, param, options)
          scope_or_directive(block) do
            @source = source
            @sources = @sources.dup << source
          end
        end
      end

      def precache_sources(sources)
        sources.each do |source|
          key = [source_types_reverse_map[source.class], *source.to_spec_args]
          source_cache[key] = source
        end
      end

      def scope
        currents = { }
        SCOPABLES.each do |scopable|
          currents[scopable] = instance_variable_get(:"@#{scopable}").dup
        end
        yield
      ensure
        SCOPABLES.reverse.each do |scopable|
          instance_variable_set(:"@#{scopable}", currents[scopable])
        end
      end

      def scope_or_directive(scoped_block = nil)
        unless scoped_block
          yield
        else
          scope do
            yield
            scoped_block.call
          end
        end
      end

      def normalize_source_options(name, param, options)
        if name.is_a?(Hash)
          extract_source_parts(name)
        else
          [name, param, options]
        end
      end

      def extract_source_parts(options)
        if name = source_type_names.find{|name| options.key?(name)}
          options = options.dup
          param = options.delete(name)
          [name, param, options]
        else
          nil
        end
      end

      def source_from_options(options)
        if options[:source]
          source_shortcuts[options[:source]]
        elsif source_parts = extract_source_parts(options)
          source_from_params(*source_parts)
        else
          nil
        end
      end

      def source_from_params(name, param, options)
        source_cache[[name, param, options]] ||= begin
          type = source_types_map[name]
          type.from_spec_args(environment, param, options)
        end
      end

      def source_from_source_shortcut_definition(definition)
        case definition
        when Array
          source_from_params(*definition)
        when Hash
          source_from_options(definition)
        when Proc
          receiver = SourceShortcutDefinitionReceiver.new(self)
          receiver.instance_eval(&definition)
        end
      end

      def define_source_shortcut(name, definition)
        source = source_from_source_shortcut_definition(definition)
        source_shortcuts[name] = source
      end

      def environment
        dsl.environment
      end

    end
  end
end
