require_relative "./field_meta"
require "date"

module ReeDto::DtoInstanceMethods
  include Ree::Contracts::Core
  include Ree::Contracts::ArgContracts

  FieldNotSetError = Class.new(ArgumentError)

  if ReeDto.debug_mode?
    contract Hash, Ksplat[RestKeys => Any] => Any
    def initialize(attrs = nil, **kwargs)
      @_attrs = attrs || kwargs
      list = self.class.fields.map(&:name)
      extra = attrs.keys - list

      if !extra.empty?
        puts("WARNING: #{self.class}.new does not have definition for #{extra.inspect} fields")
      end
    end
  else
    contract Hash, Ksplat[RestKeys => Any] => Any
    def initialize(attrs = nil, **kwargs)
      @_attrs = attrs || kwargs
    end
  end

  contract None => nil
  def reset_changes
    @changed_fields = nil
  end

  contract Symbol => ReeDto::FieldMeta
  def get_meta(name)
    self
      .class
      .fields
      .find { _1.name == name} || (raise ArgumentError.new("field :#{name} not defined for :#{self.class}"))
  end

  contract Symbol => Any
  def get_value(name)
    @_attrs.fetch(name) do
      meta = get_meta(name)

      if !meta.has_default?
        raise FieldNotSetError.new("field `#{name}` not set for: #{self}")
      else
        @_attrs[name] = meta.default
      end
    end
  end

  contract None => Hash
  def attrs
    @_attrs
  end

  contract Symbol, Any => Any
  def set_attr(name, val)
    @_attrs[name] = val
  end

  contract Symbol, Any => Any
  def set_value(name, val)
    if has_value?(name)
      old = get_value(name)
      return old if old == val
    end

    @changed_fields ||= Set.new
    @changed_fields << name
    @_attrs[name] = val
  end

  contract Symbol => Bool
  def has_value?(name)
    @_attrs.key?(name) || get_meta(name).has_default?
  end

  contract None => ArrayOf[Symbol]
  def changed_fields
    @changed_fields.to_a
  end

  def set_as_changed(name)
    if has_value?(name)
      @changed_fields ||= Set.new
      @changed_fields << name
    end
  end

  contract Block => Any
  def each_field(&proc)
    self.class.fields.select { has_value?(_1.name) }.each do |field|
      proc.call(field.name, get_value(field.name))
    end
  end

  contract None => String
  def to_s
    result = "#<dto #{self.class} "

    data = self.class.fields.select { has_value?(_1.name) }.map do |field|
      "#{field.name}=#{inspect_value(get_value(field.name))}"
    end

    data += self.class.collections.select { send(_1.name).size > 0 }.map do |col|
      "#{col.name}=#{send(col.name).inspect}"
    end

    result << data.join(", ")
    result << ">"
  end

  contract None => String
  def inspect
    to_s
  end

  private

  def inspect_value(v)
    if v.is_a?(DateTime) || v.is_a?(Date) || v.is_a?(Time)
      v.to_s.inspect
    else
      v.inspect
    end
  end
end