# Copyright 2006-2010 Michel Casabianca <michel.casabianca@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.

require 'rubygems'
require 'bee'
require 'bee_util'

module Bee
  
  # Module for bee tasks.
  module Task

    # Base class for task package. Provides methods to print output using
    # the build formatter and a method to check task parameters. Furthermore,
    # this base class extends MethodInfoBase which provides methods
    # comments, for autodocumentation purpose.
    class Package < Bee::Util::MethodInfoBase
      
      include Bee::Util::BuildErrorMixin
      
      # Constructor.
      # - build: the build we are running.
      def initialize(build)
        @build = build
      end

      protected

      # Check task parameters. Raise a RuntimeError with explanation message
      # if a mandatory parameter is missing or an unknown parameter was found.
      # - params: task parameters as a Hash.
      # - description: parameters description as a Hash with following keys:
      #   :mandatory telling if the parameter is mandatory (true or false),
      #   :type which is the class name of the parameter and
      #   :default for default value.
      def check_parameters(params, description)
        task = caller[0].match(/`(.*)'/)[1]
        error "'#{task}' parameters must be a hash" unless params.kind_of?(Hash)
        for param in description.keys
          error "#{task} '#{param}' parameter is mandatory" unless
            params[param.to_s] or description[param][:mandatory] == false
          if params[param.to_s]
            case description[param][:type]
            when :string
              error "#{task} '#{param}' parameter must be a string" unless
                params[param.to_s].kind_of?(String)
            when :integer
              error "#{task} '#{param}' parameter must be an integer" unless
                params[param.to_s].kind_of?(Integer)
            when :float
              error "#{task} '#{param}' parameter must be a float" unless
                params[param.to_s].kind_of?(Float)
            when :number
              error "#{task} '#{param}' parameter must be a number" unless
                params[param.to_s].kind_of?(Numeric)
            when :boolean
              error "#{task} '#{param}' parameter must be a boolean" unless
                params[param.to_s] == true or params[param] == false
            when :array
              error "#{task} '#{param}' parameter must be an array" unless
                params[param.to_s].kind_of?(Array)
            when :string_or_array
              error "#{task} '#{param}' parameter must be a string or an array" unless
                params[param.to_s].kind_of?(String) or params[param.to_s].kind_of?(Array)
              params[param.to_s] = Array(params[param.to_s])
            when :string_or_integer
              error "#{task} '#{param}' parameter must be a string or an integer" unless
                params[param.to_s].kind_of?(String) or params[param.to_s].kind_of?(Integer)
            when :hash
              error "#{task} '#{param}' parameter must be a hash" unless
                params[param.to_s].kind_of?(Hash)
            else
              error "Unknown parameter type '#{description[param][:type]}'"
            end
            params[param.to_sym] = params[param.to_s]
          else
            if description[param][:default]
              params[param.to_sym] = description[param][:default]
              params[param.to_s] = description[param][:default]
            end
          end
        end
        for param in params.keys
          error "Unknown parameter '#{param}'" if 
            not (description.key?(param) or description.key?(param.to_sym))
        end
      end
      
      # Utility method to find and filter files.
      # - root: root directory for files to search.
      # - includes: list of globs for files to include in search.
      # - excludes: list of globs for files to exclude from search.
      # - dotmatch: tells if joker matches dot files.
      # Return: the list of found files (no directories included).
      def filter_files(includes, excludes, root, dotmatch=true)
        error "includes must be a glob or a list of globs" unless
          !includes or includes.kind_of?(String) or includes.kind_of?(Array)
        error "excludes must be a glob or a list of globs" unless
          !excludes or excludes.kind_of?(String) or excludes.kind_of?(Array)
        error "root must be an existing directory" unless
          !root or File.exists?(root)
        current_dir = Dir.pwd
        begin
          if dotmatch
            options = File::FNM_PATHNAME | File::FNM_DOTMATCH
          else
            options = File::FNM_PATHNAME
          end
          Dir.chdir(root) if root
          included = []
          includes = '**/*' if not includes
          includes = Array(includes)
          for include in includes
            error "includes must be a glob or a list of globs" unless
              include.kind_of?(String)
            # should expand directories ?
            # include = "#{include}/**/*" if File.directory?(include)
            entries = Dir.glob(include, options)
            included += entries if entries
          end
          included.uniq!
          if excludes
            included.reject! do |file|
              rejected = false
              for exclude in excludes
                if File.fnmatch?(exclude, file, options)
                  rejected = true
                  break
                end
              end
              rejected
            end
          end
          included.reject! { |file| File.directory?(file) }
          return included
        ensure
          Dir.chdir(current_dir)
        end
      end

      # Print text on the console.
      # - text: text to print.
      def print(text)
        if @build.listener
          @build.listener.print(text)
        else
          Kernel.print(text)
        end
      end
      
      # Puts text on the console.
      # - text: text to puts.
      def puts(text)
        if @build.listener
          @build.listener.puts(text)
        else
          Kernel.puts(text)
        end
      end

      # Prompts the user for a string.
      # Return the user input string.
      def gets
        if @build.listener
          return @build.listener.gets
        else
          return Kernel::STDIN.gets
        end        
      end
      
    end
    
    # Package manager is responsible for loading packages and calling tasks.
    class PackageManager
      
      include Bee::Util::BuildErrorMixin
      
      # Constructor.
      # - build: the build we are running.
      def initialize(build)
        @build = build
        @packages = {}
      end
      
      # Run a given task.
      # - task: YAML object for the task to run.
      def run_task(task)
        packaged = task.keys[0]
        package, name = Bee::Util::get_package_name(packaged)
        parameters = @build.context.evaluate_object(task[packaged])
        if not @packages[package]
          load_package(package)
        end
        error "Task '#{name}' not found in package '#{package}'" if
          not @packages[package].respond_to?(name)
        @packages[package].send(name, parameters)
      end
      
      # Get help for a given task.
      # - task: YAML object for the task to run.
      def help_task(task)
        package, name = Bee::Util::get_package_name(task)
        if not @packages[package]
          load_package(package)
        end
        help = {}
        if name == '?'
          methods = @packages[package].class.public_instance_methods(false)
          for method in methods
            help[method] = @packages[package].class.method_info(method).comment
          end
          return help
        else
          error "Task '#{name}' not found in package '#{package}'" if
            not @packages[package].respond_to?(name)
          help[task] = @packages[package].class.method_info(name).comment
        end
        return help
      end
      
      private

      # Load a given package using introspection: we try to instantiate class
      # named after the package capitalized, in module Bee::Task.
      # - package: the package name.
      def load_package(package)
        package = 'default' if not package
        package.downcase!
        script = "bee_task_#{package}"
        clazz = package.capitalize
        begin
          require script
          @packages[package] = Bee::Task.const_get(clazz).new(@build)
        rescue Exception
          error "Task package '#{package}' not found"
        end
      end
      
    end

  end
  
end