=begin
    Copyright 2010-2013 Tasos Laskos <tasos.laskos@gmail.com>

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

        http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
=end

module Arachni

module Component

#
# {Component} error namespace.
#
# All {Component} errors inherit from and live under it.
#
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
#
class Error < Arachni::Error

    #
    # Raised when a specified component could not be found/does not exist.
    #
    # @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
    #
    class NotFound < Error
    end
end

require Options.dir['lib'] + 'component/options'

#
# Handles modules, reports, path extractor modules, plug-ins, pretty much
# every modular aspect of the framework.
#
# It is usually extended to fill-in for system specific functionality.
#
# @example
#
#    # create a namespace for our components
#    module Components
#    end
#
#    LIB       = "#{File.dirname( __FILE__ )}/lib/"
#    NAMESPACE = Components
#
#    # $ ls LIB
#    #   component1.rb  component2.rb
#    #
#    # $ cat LIB/component1.rb
#    #   class Components::Component1
#    #   end
#    #
#    # $ cat LIB/component2.rb
#    #   class Components::Component2
#    #   end
#
#
#    p components = Arachni::Component::Manager.new( LIB, NAMESPACE )
#    #=> {}
#
#    p components.available
#    #=> ["component2", "component1"]
#
#    p components.load_all
#    #=> ["component2", "component1"]
#
#    p components
#    #=> {"component2"=>Components::Component2, "component1"=>Components::Component1}
#
#    p components.clear
#    #=> {}
#
#    p components.load :component1
#    #=> ["component1"]
#
#    p components
#    #=> {"component1"=>Components::Component1}
#
#    p components.clear
#    #=> {}
#
#    p components[:component2]
#    #=> Components::Component2
#
#
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
#
class Manager < Hash
    include UI::Output

    WILDCARD = '*'
    EXCLUDE  = '-'

    # @return   [String]    The path to the component library/directory.
    attr_reader :lib

    # @return [Module]
    #   Namespace under which all components are directly defined.
    attr_reader :namespace

    #
    # @param    [String]    lib    The path to the component library/directory.
    # @param    [Module,Class]    namespace
    #   Namespace under which all components are directly defined.
    #
    def initialize( lib, namespace )
        @lib = lib
        @namespace = namespace
    end

    #
    # Loads components.
    #
    # @param    [Array<String,Symbol>]    components
    #   Components to load.
    #
    # @return   [Array] Names of loaded components.
    #
    def load( *components )
        parse( [components].flatten ).each { |component| self.[]( component ) }
    end

    #
    # Loads all components, equivalent of `load '*'`.
    #
    # @return   [Array] Names of loaded components.
    #
    def load_all
        load '*'
    end

    #
    # Loads components by the tags found in the `Hash` returned by their `.info` method
    # (tags should be in either: `:tags` or `:issue[:tags]`).
    #
    # @param    [Array] tags    Tags to look for in components.
    #
    # @return   [Array] Components loaded.
    #
    def load_by_tags( tags )
        return [] if !tags

        tags = [tags].flatten.compact.map( &:to_s )
        return [] if tags.empty?

        load_all
        map do |k, v|
            component_tags  = [v.info[:tags]]
            component_tags |= [v.info[:issue][:tags]] if v.info[:issue]
            component_tags  = [component_tags].flatten.uniq.compact

            if !component_tags.includes_tags?( tags )
                delete( k )
                next
            end
            k
        end.compact
    end

    #
    # Validates and prepares options for a given component.
    #
    # @param    [String]    component_name    Name of the component.
    # @param    [Class]     component         Component.
    # @param    [Hash]      user_opts         User options.
    #
    # @return   [Hash]   Prepared options to be passed to the component.
    #
    # @raise    [Component::Options::Error::Invalid]
    #   If given options are invalid.
    #
    def prep_opts( component_name, component, user_opts = {} )
        info = component.info
        return {} if !info.include?( :options ) || info[:options].empty?

        user_opts ||= {}
        options = {}
        errors  = {}
        info[:options].each do |opt|
            name = opt.name
            val  = user_opts[name] || opt.default

            if opt.empty_required_value?( val )
                errors[name] = {
                    opt:   opt,
                    value: val,
                    type:  :empty_required_value
                }
            elsif !opt.valid?( val )
                errors[name] = {
                    opt:   opt,
                    value: val,
                    type:  :invalid
                }
            end

            options[name] = opt.normalize( val )
        end

        if !errors.empty?
            fail Component::Options::Error::Invalid,
                 format_error_string( component_name, errors )
        end

        options
    end

    #
    # It parses the component array making sure that its structure is valid
    # and takes into consideration {WILDCARD wildcard} and {EXCLUDE exclusion}
    # modifiers.
    #
    # @param    [Array<String,Symbol>]    components   Component names.
    #
    # @return   [Array]    Components to load.
    #
    def parse( components )
        unload = []
        load   = []

        components = [components].flatten.map( &:to_s )

        return load if components[0] == EXCLUDE

        components = components.deep_clone

        components.each do |component|
            if component[0] == EXCLUDE
                component[0] = ''

                if component[WILDCARD]
                    unload |= wilcard_to_names( component )
                else
                    unload << component
                end

            end
        end

        if !components.include?( WILDCARD )

            avail_components  = available(  )

            components.each do |component|

                if component.substring?( WILDCARD )
                    load |= wilcard_to_names( component )
                else

                    if avail_components.include?( component )
                        load << component
                    else
                        fail Error::NotFound,
                             "Component '#{component}' could not be found."
                    end
                end
            end

            load.flatten!
        else
            available.each{ |component| load << component }
        end

        load - unload
    end

    #
    # Fetches a component's class by name, loading it on the fly if need be.
    #
    # @param    [String, Symbol]    name    Component name.
    #
    # @return   [Class] Component.
    #
    def []( name )
        name = name.to_s
        return fetch( name ) if include?( name )
        self[name] = load_from_path( name_to_path( name ) )
    end

    def include?( k )
        super( k.to_s )
    end
    alias :loaded? :include?

    # Unloads all loaded components.
    def clear
        keys.each { |l| delete( l ) }
    end
    alias :unload_all :clear

    #
    # Unloads a component by name.
    #
    # @param    [String, Symbol]    name   Component name.
    #
    def delete( name )
        name = name.to_s
        begin
            @namespace.send( :remove_const, fetch( name ).to_s.split( ':' ).last.to_sym )
        rescue
        end
        super( name )
    end
    alias :unload :delete

    # @return    [Array]    Names of available components.
    def available
        paths.map{ |path| path_to_name( path ) }
    end

    # @return    [Array]    Names of loaded components.
    def loaded
        keys
    end

    #
    # Converts the name of a component to a its file's path.
    #
    # @param    [String]    name    Name of the component.
    #
    # @return   [String]    Path to component file.
    #
    def name_to_path( name )
        paths.each { |path| return path if name.to_s == path_to_name( path ) }
        nil
    end

    #
    # Converts the path of a component to a component name.
    #
    # @param    [String]    path    File-path of the component.
    #
    # @return   [String]    Component name.
    #
    def path_to_name( path )
        File.basename( path, '.rb' )
    end

    # @return   [Array]
    #   Paths of all available components (excluding helper files).
    def paths
        Dir.glob( File.join( "#{@lib}**", "*.rb" ) ).reject{ |path| helper?( path ) }
    end

    private

    def wilcard_to_names( name )
        if name[WILDCARD]
            paths.map do |path|
                path_to_name( path ) if path.match( name.gsub( '*', '(.*)' ) )
            end.compact
        end
    end

    def format_error_string( name, errors )
        "Invalid options for component: #{name}\n" +
        errors.map do |optname, error|
            val = error[:value].nil? ? '<empty>' : error[:value]
            msg = (error[:type] == :invalid) ? "Invalid type" : "Empty required value"

            " *  #{msg}: #{optname} => '#{val}'\n" +
            " *  Expected type: #{error[:opt].type}"
        end.join( "\n\n" )
    end

    def load_from_path( path )
        pre = classes
        ::Kernel::load( path )
        post = classes

        return if pre == post
        get_obj( (post - pre).first )
    end

    def classes
        @namespace.constants.reject{ |c| !get_obj( c ).is_a?( Class ) }
    end

    def get_obj( sym )
        @namespace.const_get( sym )
    end

    def helper?( path )
        File.exist?( File.dirname( path ) + '.rb' )
    end

end

end
end