# frozen_string_literal: true

module FmRest
  module V1
    module Utils
      VALID_SCRIPT_KEYS = [:prerequest, :presort, :after].freeze

      # See https://help.claris.com/en/pro-help/content/finding-text.html
      FM_FIND_OPERATORS_RE = /([@\*#\?!=<>"])/

      FULLY_QUALIFIED_FIELD_NAME_MATCHER = /\A[^:]+::[^:]+\Z/.freeze

      # Converts custom script options to a hash with the Data API's expected
      # JSON script format.
      #
      # If script_options is a string or symbol it will be passed as the name
      # of the script to execute (after the action, e.g. save).
      #
      # If script_options is an array the first element will be the name of the
      # script to execute (after the action) and the second element (if any)
      # will be its param value.
      #
      # If script_options is a hash it will expect to contain one or more of
      # the following keys: :prerequest, :presort, :after
      #
      # Any of those keys should contain either a string/symbol or array, which
      # will be treated as described above, except for their own script
      # execution order (prerequest, presort or after action).
      #
      # @param script_options [String, Hash, Array] The script parameters to
      # convert to canonical form
      #
      # @return [Hash] The converted script parameters
      #
      # @example
      #   convert_script_params("My Script")
      #   # => { "script": "My Script" }
      #
      #   convert_script_params(["My Script", "the param"])
      #   # => { "script": "My Script", "script.param": "the param" }
      #
      #   convert_script_params(after: "After Script", prerequest: "Prerequest Script")
      #   # => { "script": "After Script", "script.prerequest": "Prerequest Script" }
      #
      #   convert_script_params(presort: ["Presort Script", "foo"], prerequest: "Prerequest Script")
      #   # => {
      #   #      "script.presort": "After Script",
      #   #      "script.presort.param": "foo",
      #   #      "script.prerequest": "Prerequest Script"
      #   #    }
      #
      def convert_script_params(script_options)
        params = {}

        case script_options
        when String, Symbol
          params[:script] = script_options.to_s

        when Array
          params.merge!(convert_script_arguments(script_options))

        when Hash
          script_options.each_key do |key|
            next if VALID_SCRIPT_KEYS.include?(key)
            raise ArgumentError, "Invalid script option #{key.inspect}"
          end

          if script_options.has_key?(:prerequest)
            params.merge!(convert_script_arguments(script_options[:prerequest], :prerequest))
          end

          if script_options.has_key?(:presort)
            params.merge!(convert_script_arguments(script_options[:presort], :presort))
          end

          if script_options.has_key?(:after)
            params.merge!(convert_script_arguments(script_options[:after]))
          end
        end

        params
      end

      # Borrowed from `ERB::Util`
      #
      # This method is preferred to escape Data API URIs components over
      # `URI.encode_www_form_component` (and similar methods) because the
      # latter converts spaces to `+` instead of `%20`, which the Data API
      # doesn't seem to like.
      #
      # @param s [String] An URL component to encode
      # @return [String] The URL-encoded string
      def url_encode(s)
        s.to_s.b.gsub(/[^a-zA-Z0-9_\-.]/n) { |m|
          sprintf("%%%02X", m.unpack("C")[0])
        }
      end

      # Escapes FileMaker find operators from the given string in order to use
      # it in a find request.
      #
      # @param s [String] The string to escape
      # @return [String] A new string with find operators escaped with
      #   backslashes
      def escape_find_operators(s)
        s.gsub(FM_FIND_OPERATORS_RE, "\\\\\\1")
      end

      # Returns whether the given FileMaker field name is a fully-qualified
      # name. In other words, whether it contains the string "::".
      #
      # Note that this is a simple naive check which doesn't account for
      # invalid field names.
      #
      # @param field_name [String] The field name to test
      # @return [Boolean] Whether the field is a FQN
      def is_fully_qualified?(field_name)
        FULLY_QUALIFIED_FIELD_NAME_MATCHER === field_name.to_s
      end

      private

      def convert_script_arguments(script_arguments, suffix = nil)
        base = suffix ? "script.#{suffix}".to_sym : :script

        {}.tap do |params|
          case script_arguments
          when String, Symbol
            params[base] = script_arguments.to_s
          when Array
            params[base] = script_arguments.first.to_s
            params["#{base}.param".to_sym] = script_arguments[1] if script_arguments[1]
          else
            raise ArgumentError, "Script arguments are expected as a String, Symbol or Array"
          end
        end
      end
    end
  end
end