# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


require "google/cloud/firestore/v1"

module Google
  module Cloud
    module Firestore
      ##
      # Represents the filter for structured query.
      #
      class Filter
        ##
        # @private Object of type
        # Google::Cloud::Firestore::V1::StructuredQuery::Filter
        attr_accessor :filter

        ##
        # Create a Filter object.
        #
        # @param field [FieldPath, String, Symbol] A field path to filter
        #   results with.
        #   If a {FieldPath} object is not provided then the field will be
        #   treated as a dotted string, meaning the string represents individual
        #   fields joined by ".". Fields containing `~`, `*`, `/`, `[`, `]`, and
        #   `.` cannot be in a dotted string, and should provided using a
        #   {FieldPath} object instead.
        #
        # @param operator [String, Symbol] The operation to compare the field
        #   to. Acceptable values include:
        #   * less than: `<`, `lt`
        #   * less than or equal: `<=`, `lte`
        #   * greater than: `>`, `gt`
        #   * greater than or equal: `>=`, `gte`
        #   * equal: `=`, `==`, `eq`, `eql`, `is`
        #   * not equal: `!=`
        #   * in: `in`
        #   * not in: `not-in`, `not_in`
        #   * array contains: `array-contains`, `array_contains`
        #
        # @param value [Object] The value to compare the property to. Defaults to nil.
        #   Possible values are:
        #   * Integer
        #   * Float/BigDecimal
        #   * String
        #   * Boolean
        #   * Array
        #   * Date/Time
        #   * StringIO
        #   * Google::Cloud::Datastore::Key
        #   * Google::Cloud::Datastore::Entity
        #   * nil
        #
        # @return [Google::Cloud::Firestore::Filter] New filter for the given condition
        #
        # @example
        #   require "google/cloud/firestore"
        #
        #   firestore = Google::Cloud::Firestore.new
        #
        #   # Create a Filter
        #   Google::Cloud::Firestore::Filter.new(:population, :>=, 1000000)
        #
        def initialize field, operator, value
          @filter = create_filter field, operator, value
        end

        ##
        # Joins filter using AND operator.
        #
        # @overload where(filter)
        #   Pass Firestore::Filter to `where` via field_or_filter argument.
        #
        #  @param filter [::Google::Cloud::Firestore::Filter]
        #
        # @overload where(field, operator, value)
        #   Pass arguments to `where` via positional arguments.
        #
        #   @param field [FieldPath, String, Symbol] A field path to filter
        #     results with.
        #     If a {FieldPath} object is not provided then the field will be
        #     treated as a dotted string, meaning the string represents individual
        #     fields joined by ".". Fields containing `~`, `*`, `/`, `[`, `]`, and
        #     `.` cannot be in a dotted string, and should provided using a
        #     {FieldPath} object instead.
        #
        #   @param operator [String, Symbol] The operation to compare the field
        #     to. Acceptable values include:
        #     * less than: `<`, `lt`
        #     * less than or equal: `<=`, `lte`
        #     * greater than: `>`, `gt`
        #     * greater than or equal: `>=`, `gte`
        #     * equal: `=`, `==`, `eq`, `eql`, `is`
        #     * not equal: `!=`
        #     * in: `in`
        #     * not in: `not-in`, `not_in`
        #     * array contains: `array-contains`, `array_contains`
        #
        #   @param value [Object] The value to compare the property to. Defaults to nil.
        #     Possible values are:
        #     * Integer
        #     * Float/BigDecimal
        #     * String
        #     * Boolean
        #     * Array
        #     * Date/Time
        #     * StringIO
        #     * Google::Cloud::Datastore::Key
        #     * Google::Cloud::Datastore::Entity
        #     * nil
        #
        # @return [Filter] New Filter object.
        #
        # @example Pass a Filter type object in argument
        #   require "google/cloud/firestore"
        #
        #   filter_1 = Google::Cloud::Firestore.Firestore.new(:population, :>=, 1000000)
        #   filter_2 = Google::Cloud::Firestore.Firestore.new("done", "=", "false")
        #
        #   filter = filter_1.and(filter_2)
        #
        # @example Pass filter conditions in the argument
        #   require "google/cloud/firestore"
        #
        #   filter_1 = Google::Cloud::Firestore.Firestore.new(:population, :>=, 1000000)
        #
        #   filter = filter_1.and("done", "=", "false")
        #
        def and filter_or_field = nil, operator = nil, value = nil
          combine_filters composite_filter_and, filter_or_field, operator, value
        end

        ##
        # Joins filter using OR operator.
        #
        # @overload where(filter)
        #   Pass Firestore::Filter to `where` via field_or_filter argument.
        #
        #  @param filter [::Google::Cloud::Firestore::Filter]
        #
        # @overload where(field, operator, value)
        #   Pass arguments to `where` via positional arguments.
        #
        #   @param field [FieldPath, String, Symbol] A field path to filter
        #     results with.
        #     If a {FieldPath} object is not provided then the field will be
        #     treated as a dotted string, meaning the string represents individual
        #     fields joined by ".". Fields containing `~`, `*`, `/`, `[`, `]`, and
        #     `.` cannot be in a dotted string, and should provided using a
        #     {FieldPath} object instead.
        #
        #   @param operator [String, Symbol] The operation to compare the field
        #     to. Acceptable values include:
        #     * less than: `<`, `lt`
        #     * less than or equal: `<=`, `lte`
        #     * greater than: `>`, `gt`
        #     * greater than or equal: `>=`, `gte`
        #     * equal: `=`, `==`, `eq`, `eql`, `is`
        #     * not equal: `!=`
        #     * in: `in`
        #     * not in: `not-in`, `not_in`
        #     * array contains: `array-contains`, `array_contains`
        #
        #   @param value [Object] The value to compare the property to. Defaults to nil.
        #     Possible values are:
        #     * Integer
        #     * Float/BigDecimal
        #     * String
        #     * Boolean
        #     * Array
        #     * Date/Time
        #     * StringIO
        #     * Google::Cloud::Datastore::Key
        #     * Google::Cloud::Datastore::Entity
        #     * nil
        #
        # @return [Filter] New Filter object.
        #
        # @example Pass a Filter type object in argument
        #   require "google/cloud/firestore"
        #
        #   filter_1 = Google::Cloud::Firestore.Firestore.new(:population, :>=, 1000000)
        #   filter_2 = Google::Cloud::Firestore.Firestore.new("done", "=", "false")
        #
        #   filter = filter_1.or(filter_2)
        #
        # @example Pass filter conditions in the argument
        #   require "google/cloud/firestore"
        #
        #   filter_1 = Google::Cloud::Firestore.Firestore.new(:population, :>=, 1000000)
        #
        #   filter = filter_1.or("done", "=", "false")
        #
        def or filter_or_field = nil, operator = nil, value = nil
          combine_filters composite_filter_or, filter_or_field, operator, value
        end

        private

        ##
        # @private
        StructuredQuery = Google::Cloud::Firestore::V1::StructuredQuery

        ##
        # @private
        FILTER_OPS = {
          "<"                  => :LESS_THAN,
          "lt"                 => :LESS_THAN,
          "<="                 => :LESS_THAN_OR_EQUAL,
          "lte"                => :LESS_THAN_OR_EQUAL,
          ">"                  => :GREATER_THAN,
          "gt"                 => :GREATER_THAN,
          ">="                 => :GREATER_THAN_OR_EQUAL,
          "gte"                => :GREATER_THAN_OR_EQUAL,
          "="                  => :EQUAL,
          "=="                 => :EQUAL,
          "eq"                 => :EQUAL,
          "eql"                => :EQUAL,
          "is"                 => :EQUAL,
          "!="                 => :NOT_EQUAL,
          "array_contains"     => :ARRAY_CONTAINS,
          "array-contains"     => :ARRAY_CONTAINS,
          "include"            => :ARRAY_CONTAINS,
          "include?"           => :ARRAY_CONTAINS,
          "has"                => :ARRAY_CONTAINS,
          "in"                 => :IN,
          "not_in"             => :NOT_IN,
          "not-in"             => :NOT_IN,
          "array_contains_any" => :ARRAY_CONTAINS_ANY,
          "array-contains-any" => :ARRAY_CONTAINS_ANY
        }.freeze

        ##
        # @private
        INEQUALITY_FILTERS = [
          :LESS_THAN,
          :LESS_THAN_OR_EQUAL,
          :GREATER_THAN,
          :GREATER_THAN_OR_EQUAL
        ].freeze

        def composite_filter_and
          StructuredQuery::Filter.new(
            composite_filter: StructuredQuery::CompositeFilter.new(op: :AND)
          )
        end

        def composite_filter_or
          StructuredQuery::Filter.new(
            composite_filter: StructuredQuery::CompositeFilter.new(op: :OR)
          )
        end

        def combine_filters new_filter, filter_or_field, operator, value
          new_filter.composite_filter.filters << @filter
          new_filter.composite_filter.filters << if filter_or_field.is_a? Google::Cloud::Firestore::Filter
                                                   filter_or_field.filter
                                                 else
                                                   create_filter filter_or_field, operator, value
                                                 end
          dup.tap do |f|
            f.filter = new_filter
          end
        end

        def value_nil? value
          [nil, :null, :nil].include? value
        end

        def value_nan? value
          # Comparing NaN values raises, so check for #nan? first.
          return true if value.respond_to?(:nan?) && value.nan?
          [:nan].include? value
        end

        def value_unary? value
          value_nil?(value) || value_nan?(value)
        end

        def create_filter field, op_key, value
          return if field.nil? && op_key.nil? && value.nil?
          field = FieldPath.parse field unless field.is_a? FieldPath
          field = StructuredQuery::FieldReference.new field_path: field.formatted_string.to_s
          operator = FILTER_OPS[op_key.to_s.downcase]
          raise ArgumentError, "unknown operator #{op_key}" if operator.nil?

          if value_unary? value
            operator = case operator
                       when :EQUAL
                         value_nan?(value) ? :IS_NAN : :IS_NULL
                       when :NOT_EQUAL
                         value_nan?(value) ? :IS_NOT_NAN : :IS_NOT_NULL
                       else
                         raise ArgumentError, "can only perform '==' and '!=' comparisons on #{value} values"
                       end

            return StructuredQuery::Filter.new(
              unary_filter: StructuredQuery::UnaryFilter.new(
                field: field, op: operator
              )
            )
          end

          value = Convert.raw_to_value value
          StructuredQuery::Filter.new(
            field_filter: StructuredQuery::FieldFilter.new(
              field: field, op: operator, value: value
            )
          )
        end
      end
    end
  end
end
