require 'bundler/gem_tasks'
require 'rspec/core/rake_task'
require 'stove/rake_task'
require 'fileutils'

RSpec::Core::RakeTask.new(:spec) do |t|
  t.pattern = FileList['files/spec/**/*_spec.rb']
end
Stove::RakeTask.new

task default: :spec

#
# "rake update" updates the copied_from_chef files so we can grab bugfixes or new features
#
CHEF_FILES = %w(
                chef/constants
                chef/delayed_evaluator
                chef/dsl/core
                chef/dsl/declare_resource
                chef/dsl/recipe
                chef/mixin/lazy_module_include
                chef/mixin/notifying_block
                chef/mixin/params_validate
                chef/mixin/powershell_out
                chef/mixin/properties
                chef/property
                chef/provider
                chef/provider/noop
                chef/resource
                chef/resource/action_class
                chef/resource_builder
              )
SPEC_FILES = %w(
                unit/mixin/properties_spec.rb
                unit/property_spec.rb
                unit/property/state_spec.rb
                unit/property/validation_spec.rb
                integration/recipes/resource_action_spec.rb
                integration/recipes/resource_converge_if_changed_spec.rb
                integration/recipes/resource_load_spec.rb
             )
KEEP_FUNCTIONS = {
  'chef/resource' => %w(
    initialize

    name

    resource_name self.use_automatic_resource_name

    identity state state_for_resource_reporter property_is_set reset_property
    resource_initializing resource_initializing= to_hash
    self.properties self.state_properties self.state_attr
    self.identity_properties self.identity_property self.identity_attrs
    self.property self.property_type
    self.lazy

    action allowed_actions self.allowed_actions self.default_action
    self.action self.declare_action_class self.action_class

    load_current_value current_value_does_not_exist
    self.load_current_value
  ),
  'chef/provider' => %w(
    initialize
    converge_if_changed
    compile_and_converge_action
    action
    self.use_inline_resources
    self.include_resource_dsl
    self.include_resource_dsl_module
  ),
  'chef/dsl/recipe' => %w(),
}
KEEP_INCLUDES = {
  'chef/resource' => %w(Chef::Mixin::ParamsValidate Chef::Mixin::Properties),
  'chef/provider' => %w(Chef::DSL::Core),
  'chef/dsl/recipe' => %w(Chef::DSL::Core Chef::DSL::Recipe Chef::Mixin::LazyModuleInclude),
}
KEEP_CLASSES = {
  'chef/provider' => %w(Chef::Provider Chef::Provider::InlineResources Chef::Provider::InlineResources::ClassMethods)
}
SKIP_LINES = {
  'chef/dsl/recipe' => [ /include Chef::Mixin::PowershellOut/ ]
}
PROCESS_LINES = {
}
# See chef_compat/resource for def. of resource_name and provider
# See chef_compat/monkeypatches/chef/resource for def. of current_value

desc "Pull new files from the chef client this is bundled with and update this cookbook"
task :update do
  # Copy files from chef to chef_compat/chef, with a few changes
  target_path = File.expand_path("../files/lib/chef_compat/copied_from_chef", __FILE__)
  chef_gem_path = Bundler.environment.specs['chef'].first.full_gem_path
  CHEF_FILES.each do |file|
    output = StringIO.new
    # First lets try to load the original file if it exists
    output.puts "begin"
    output.puts "  require '#{file}'"
    output.puts "rescue LoadError; end"
    output.puts ""
    # Wrap the whole thing in a ChefCompat module
    output.puts "require 'chef_compat/copied_from_chef'"
    output.puts "class Chef"
    output.puts "module ::ChefCompat"
    output.puts "module CopiedFromChef"

    # Bring over the Chef file
    chef_contents = IO.read(File.join(chef_gem_path, 'lib', "#{file}.rb"))
    skip_until = nil
    keep_until = nil
    in_class = []
    chef_contents.lines.each do |line|
      if keep_until
        keep_until = nil if keep_until === line
      else

        if skip_until
          skip_until = nil if skip_until === line
          next
        end

        # If this file only keeps certain functions, detect which function we are
        # in and only keep those. Also strip comments outside functions

        case line

        # Skip modules and classes that aren't part of our list
        when /\A(\s*)def\s+([A-Za-z0-9_.]+)/
          if KEEP_FUNCTIONS[file] && !KEEP_FUNCTIONS[file].include?($2)
            skip_until = /\A#{$1}end\s*$/
            next
          else
            function = $2
            # Keep everything inside a function no matter what it is
            keep_until = /\A#{$1}end\s*$/
          end

        # Skip comments and whitespace if we're narrowing the file (otherwise it
        # looks super weird)
        when /\A\s*#/, /\A\s*$/
          next if KEEP_CLASSES[file] || KEEP_FUNCTIONS[file]

        # Skip aliases/attrs/properties that we're not keeping
        when /\A\s*(attr_reader|attr_writer|attr_accessor|property|alias)\s*:(\w+)/
          next if KEEP_FUNCTIONS[file] && !KEEP_FUNCTIONS[file].include?($2)

        # Skip includes and extends that we're not keeping
        when /\A\s*(include|extend)\s*([A-Za-z0-9_:]+)/
          next if KEEP_INCLUDES[file] && !KEEP_INCLUDES[file].include?($2)

        end

        next if SKIP_LINES[file] && SKIP_LINES[file].any? { |skip| skip === line }
      end

      # If we are at the end of a class, pop in_class
      if in_class[-1] && in_class[-1][:until].match(line)
        class_name = in_class.pop[:name]
        # Don't bother printing classes/modules that we're not going to print anything under
        next if KEEP_CLASSES[file] && !KEEP_CLASSES[file].any? { |c| c.start_with?(class_name) }

      # Detect class open
      elsif line =~ /\A(\s*)(class|module)(\s+)([A-Za-z0-9_:]+)(\s*<\s*([A-Za-z0-9_:]+))?.*$/
        indent, type, space, class_name, _, superclass_name = $1, $2, $3, $4, $5, $6
        full_class_name = in_class[-1] ? "#{in_class[-1][:name]}::#{class_name}" : class_name
        in_class << { name: full_class_name, until: /\A#{indent}end\s*$/ }
        superclass_name ||= "Object"

        # Don't print the class open unless it contains stuff we'll keep
        next if KEEP_CLASSES[file] && !KEEP_CLASSES[file].any? { |c| c.start_with?(full_class_name) }

        # Fix the class to extend from its parent
        original_class = "::#{full_class_name}"
        if type == 'class'
          line = "#{indent}#{type}#{space}#{class_name} < (defined?(#{original_class}) ? #{original_class} : #{superclass_name})"
        else
          # Modules have a harder time of it because of self methods
          line += "#{indent}  CopiedFromChef.extend_chef_module(#{original_class}, self) if defined?(#{original_class})"
        end

      # If we're not in a class we care about, don't print stuff
      elsif KEEP_CLASSES[file] && in_class[-1] && !KEEP_CLASSES[file].any? { |c| c == in_class[-1][:name] }
        next
      end

      # Modify requires to overridden files to bring in the local version
      if line =~ /\A(\s*require\s*['"])([^'"]+)(['"].*)/
        if CHEF_FILES.include?($2)
          line = "#{$1}chef_compat/copied_from_chef/#{$2}#{$3}"
        else
          next
        end
      end

      line = PROCESS_LINES[file].call(line) if PROCESS_LINES[file]

      output.puts line

      # If this was the header for an initialize function, write out "super"
      if function == 'initialize'
        output.puts "super if defined?(::#{in_class[-1][:name]})"
      end
    end
    # Close the ChefCompat module declaration from the top
    output.puts "end"
    output.puts "end"
    output.puts "end"

    # Write out the file in chef_compat
    target_file = File.join(target_path, "#{file}.rb")
    if !File.exist?(target_file) || IO.read(target_file) != output.string
      puts "Writing #{target_file} ..."
      FileUtils.mkdir_p(File.dirname(target_file))
      File.open(target_file, "w") { |f| f.write(output.string) }
    end
  end

  # SPEC_FILES.each do |file|
  #   target_path = File.expand_path("../files/spec/copied_from_chef", __FILE__)
  #   source_file = File.join(chef_gem_path, 'lib', "#{file}.rb")
  #   target_file = File.join(target_path, "#{file}")
  # end
end
