# frozen_string_literal: true

module Litany

  # Provides a few class methods to define new workspace options so validation can take place.
  module ProjectWorkspaceOptionsDSL
    # Provides instance methods for classes that extend the {ProjectWorkspaceOptionsDSL}.
    module InstanceMethods
      # Validates that the option is defined.
      # @return [Boolean]
      def option_exist?(key)
        workspace_options.include?(key)
      end

      # Validates a possible value for a given option
      # @param key [String] the option key you're testing.
      # @param value [Object] the value to validate for the given setting.
      # @note Will silently return `false` if this is testing against an undefined option.  Validity of an option should be tested separately with {#option_exist?}.
      # @return [Boolean] False is returned if the requested option does not exist.
      def valid_option_value?(key, value)
        return false unless option_exist?(key)
        workspace_option(key).valid_value?(value)
      end

      # Gets a copy of a specific option metadata for use in other functions.
      # @param key [String] the option key you're requesting.
      # @return [nil, WorkspaceOption] Nil is returned if the requested option does not exist.
      def workspace_option(key)
        workspace_options[key].clone
      end

      # (see ProjectWorkspaceOptionsDSL#workspace_options)
      def workspace_options
        self.class.workspace_options
      end
    end

    # Metadata and helpers representing a workspace option.
    WorkspaceOption = Struct.new(:key, :targets, :nilable, :default) do # rubocop:disable Metrics/BlockLength

      # Tests if this option is required to have a non-nil value.  This is effectively how we implement optionals.
      # @return [Boolean]
      def nilable?
        nilable
      end

      # Tests if the value is considered valid for this option.
      # @param value [Object, Array<Object>, Set<Object>] the value to test for validity.
      # @return [Boolean]
      def valid_value?(value)
        return true if nilable? && (value.nil? || (value.respond_to?(:empty?) && value.empty?))
        return value.all? { |v| valid_value?(v) } if value.is_a?(Array) || value.is_a?(Set)

        valid_against_target?(value, targets)
      end

      # Tests the validity of a specific value against a specific target.
      # @param value [Object] the value to test for validity
      # @param target [Array, Set, Class, Module, nil, NilClass, Range, Proc, Regexp, Object]
      # @return [Boolean]
      private def valid_against_target?(value, target) # rubocop:disable Metrics/CyclomaticComplexity
        target = target.call if target.is_a?(Proc)

        case target
          when Array, Set
            target.any? { |t| valid_against_target?(value, t) }
          when Module, Class
            value.is_a?(target)
          when NilClass, nil
            value.nil?
          when Range
            target.include?(value)
          when Regexp
            target.match?(value)
          else
            target == value
        end
      rescue TypeError # caused by a regex.match? call and the argument can't be implicitly converted into a string.
        false
      end
    end

    # Auto includes InstanceMethods
    # @!visibility private
    def self.extended(target)
      target.include InstanceMethods
    end

    # Defines a new workspace option
    # @param key [String] the name of the option.  Must be unique.  Must be a string that conforms to `/^[a-z0-9._]+$/`.
    # @param valid_values [Object] One or more values that are considered valid for this option.  Can be a class, type, regex, range, explicit values, or an array of any of the previous types.
    # @param nilable: [Boolean] If the option accepts nil or empty values.
    # @param default: [Object] The default value for this option if none is explicitly set on any layer.
    # @raise [ArgumentError] If the key is a duplicate or does not appear to be in the valid format.
    # @return [WorkspaceOption]
    def workspace_option(key, valid_values, nilable: false, default: nil)
      raise ArgumentError, "Invalid key: #{key}. Must be a string that conforms to /^[a-z0-9.]+$/." unless key.is_a?(String) && /^[a-z0-9._]+$/.match?(key)
      raise ArgumentError, 'You may not specify the same option key twice.' if workspace_options.include?(key)

      workspace_options[key] = WorkspaceOption.new(key, valid_values, nilable, default).freeze
    end

    # Returns all previously defined workspace options.
    # @return [Hash<String, WorkspaceOption>] keyed based on the name of the option.
    def workspace_options
      @workspace_options ||= {}
    end
  end
end
