require 'yaml'
require 'time'
require 'rake'
require 'rake/clean'
require 'shellwords'

module Inochi
  class Engine

    require 'inochi/generate'
    include Inochi::Generate

    def run
      register_init_task

      if has_project_config?
        load_project_config
        register_rake_tasks
      end

      run_rake_tasks
    end

    ##
    # Renders the given RBS (ruby string) file
    # and/or template inside the given binding.
    #
    def self.render_rbs binding, filename, template = File.read(filename)
      here = "TEMPORARY_HERE_DOC#{object_id}TEMPORARY_HERE_DOC"
      eval("<<#{here}\n#{template}\n#{here}", binding, filename).chomp
    end

    private

    def run_rake_tasks
      task :default do
        Rake.application.options.show_task_pattern = //
        Rake.application.display_tasks_and_comments
      end

      Rake.application.init 'inochi'
      Rake.application.top_level
    end

    def register_rake_tasks
      Dir[File.dirname(__FILE__) + '/tasks/*.rake'].sort.each do |file|
        instance_eval File.read(file), file
      end
    end

    def register_init_task
      desc 'Instill Inochi into current directory.'
      task :init do
        unless project_name = ENV['project']
          raise ArgumentError, 'project name not specified'
        end

        library_name = Engine.calc_library_name(project_name)
        package_name = ENV['package'] || Engine.calc_package_name(library_name)

        project_version = '0.0.0'
        project_release = Time.now.strftime('%F')

        command_file = "bin/#{package_name}"
        create_from_rbs binding, command_file, 'command'
        chmod 0755, command_file

        create_from_rbs binding, PROJECT_CONFIG_FILE

        create_from_rbs binding, "lib/#{package_name}.rb", 'library'
        create_from_rbs binding, "lib/#{package_name}/inochi.rb"

        create_from_rbs binding, 'test/runner', 'test_runner'
        chmod 0755, 'test/runner'
        create_from_rbs binding, 'test/helper.rb', 'test_helper.rb'
        create_from_rbs binding, "test/#{package_name}_test.rb", 'library_test.rb'

        create_from_rbs binding, 'LICENSE'
        create_from_rbs binding, 'MANUAL'
        create_from_rbs binding, 'SYNOPSIS'
        create_from_rbs binding, 'README'
        create_from_rbs binding, 'INSTALL'
        create_from_rbs binding, 'USAGE'
        create_from_rbs binding, 'HACKING'
        create_from_rbs binding, 'HISTORY'
        create_from_rbs binding, 'CREDITS'
        create_from_rbs binding, 'BEYOND'
      end
    end

    PROJECT_CONFIG_FILE = 'inochi.conf'

    def has_project_config?
      File.exist? PROJECT_CONFIG_FILE
    end

    def load_project_config
      @project_config = YAML.load_file(PROJECT_CONFIG_FILE).to_hash

      # load the project module
      library_file = Dir['lib/*/inochi.rb'].first
      package_name = File.basename(File.dirname(library_file))
      library_name = File.read(library_file)[/\b(module|class)\b\s+(\w+)/, 2]

      $LOAD_PATH.unshift 'lib'
      require "#{package_name}/inochi"

      @project_module = Object.const_get(library_name)
      @project_package_name = package_name
      @project_library_name = library_name
      @project_gem_file = "#{@project_package_name}-#{@project_module::VERSION}.gem"

    rescue => error
      error.message.insert 0,
        "Could not load project configuration file #{PROJECT_CONFIG_FILE.inspect}: "
      raise error
    end

    TEMPLATE_DIR = File.join(File.dirname(__FILE__), 'templates')

    ##
    # Renders the given RBS template file (found in the
    # TEMPLATE_DIR directory) to the given output file.
    #
    def create_from_rbs binding, output_file, template_file = File.basename(output_file)
      actual_template_file = "#{TEMPLATE_DIR}/#{template_file}.rbs"
      create output_file, self.class.render_rbs(binding, actual_template_file)
    end

    ##
    # Writes the given contents to the file at the given
    # path.  If the given path already exists, then a
    # backup is created before invoking the merging tool.
    #
    def create path, body, merger = ENV['merger']
      generate path, body do |*files|
        system "#{merger} #{Shellwords.join files}" if merger
      end
    end

    ##
    # Returns the name of the main program executable, which
    # is the same as the project name fully in lowercase.
    #
    def self.calc_package_name library_name
      camel_to_snake_case(library_name).downcase
    end

    ##
    # Calculates the name of the project module from the given project name.
    #
    def self.calc_library_name project_name
      name = project_name.to_s.gsub(/\W+/, '_').squeeze('_').gsub(/^_|_$/, '')
      (name[0,1].upcase + name[1..-1]).to_sym
    end

    ##
    # Transforms the given input from CamelCase to snake_case.
    #
    def self.camel_to_snake_case input
      input = input.to_s.dup

      # handle camel case like FooBar => Foo_Bar
      while input.gsub!(/([a-z]+)([A-Z])(\w+)/) { $1 + '_' + $2 + $3 }
      end

      # handle abbreviations like XMLParser => XML_Parser
      while input.gsub!(/([A-Z]+)([A-Z])([a-z]+)/) { $1 + '_' + $2 + $3 }
      end

      input
    end

  end
end