require 'ore/template/directory'
require 'ore/template/interpolations'
require 'ore/template/helpers'
require 'ore/naming'
require 'ore/config'

require 'thor/group'
require 'date'
require 'set'

module Ore
  class Generator < Thor::Group

    include Thor::Actions
    include Naming
    include Template::Interpolations
    include Template::Helpers

    #
    # The templates registered with the generator.
    #
    def self.templates
      @@templates ||= {}
    end

    #
    # Determines whether a template was registered.
    #
    # @param [Symbol, String] name
    #   The name of the template.
    #
    # @return [Boolean]
    #   Specifies whether the template was registered.
    #
    # @since 0.7.2
    #
    def self.template?(name)
      self.templates.has_key?(name.to_sym)
    end

    #
    # Registers a template with the generator.
    #
    # @param [String] path
    #   The path to the template.
    #
    # @return [Symbol]
    #   The name of the registered template.
    #
    # @raise [StandardError]
    #   The given path was not a directory.
    #
    def self.register_template(path)
      unless File.directory?(path)
        raise(StandardError,"#{path.dump} is must be a directory")
      end

      name = File.basename(path).to_sym

      self.templates[name] = path
      return name
    end

    #
    # Default options for the generator.
    #
    # @return [Hash{Symbol => Object}]
    #   The option names and default values.
    #
    # @since 0.5.0
    #
    def self.defaults
      @@defaults ||= {
        :templates => [],
        :version => '0.1.0',
        :summary => 'TODO: Summary',
        :description => 'TODO: Description',
        :license => 'MIT',
        :authors => [ENV['USER']],
        :ore_tasks => true,
        :rdoc => true,
        :rspec => true,
        :git => true
      }
    end

    #
    # Defines a generator option.
    #
    # @param [Symbol] name
    #   The name of the option.
    #
    # @param [Hash{Symbol => Object}] options
    #   The Thor options of the option.
    #
    # @since 0.5.0
    #
    def self.generator_option(name,options={})
      class_option(name,options.merge(:default => defaults[name]))
    end

    # The enabled templates.
    attr_reader :enabled_templates

    # The disabled templates.
    attr_reader :disabled_templates

    # The loaded templates.
    attr_reader :templates

    # The generated directories.
    attr_reader :generated_dirs

    # The generated files.
    attr_reader :generated_files

    #
    # Generates a new project.
    #
    def generate
      self.destination_root = path

      enable_templates!
      initialize_variables!

      say "Generating #{self.destination_root}", :green

      generate_directories!
      generate_files!

      if options.git?
        in_root do
          unless File.directory?('.git')
            run 'git init'
            run 'git add .'
            run 'git commit -m "Initial commit."'
          end
        end
      end
    end

    protected

    # merge default options
    defaults.merge!(Config.default_options)

    # register builtin templates
    Config.builtin_templates { |path| register_template(path) }
    # register installed templates
    Config.installed_templates { |path| register_template(path) }

    # define options for all templates
    templates.each_key do |name|
      # skip the `base` template
      next if name == :base

      class_option name, :type => :boolean,
                         :default => defaults.fetch(name,false)
    end

    # disable the Thor namespace
    namespace ''

    # define the options
    generator_option :markdown, :type => :boolean
    generator_option :textile, :type => :boolean
    generator_option :templates, :type => :array,
                                 :aliases => '-T',
                                 :banner => 'TEMPLATE [...]'
    generator_option :name, :type => :string, :aliases => '-n'
    generator_option :version, :type => :string, :aliases => '-V'
    generator_option :summary, :aliases => '-s'
    generator_option :description, :aliases => '-D'
    generator_option :authors, :type => :array,
                               :aliases => '-a',
                               :banner => 'NAME [...]'
    generator_option :email, :type => :string, :aliases => '-e'
    generator_option :homepage, :type => :string, :aliases => '-U'
    generator_option :license, :aliases => '-L'
    generator_option :git, :type => :boolean

    argument :path, :required => true

    #
    # Generates an empty directory.
    #
    # @param [String] dest
    #   The uninterpolated destination path.
    #
    # @return [String]
    #   The destination path of the directory.
    #
    # @since 0.7.1
    #
    def generate_dir(dest)
      return if @generated_dirs.include?(dest)

      path = interpolate(dest)
      empty_directory path

      @generated_dirs << dest
      return path
    end

    #
    # Generates a file.
    #
    # @param [String] dest
    #   The uninterpolated destination path.
    #
    # @param [String] file
    #   The source file or template.
    #
    # @param [Hash] options
    #   Additional options.
    #
    # @option options [Boolean] :template
    #   Specifies that the file is a template, and should be rendered.
    #
    # @return [String]
    #   The destination path of the file.
    #
    # @since 0.7.1
    #
    def generate_file(dest,file,options={})
      return if @generated_files.include?(dest)

      path = interpolate(dest)

      if options[:template]
        @current_template_dir = File.dirname(dest)
        template file, path
        @current_template_dir = nil
      else
        copy_file file, path
      end

      if File.executable?(file)
        umask = File.stat(File.join(destination_root,path)).mode
        chmod path, (umask | 0111)
      end

      @generated_files << dest
      return path
    end

    #
    # Enables a template, adding it to the generator.
    #
    # @param [Symbol, String] name
    #   The name of the template to add.
    #
    # @since 0.4.0
    #
    def enable_template(name)
      name = name.to_sym

      return false if @enabled_templates.include?(name)

      unless (template_dir = self.class.templates[name])
        say "Unknown template #{name}", :red
        exit -1
      end

      new_template = Template::Directory.new(template_dir)

      # mark the template as enabled
      @enabled_templates << name

      # enable any other templates
      new_template.enable.each do |sub_template|
        enable_template(sub_template)
      end

      # append the new template to the end of the list,
      # to override previously loaded templates
      @templates << new_template

      # add the template directory to the source-paths
      self.source_paths << new_template.path
      return true
    end

    #
    # Disables a template in the generator.
    #
    # @param [Symbol, String] name
    #   The name of the template.
    #
    # @since 0.4.0
    #
    def disable_template(name)
      name = name.to_sym

      return false if @disabled_templates.include?(name)

      unless (template_dir = self.class.templates[name])
        say "Unknown template #{name}", :red
        exit -1
      end

      source_paths.delete(template_dir)

      @templates.delete_if { |template| template.path == template_dir }
      @enabled_templates.delete(name)
      @disabled_templates << name
      return true
    end

    #
    # Enables templates.
    #
    def enable_templates!
      @templates = []
      @enabled_templates = Set[]
      @disabled_templates = Set[]
      
      enable_template :base

      # enable the default templates first
      self.class.defaults.each_key do |name|
        if (self.class.template?(name) && options[name])
          enable_template(name)
        end
      end

      # enable the templates specified by option
      options.each do |name,value|
        if (self.class.template?(name) && value)
          enable_template(name)
        end
      end

      # enable any additionally specified templates
      options.templates.each { |name| enable_template(name) }

      # disable any previously enabled templates
      @templates.reverse_each do |template|
        template.disable.each { |name| disable_template(name) }
      end
    end

    #
    # Initializes variables for the templates.
    #
    def initialize_variables!
      @project_dir = File.basename(destination_root)
      @name = (options.name || @project_dir)

      @modules = modules_of(@name)
      @module_depth = @modules.length
      @module = @modules.last

      @namespace = namespace_of(@name)
      @namespace_dirs = namespace_dirs_of(@name)
      @namespace_path = namespace_path_of(@name)
      @namespace_dir = @namespace_dirs.last

      @version = options.version
      @summary = options.summary
      @description = options.description
      @license = options.license
      @email = options.email
      @safe_email = @email.sub('@',' at ') if @email
      @homepage = (options.homepage || "http://rubygems.org/gems/#{@name}")
      @authors = options.authors
      @author = options.authors.first

      @markup = if options.markdown?
                  :markdown
                elsif options.textile?
                  :textile
                else
                  :rdoc
                end

      @date = Date.today
      @year = @date.year
      @month = @date.month
      @day = @date.day

      @templates.each do |template|
        template.variables.each do |name,value|
          instance_variable_set("@#{name}",value)
        end
      end

      @generated_dirs = Set[]
      @generated_files = Set[]
    end

    #
    # Creates directories listed in the template directories.
    #
    def generate_directories!
      @templates.each do |template|
        template.each_directory do |dir|
          generate_dir dir
        end
      end
    end

    #
    # Copies static files and renders templates in the template directories.
    #
    def generate_files!
      # iterate through the templates in reverse, so files in the templates
      # loaded last override the previously templates.
      @templates.reverse_each do |template|
        # copy in the static files first
        template.each_file(@markup) do |dest,file|
          generate_file dest, file
        end

        # then render the templates
        template.each_template(@markup) do |dest,file|
          generate_file dest, file, :template => true
        end
      end
    end

  end
end