# frozen_string_literal: true

module Checkoff
  # Base class to evaluate a task selector function given fully evaluated arguments
  class FunctionEvaluator
    def initialize(task_selector:,
                   tasks:)
      @task_selector = task_selector
      @tasks = tasks
    end

    def evaluate_arg?(_index)
      true
    end

    private

    def fn?(object, fn_name)
      object.is_a?(Array) && !object.empty? && [fn_name, fn_name.to_s].include?(object[0])
    end

    def pull_custom_field_or_raise(task, custom_field_gid)
      custom_fields = task.custom_fields
      if custom_fields.nil?
        raise "Could not find custom_fields under task (was 'custom_fields' included in 'extra_fields'?)"
      end

      matched_custom_field = custom_fields.find { |data| data.fetch('gid') == custom_field_gid }
      if matched_custom_field.nil?
        raise "Could not find custom field with gid #{custom_field_gid} " \
              "in task #{task.gid} with custom fields #{custom_fields}"
      end

      matched_custom_field
    end

    attr_reader :task_selector
  end

  # :and function
  class AndFunctionEvaluator < FunctionEvaluator
    def matches?
      fn?(task_selector, :and)
    end

    def evaluate(_task, lhs, rhs)
      lhs && rhs
    end
  end

  # :not function
  class NotFunctionEvaluator < FunctionEvaluator
    def matches?
      fn?(task_selector, :not)
    end

    def evaluate(_task, subvalue)
      !subvalue
    end
  end

  # :nil? function
  class NilPFunctionEvaluator < FunctionEvaluator
    def matches?
      fn?(task_selector, :nil?)
    end

    def evaluate(_task, subvalue)
      subvalue.nil?
    end
  end

  # :tag function
  class TagPFunctionEvaluator < FunctionEvaluator
    def matches?
      fn?(task_selector, :tag)
    end

    def evaluate_arg?(_index)
      false
    end

    def evaluate(task, tag_name)
      task.tags.map(&:name).include? tag_name
    end
  end

  # :due function
  class DuePFunctionEvaluator < FunctionEvaluator
    def matches?
      fn?(task_selector, :due)
    end

    def evaluate(task)
      @tasks.task_ready?(task)
    end
  end

  # :custom_field_value function
  class CustomFieldValueFunctionEvaluator < FunctionEvaluator
    def matches?
      fn?(task_selector, :custom_field_value)
    end

    def evaluate_arg?(_index)
      false
    end

    def evaluate(task, custom_field_name)
      custom_fields = task.custom_fields
      if custom_fields.nil?
        raise "custom fields not found on task - did you add 'custom_field' in your extra_fields argument?"
      end

      custom_field = custom_fields.find { |field| field.fetch('name') == custom_field_name }
      return nil if custom_field.nil?

      custom_field['display_value']
    end
  end

  # :custom_field_gid_value function
  class CustomFieldGidValueFunctionEvaluator < FunctionEvaluator
    def matches?
      fn?(task_selector, :custom_field_gid_value)
    end

    def evaluate_arg?(_index)
      false
    end

    def evaluate(task, custom_field_gid)
      custom_field = pull_custom_field_or_raise(task, custom_field_gid)
      custom_field['display_value']
    end
  end

  # :custom_field_gid_value_contains_any_gid function
  class CustomFieldGidValueContainsAnyGidFunctionEvaluator < FunctionEvaluator
    def matches?
      fn?(task_selector, :custom_field_gid_value_contains_any_gid)
    end

    def evaluate_arg?(_index)
      false
    end

    def evaluate(task, custom_field_gid, custom_field_values_gids)
      actual_custom_field_values_gids = pull_custom_field_values_gids(task, custom_field_gid)

      actual_custom_field_values_gids.any? do |custom_field_value|
        custom_field_values_gids.include?(custom_field_value)
      end
    end

    private

    def pull_enum_values(custom_field)
      resource_subtype = custom_field.fetch('resource_subtype')
      case resource_subtype
      when 'enum'
        [custom_field.fetch('enum_value')]
      when 'multi_enum'
        custom_field.fetch('multi_enum_values')
      else
        raise "Teach me how to handle resource_subtype #{resource_subtype}"
      end
    end

    def find_gids(custom_field, enum_value)
      if enum_value.nil?
        []
      else
        raise "Unexpected enabled value on custom field: #{custom_field}" if enum_value.fetch('enabled') == false

        [enum_value.fetch('gid')]
      end
    end

    def pull_custom_field_values_gids(task, custom_field_gid)
      custom_field = pull_custom_field_or_raise(task, custom_field_gid)
      pull_enum_values(custom_field).flat_map do |enum_value|
        find_gids(custom_field, enum_value)
      end
    end
  end

  # Evaluator task selectors against a task
  class TaskSelectorEvaluator
    def initialize(task:,
                   tasks: Checkoff::Tasks.new)
      @task = task
      @tasks = tasks
    end

    FUNCTION_EVALUTORS = [
      NotFunctionEvaluator,
      NilPFunctionEvaluator,
      TagPFunctionEvaluator,
      CustomFieldValueFunctionEvaluator,
      CustomFieldGidValueFunctionEvaluator,
      CustomFieldGidValueContainsAnyGidFunctionEvaluator,
      AndFunctionEvaluator,
      DuePFunctionEvaluator,
    ].freeze

    def evaluate(task_selector)
      return true if task_selector == []

      FUNCTION_EVALUTORS.each do |evaluator_class|
        evaluator = evaluator_class.new(task_selector: task_selector,
                                        tasks: tasks)

        next unless evaluator.matches?

        return try_this_evaluator(task_selector, evaluator)
      end

      raise "Syntax issue trying to handle #{task_selector}"
    end

    private

    def try_this_evaluator(task_selector, evaluator)
      evaluated_args = task_selector[1..].map.with_index do |item, index|
        if evaluator.evaluate_arg?(index)
          evaluate(item)
        else
          item
        end
      end

      evaluator.evaluate(task, *evaluated_args)
    end

    attr_reader :task, :tasks, :task_selector
  end
end