module Jennifer::Model
  # :nodoc:
  module CommonMapping
    # :nodoc:
    macro common_mapping(strict)
      {%
        primary = COLUMNS_METADATA.keys.find { |field| COLUMNS_METADATA[field][:primary] }
        primary_auto_incrementable = false
        if primary
          primary_options = COLUMNS_METADATA[primary]
          primary_auto_incrementable = AUTOINCREMENTABLE_STR_TYPES.any? { |type| primary_options[:parsed_type].includes?(type) }
        end
        properties = COLUMNS_METADATA
        nonvirtual_attrs = properties.keys.select { |attr| !properties[attr][:virtual] }
      %}

      __field_declaration({{properties}}, {{primary_auto_incrementable}})

      # :nodoc:
      def self.field_count : Int32
        {{properties.size}}
      end

      # :nodoc:
      FIELD_NAMES = [{{properties.keys.map { |e| "#{e.id.stringify}" }.join(", ").id}}]

      # :nodoc:
      def self.field_names : Array(String)
        [{{properties.keys.map { |e| "#{e.id.stringify}" }.join(", ").id}}]
      end

      def self.column_names : Array(String)
        [{{properties.keys.select { |attr| !properties[attr][:virtual] }.map { |e| "#{e.id.stringify}" }.join(", ").id}}]
      end

      # :nodoc:
      def self.columns_tuple
        COLUMNS_METADATA
      end

      # Creates object from `DB::ResultSet`
      def initialize(%pull : DB::ResultSet)
        @new_record = false
        {{properties.keys.map { |key| "@#{key.id}" }.join(", ").id}} = _extract_attributes(%pull)
      end

      # :nodoc:
      def self.new(values : DB::ResultSet)
        {% verbatim do %}
        {% begin %}
          {% klasses = @type.all_subclasses.select { |s| s.constant("STI") == true } %}
          {% if !klasses.empty? %}
            hash = adapter.result_to_hash(values)
            case hash["type"]
            when "", nil, "{{@type}}"
              instance = allocate
              instance.initialize(hash, false)
              instance.__after_initialize_callback
              instance
            {% for klass in klasses %}
            when "{{klass}}"
              {{klass}}.new(hash, false)
            {% end %}
            else
              raise ::Jennifer::UnknownSTIType.new(self, hash["type"])
            end
          {% else %}
            instance = allocate
            instance.initialize(values)
            instance.__after_initialize_callback
            instance
          {% end %}
        {% end %}
        {% end %}
      end

      # Accepts symbol hash or named tuple, stringify it and calls constructor with string-based keys hash.
      def initialize(values : Hash(Symbol, AttrType) | NamedTuple, @new_record)
        initialize(Ifrit.stringify_hash(values, AttrType), @new_record)
      end

      # :nodoc:
      def self.new(values : Hash(Symbol, AttrType) | NamedTuple, new_record = true)
        {% verbatim do %}
        {% begin %}
          {% klasses = @type.all_subclasses.select { |s| s.constant("STI") == true } %}
          {% if !klasses.empty? %}
            case values[:type]?
            when "", nil, "{{@type}}"
              instance = allocate
              instance.initialize(values, new_record)
              instance.__after_initialize_callback
              instance
            {% for klass in klasses %}
            when "{{klass}}"
              {{klass}}.new(values, new_record)
            {% end %}
            else
              raise ::Jennifer::UnknownSTIType.new(self, values[:type])
            end
          {% else %}
            instance = allocate
            instance.initialize(values, new_record)
            instance.__after_initialize_callback
            instance
          {% end %}
        {% end %}
        {% end %}
      end

      def initialize(values : Hash(String, AttrType), @new_record)
        {{properties.keys.map { |key| "@#{key.id}" }.join(", ").id}} = _extract_attributes(values)
      end

      # :nodoc:
      def self.new(values : Hash(String, AttrType), new_record = true)
        {% verbatim do %}
        {% begin %}
          {% klasses = @type.all_subclasses.select { |s| s.constant("STI") == true } %}
          {% if !klasses.empty? %}
            case values["type"]?
            when "", nil, "{{@type}}"
              instance = allocate
              instance.initialize(values, new_record)
              instance.__after_initialize_callback
              instance
            {% for klass in klasses %}
            when "{{klass}}"
              {{klass}}.new(values, new_record)
            {% end %}
            else
              raise ::Jennifer::UnknownSTIType.new(self, values["type"])
            end
          {% else %}
            instance = allocate
            instance.initialize(values, new_record)
            instance.__after_initialize_callback
            instance
          {% end %}
        {% end %}
        {% end %}
      end

      # :nodoc:
      def to_h
        {
          {% for key in nonvirtual_attrs %}
            :{{key.id}} => {{key.id}},
          {% end %}
        } of Symbol => AttrType
      end

      # :nodoc:
      def to_str_h
        {
          {% for key in nonvirtual_attrs %}
            {{key.stringify}} => {{key.id}},
          {% end %}
        } of String => AttrType
      end

      # :nodoc:
      def attribute(name : String | Symbol, raise_exception : Bool = true)
        case name.to_s
        {% for attr in properties.keys %}
        when "{{attr.id}}"
          self.{{attr.id}}
        {% end %}
        else
          raise ::Jennifer::UnknownAttribute.new(name, self.class) if raise_exception
        end
      end

      # :nodoc:
      def attribute_before_typecast(name : String | Symbol) : ::Jennifer::DBAny
        case name.to_s
        {% for attr, options in properties %}
        when "{{attr.id}}"
          {% if options[:converter] %}
            {{options[:converter]}}.to_db(self.{{attr.id}}, self.class.columns_tuple[:{{attr.id}}])
          {% else %}
            self.{{attr.id}}
          {% end %}
        {% end %}
        else
          raise ::Jennifer::UnknownAttribute.new(name, self.class)
        end
      end

      private def init_attributes(values : Hash)
        {{properties.keys.map { |key| "@#{key.id}" }.join(", ").id}} = _extract_attributes(values)
      end

      private def init_attributes(values : DB::ResultSet)
        {{properties.keys.map { |key| "@#{key.id}" }.join(", ").id}} = _extract_attributes(values)
      end
    end
  end
end
