# frozen_string_literal: true

require 'rake/tasklib'

require_relative 'values'
require_relative 'parameters'
require_relative 'definable'
require_relative 'defaults'

module RakeFactory
  class TaskSet < ::Rake::TaskLib
    extend Definable

    include Parameters
    include Configurable
    include Arguments

    class << self
      def tasks
        @tasks ||= []
      end

      def task(klass, *args, &block)
        tasks << TaskSpecification.new(klass, args, &block)
      end
    end

    def define_on(application)
      around_define(application) do
        self.class.tasks.each do |task_specification|
          task_specification
            .for_task_set(self)
            .define_on(application)
        end
      end
      self
    end

    def around_define(_application)
      yield
    end

    class TaskArguments
      attr_reader :arguments, :task_set

      def initialize(arguments, task_set)
        @arguments = arguments || []
        @task_set = task_set
      end

      def parameter_overrides
        task_set&.parameter_values || {}
      end

      def parameter_hash
        if arguments.first.is_a?(Hash)
          arguments.first
        else
          {}
        end
      end

      def resolve
        return [parameter_overrides] if arguments.empty?

        if arguments.first.is_a?(Hash)
          return [
            parameter_overrides
                 .merge(process_parameter_hash(arguments.first)),
            *arguments.drop(1)
          ]
        end

        arguments
      end

      private

      def process_parameter_hash(parameter_hash)
        parameter_hash.reduce({}) do |acc, (name, value)|
          acc.merge(name => Values.resolve(value).prepend_argument(task_set))
        end
      end
    end

    class TaskSpecification
      attr_reader :klass, :args, :block

      def initialize(klass, args, &block)
        @klass = klass
        @args = args
        @block = block
      end

      def for_task_set(task_set)
        TaskDefinition.new(klass, args, task_set, &block)
      end
    end

    class TaskDefinition
      attr_reader :task_set, :klass, :args, :block

      def initialize(klass, args, task_set, &block)
        @task_set = task_set
        @klass = klass
        @args = args
        @block = block
      end

      def define_on(application)
        return unless should_define?

        klass.new(*resolve_arguments, &resolve_block)
             .define_on(application)
      end

      private

      def task_arguments
        TaskArguments.new(args, task_set)
      end

      def should_define?
        if task_arguments.parameter_hash.include?(:define_if)
          task_arguments.parameter_hash[:define_if].call(task_set)
        else
          true
        end
      end

      def resolve_arguments
        task_arguments.resolve
      end

      def resolve_block
        lambda do |task, args|
          maybe_call_block(task, args)
          maybe_call_configuration_block(task, args)
        end
      end

      def maybe_call_configuration_block(task, args)
        return unless task_set.configuration_block.respond_to?(:call)

        view = ParameterView.new(task, task.class, task_set.class, args)
        task_set.invoke_configuration_block_on(view, args)
      end

      def maybe_call_block(task, args)
        return unless block.respond_to?(:call)

        block.call(*[task_set, task, args].slice(0, block.arity))
      end
    end

    private_constant :TaskArguments
    private_constant :TaskSpecification
    private_constant :TaskDefinition
  end
end