# 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
    # A distributed map of multiple fields, such as counters, flags, registers,
    # sets, and, recursively, other maps, using the Riak 2 Data Types feature.
    #
    # Maps are complex, and the implementation is spread across many classes.
    # You're looking at the top-level {Map} class, but there are also others
    # that are also responsible for how maps work:
    #
    # * {InnerMap}: used for maps that live inside other maps
    # * {BatchMap}: proxies multiple operations into a single Riak update request
    # * {TypedCollection}: a collection of members of a single map, similar
    #   to a Ruby {Hash}
    # * {InnerFlag}: a boolean value inside a map
    # * {InnerRegister}: a {String} value inside a map
    # * {InnerCounter}: a {Riak::Crdt::Counter}, but inside a map
    # * {InnerSet}: a {Riak::Crdt::Set}, but inside a map
    #
    class Map < Base
      attr_reader :counters, :flags, :maps, :registers, :sets

      # Create a map instance. 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. The `Crdt::Base::DEFAULT_BUCKET_TYPES[:map]` entry
      #
      # @param bucket [Bucket] the {Riak::Bucket} for this map
      # @param [String, nil] key The name of the map. A nil key makes
      #        Riak assign a key.
      # @param [String] bucket_type The optional bucket type for this map.
      #        The default is in `Crdt::Base::DEFAULT_BUCKET_TYPES[:map]`.
      # @param options [Hash]
      def initialize(bucket, key, bucket_type = nil, options = {})
        super(bucket, key, bucket_type || :map, options)

        if key
          initialize_collections
        else
          initialize_blank_collections
        end
      end

      # Maps are frequently updated in batches. Use this method to get a
      # {BatchMap} to turn multiple operations into a single Riak update
      # request.
      #
      # @yieldparam batch_map [BatchMap] collects updates and other operations
      def batch(*args)
        batch_map = BatchMap.new self

        yield batch_map

        write_operations batch_map.operations, *args
      end

      # This method *for internal use only* is used to collect oprations from
      # disparate sources to provide a user-friendly API.
      #
      # @api private
      def operate(operation, *args)
        batch *args do |m|
          m.operate operation
        end
      end

      def pretty_print(pp)
        super pp do
          %w{counters flags maps registers sets}.each do |h|
            pp.comma_breakable
            pp.text "#{h}="
            pp.pp send h
          end
        end
      end

      def to_value_h
        %w{counters flags maps registers sets}.map do |k|
          [k, send(k).to_value_h]
        end.to_h
      end

      alias :value :to_value_h

      private
      def vivify(data)
        @counters = TypedCollection.new InnerCounter, self, data[:counters]
        @flags = TypedCollection.new InnerFlag, self, data[:flags]
        @maps = TypedCollection.new InnerMap, self, data[:maps]
        @registers = TypedCollection.new InnerRegister, self, data[:registers]
        @sets = TypedCollection.new InnerSet, self, data[:sets]
      end

      def initialize_collections(data = {})
        reload if dirty?
      end

      def initialize_blank_collections
        vivify counters: {}, flags: {}, maps: {}, registers: {}, sets: {}
      end

      def write_operations(operations, *args)
        result = operator do |op|
          op.operate(bucket.name,
                     key,
                     bucket_type,
                     operations,
                     *args
                     )
        end

        @key ||= result.key

        # collections break dirty tracking
        reload
      end
    end
  end
end
