# frozen_string_literal: true

module AttrJson
  module Type
    # An ActiveModel::Type representing a particular AttrJson::Model
    # class, supporting casting, serialization, and deserialization from/to
    # JSON-able serializable hashes.
    #
    # You create one with AttrJson::Model::Type.new(attr_json_model_class),
    # but normally that's only done in AttrJson::Model.to_type, there isn't
    # an anticipated need to create from any other place.
    #
    #
    class Model < ::ActiveModel::Type::Value
      class BadCast < ArgumentError ; end

      attr_accessor :model, :strip_nils

      # @param model [AttrJson::Model] the model _class_ object
      # @param strip_nils [Symbol,Boolean] [true, false, or :safely]
      #  (default :safely), As a type, should we strip nils when serialiing?
      #  This value passed to AttrJson::Model#serialized_hash(strip_nils).
      #  by default it's :safely, we strip nils when it can be done safely
      #  to preserve default overrides.
      def initialize(model, strip_nils: :safely)
        #TODO type check, it really better be a AttrJson::Model. maybe?
        @model = model
        @strip_nils = strip_nils
      end

      def type
        model.to_param.underscore.to_sym
      end

      def cast(v)
        if v.nil?
          # important to stay nil instead of empty object, because they
          # are different things.
          v
        elsif v.kind_of? model
          v
        elsif v.respond_to?(:to_hash)
          # to_hash is actually the 'implicit' conversion, it really is a hash
          # even though it isn't is_a?(Hash), try to_hash first before to_h,
          # the explicit conversion.
          model.new(v.to_hash)
        elsif v.respond_to?(:to_h)
          # TODO Maybe we ought not to do this on #to_h?
          model.new(v.to_h)
        elsif model.attr_json_config.bad_cast == :as_nil
          # This was originally default behavior, to be like existing ActiveRecord
          # which kind of silently does this for non-castable basic values. That
          # ended up being confusing in the basic case, so now we raise by default,
          # but this is still configurable.
          nil
        else
          raise BadCast.new("Can not cast from #{v.inspect} to #{self.type}")
        end
      end

      def serialize(v)
        if v.nil?
          nil
        elsif v.kind_of?(model)
          v.serializable_hash(strip_nils: strip_nils)
        else
          (cast_v = cast(v)) && cast_v.serializable_hash(strip_nils: strip_nils)
        end
      end

      def deserialize(v)
        if v.nil?
          # important to stay nil instead of empty object, because they
          # are different things.
          v
        elsif v.kind_of? model
          v
        elsif v.respond_to?(:to_hash)
          # to_hash is actually the 'implicit' conversion, it really is a hash
          # even though it isn't is_a?(Hash), try to_hash first before to_h,
          # the explicit conversion.
          model.new_from_serializable(v.to_hash)
        elsif v.respond_to?(:to_h)
          # TODO Maybe we ought not to do this on #to_h? especially here in deserialize?
          model.new_from_serializable(v.to_h)
        elsif model.attr_json_config.bad_cast == :as_nil
          # TODO should we have different config value for bad_deserialize vs bad_cast?

          # This was originally default behavior, to be like existing ActiveRecord
          # which kind of silently does this for non-castable basic values. That
          # ended up being confusing in the basic case, so now we raise by default,
          # but this is still configurable.
          nil
        else
          raise BadCast.new("Can not cast from #{v.inspect} to #{self.type}")
        end
      end

      # these guys are definitely mutable, so we need this.
      def changed_in_place?(raw_old_value, new_value)
        serialize(new_value) != raw_old_value
      end

      # This is used only by our own keypath-chaining query stuff.
      def value_for_contains_query(key_path_arr, value)
        first_key, rest_keys = key_path_arr.first, key_path_arr[1..-1]
        attr_def = model.attr_json_registry.fetch(first_key)
        {
          attr_def.store_key => if rest_keys.present?
            attr_def.type.value_for_contains_query(rest_keys, value)
          else
            attr_def.serialize(attr_def.cast value)
          end
        }
      end
    end
  end
end
