# frozen_string_literal: true

require_relative 'extension'

module Puppet::Pops
module Serialization
  # The serializer is capable of writing, arrays, maps, and complex objects using an underlying protocol writer. It takes care of
  # tabulating and disassembling complex objects.
  # @api public
  class Serializer
    # Provides access to the writer.
    # @api private
    attr_reader :writer

    # @param writer [AbstractWriter] the writer that is used for writing primitive values
    # @param options [{String, Object}] serialization options
    # @option options [Boolean] :type_by_reference `true` if Object types are serialized by name only.
    # @api public
    def initialize(writer, options = EMPTY_HASH)
      # Hash#compare_by_identity is intentionally not used since we only need to map
      # object ids to their size and we don't want to store entire objects in memory
      @written = {}
      @writer = writer
      @options = options
    end

    # Tell the underlying writer to finish
    # @api public
    def finish
      @writer.finish
    end

    # Write an object
    # @param [Object] value the object to write
    # @api public
    def write(value)
      case value
      when Integer, Float, String, true, false, nil
        @writer.write(value)
      when :default
        @writer.write(Extension::Default::INSTANCE)
      else
        index = @written[value.object_id] # rubocop:disable Lint/HashCompareByIdentity
        if index.nil?
          write_tabulated_first_time(value)
        else
          @writer.write(Extension::Tabulation.new(index))
        end
      end
    end

    # Write the start of an array.
    # @param [Integer] size the size of the array
    # @api private
    def start_array(size)
      @writer.write(Extension::ArrayStart.new(size))
    end

    # Write the start of a map (hash).
    # @param [Integer] size the number of entries in the map
    # @api private
    def start_map(size)
      @writer.write(Extension::MapStart.new(size))
    end

    # Write the start of a complex pcore object
    # @param [String] type_ref the name of the type
    # @param [Integer] attr_count the number of attributes in the object
    # @api private
    def start_pcore_object(type_ref, attr_count)
      @writer.write(Extension::PcoreObjectStart.new(type_ref, attr_count))
    end

    # Write the start of a complex object
    # @param [Integer] attr_count the number of attributes in the object
    # @api private
    def start_object(attr_count)
      @writer.write(Extension::ObjectStart.new(attr_count))
    end

    def push_written(value)
      @written[value.object_id] = @written.size # rubocop:disable Lint/HashCompareByIdentity
    end

    # Write the start of a sensitive object
    # @api private
    def start_sensitive
      @writer.write(Extension::SensitiveStart::INSTANCE)
    end

    def type_by_reference?
      @options[:type_by_reference] == true
    end

    def to_s
      "#{self.class.name} with #{@writer}"
    end

    def inspect
      to_s
    end

    # First time write of a tabulated object. This means that the object is written and then remembered. Subsequent writes
    # of the same object will yield a write of a tabulation index instead.
    # @param [Object] value the value to write
    # @api private
    def write_tabulated_first_time(value)
      if value.instance_of?(Symbol) ||
         value.instance_of?(Regexp) ||
         value.instance_of?(SemanticPuppet::Version) ||
         value.instance_of?(SemanticPuppet::VersionRange) ||
         value.instance_of?(Time::Timestamp) ||
         value.instance_of?(Time::Timespan) ||
         value.instance_of?(Types::PBinaryType::Binary) ||
         value.is_a?(URI)
        push_written(value)
        @writer.write(value)
      elsif value.instance_of?(Array)
        push_written(value)
        start_array(value.size)
        value.each { |elem| write(elem) }
      elsif value.instance_of?(Hash)
        push_written(value)
        start_map(value.size)
        value.each_pair { |key, val| write(key); write(val) }
      elsif value.instance_of?(Types::PSensitiveType::Sensitive)
        start_sensitive
        write(value.unwrap)
      elsif value.instance_of?(Types::PTypeReferenceType)
        push_written(value)
        @writer.write(value)
      elsif value.is_a?(Types::PuppetObject)
        value._pcore_type.write(value, self)
      else
        impl_class = value.class
        type = Loaders.implementation_registry.type_for_module(impl_class)
        raise SerializationError, _("No Puppet Type found for %{klass}") % { klass: impl_class.name } unless type.is_a?(Types::PObjectType)

        type.write(value, self)
      end
    end
  end
end
end
