# frozen_string_literal: true

module JSI
  # this is an ActiveRecord serialization coder intended to serialize between
  # JSON-compatible objects on the database side, and a JSI instance loaded on
  # the model attribute.
  #
  # on its own this coder is useful with a JSON database column. in order to
  # serialize further to a string of JSON, or to YAML, the gem `arms` allows
  # coders to be chained together. for example, for a table `foos` and a column
  # `preferences_json` which is an actual json column, and `preferences_txt`
  # which is a string:
  #
  #     Preferences = JSI.new_schema_module(preferences_json_schema)
  #     class Foo < ActiveRecord::Base
  #       # as a single serializer, loads a Preferences instance from a json column
  #       serialize 'preferences_json', JSI::JSICoder.new(Preferences)
  #
  #       # for a text column, arms_serialize will go from JSI to JSON-compatible
  #       # objects to a string. the symbol `:jsi` is a shortcut for JSI::JSICoder.
  #       arms_serialize 'preferences_txt', [:jsi, Preferences], :json
  #     end
  #
  # the column data may be either a single instance of the schema class
  # (represented as one json object) or an array of them (represented as a json
  # array of json objects), indicated by the keyword argument `array`.
  class JSICoder
    # @param schema [#new_jsi] a Schema, SchemaSet, or JSI schema module. #load
    #   will instantiate column data using the JSI schemas represented.
    # @param array [Boolean] whether the dumped data represent one instance of the schema,
    #   or an array of them. note that it may be preferable to simply use an array schema.
    # @param jsi_opt [Hash] keyword arguments to pass to {Schema#new_jsi} when loading
    def initialize(schema, array: false, jsi_opt: {})
      unless schema.respond_to?(:new_jsi)
        raise(ArgumentError, "schema param does not respond to #new_jsi: #{schema.inspect}")
      end
      @schema = schema
      @array = array
      @jsi_opt = jsi_opt
    end

    # loads the database column to JSI instances of our schema
    #
    # @param data [Object, Array, nil] the dumped schema instance(s) of the JSI(s)
    # @return [JSI::Base, Array<JSI::Base>, nil] the JSI or JSIs containing the schema
    #   instance(s), or nil if data is nil
    def load(data)
      return nil if data.nil?

      if @array
        unless data.respond_to?(:to_ary)
          raise TypeError, "expected array-like column data; got: #{data.class}: #{data.inspect}"
        end
        data.to_ary.map { |el| load_object(el) }
      else
        load_object(data)
      end
    end

    # dumps the object for the database
    # @param object [JSI::Base, Array<JSI::Base>, nil] the JSI or array of JSIs containing
    #   the schema instance(s)
    # @return [Object, Array, nil] the schema instance(s) of the JSI(s), or nil if object is nil
    def dump(object)
      return nil if object.nil?

      if @array
        unless object.respond_to?(:to_ary)
          raise(TypeError, "expected array-like attribute; got: #{object.class}: #{object.inspect}")
        end
        object.to_ary.map do |el|
          dump_object(el)
        end
      else
        dump_object(object)
      end
    end

    private
    # @param data [Object]
    # @return [JSI::Base]
    def load_object(data)
      @schema.new_jsi(data, **@jsi_opt)
    end

    # @param object [JSI::Base, Object]
    # @return [Object]
    def dump_object(object)
      JSI::Util.as_json(object)
    end
  end
end