# Copyright 2010-present Basho Technologies, Inc.
#
# 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
#
#     http://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.

module Riak
  module Crdt
    # Basic and shared code used by the top-level CRDTs. In particular, dirty-
    # tracking, loading, and operating is implemented by this class, and
    # the {Riak::Crdt::Set}, {Riak::Crdt::HyperLogLog}, {Riak::Crdt::Counter}, and {Riak::Crdt::Map}
    # classes implement everything else.
    #
    # @api private
    class Base
      include Util::Translation
      attr_reader :bucket
      attr_reader :bucket_type

      # Returns the key of this CRDT. Extremely useful when using a
      # Riak-assigned key.
      attr_reader :key

      # Base CRDT initialization The bucket type is determined by the first of
      # these sources:
      #
      # 1. The `bucket_type` String argument
      # 2. A {BucketTyped::Bucket} as the `bucket` argument
      # 3. A `bucket_type` Symbol argument is looked up in the
      #    `Crdt::Base::DEFAULT_BUCKET_TYPES` hash
      # @api private
      #
      # @param [Bucket] bucket the {Riak::Bucket} for this counter
      # @param [String, nil] key The name of the counter. A nil key makes
      #        Riak assign a key.
      # @param [String] bucket_type The optional bucket type for this counter.
      #        The default is in `Crdt::Base::DEFAULT_BUCKET_TYPES[:counter]`.
      # @param [Hash] options
      def initialize(bucket, key, bucket_type, options = {})
        configure_bucket bucket
        configure_key key
        configure_bucket_type bucket_type
        @options = options

        @dirty = true
      end

      def dirty?
        @dirty
      end

      # Force a reload of this structure from Riak.
      def reload
        loader do |l|
          vivify l.load @bucket, @key, @bucket_type
          @context = l.context
        end
        @dirty = false

        self
      end

      # Does this CRDT have the context necessary to remove elements?
      #
      # @return [Boolean] if the set has a defined context
      def context?
        !!@context
      end

      def ==(other)
        return false unless self.class == other.class
        return false unless self.bucket_type == other.bucket_type
        return false unless self.bucket == other.bucket
        return false unless self.key == other.key
        return true
      end

      def pretty_print(pp)
        pp.object_group self do
          pp.breakable
          pp.text "bucket_type=#{@bucket_type}"
          pp.comma_breakable
          pp.text "bucket=#{@bucket.name}"
          pp.comma_breakable
          pp.text "key=#{@key}"

          yield
        end
      end

      def pretty_print_cycle(pp)
        pp.object_group self do
          pp.breakable
          pp.text "#{@bucket_type}/#{@bucket.name}/#{@key}"
        end
      end

      def inspect_name
        "#<#{self.class.name} bucket=#{@bucket.name} " \
        "key=#{@key} type=#{@bucket_type}>"
      end

      private

      def client
        @bucket.client
      end

      def backend(&blk)
        client.backend(&blk)
      end

      def loader
        backend do |be|
          yield be.crdt_loader
        end
      end

      def operator
        backend do |be|
          yield be.crdt_operator
        end
      end

      def operate(*args)
        options = {}
        options = args.pop if args.last.is_a? Hash
        options[:context] ||= @context
        result = operator do |op|
          response = op.operate(bucket.name,
                                key,
                                bucket_type,
                                *args,
                                options
                                )

          break if :empty == response
          @key = response.key if response.key
          response
        end

        @dirty = true
        vivify_returnbody(result)

        true
      end

      def vivify_returnbody(result)
        loader do |l|
          specific_loader = l.get_loader_for_value result

          return false if specific_loader.nil?

          vivify specific_loader.rubyfy
          @context = result.context unless result.context.nil?
          @dirty = false
        end
      end

      def configure_bucket_type(constructor_type)
        @bucket_type = if constructor_type.is_a? String
                         constructor_type
                       elsif constructor_type.is_a? BucketType
                         constructor_type.name
                       elsif @bucket.is_a? BucketTyped::Bucket
                         @bucket.type.name
                       elsif constructor_type.is_a? Symbol
                         DEFAULT_BUCKET_TYPES[constructor_type]
                       end
      end

      def configure_bucket(bucket)
        unless bucket.is_a? Bucket
          fail ArgumentError, t('bucket_type', bucket: bucket.inspect)
        end

        @bucket = bucket
      end

      def configure_key(key)
        unless key.is_a?(String) || key.nil?
          fail ArgumentError, t('string_type', string: key.inspect)
        end

        @key = key
      end
    end
  end
end
