# frozen_string_literal: true

require 'dry/equalizer'

module ROM
  class Header
    # An attribute provides information about a specific attribute in a tuple
    #
    # This may include information about how an attribute should be renamed,
    # or how its value should coerced.
    #
    # More complex attributes describe how an attribute should be transformed.
    #
    # @private
    class Attribute
      include Dry::Equalizer(:name, :key, :type)

      # @return [Symbol] name of an attribute
      #
      # @api private
      attr_reader :name

      # @return [Symbol] key of an attribute that corresponds to tuple attribute
      #
      # @api private
      attr_reader :key

      # @return [Symbol] type identifier (defaults to :object)
      #
      # @api private
      attr_reader :type

      # @return [Hash] additional meta information
      #
      # @api private
      attr_reader :meta

      # Return attribute class for a given meta hash
      #
      # @param [Hash] meta hash with type information and optional transformation info
      #
      # @return [Class]
      #
      # @api private
      def self.[](meta)
        key = (meta.keys & TYPE_MAP.keys).first
        TYPE_MAP.fetch(key || meta[:type], self)
      end

      # Coerce an array with attribute meta-data into an attribute object
      #
      # @param [Array<Symbol,Hash>] input attribute name/options pair
      #
      # @return [Attribute]
      #
      # @api private
      def self.coerce(input)
        name = input[0]
        meta = (input[1] || {}).dup

        meta[:type] ||= :object

        if meta.key?(:header)
          meta[:header] = Header.coerce(meta[:header], model: meta[:model])
        end

        self[meta].new(name, meta)
      end

      # @api private
      def initialize(name, meta)
        @name = name
        @meta = meta
        @key = meta.fetch(:from) { name }
        @type = meta.fetch(:type)
      end

      # Return if an attribute has a specific type identifier
      #
      # @api private
      def typed?
        type != :object
      end

      # Return if an attribute should be aliased
      #
      # @api private
      def aliased?
        key != name
      end

      # Return :key-to-:name mapping hash
      #
      # @return [Hash]
      #
      # @api private
      def mapping
        { key => name }
      end

      def union?
        key.is_a? ::Array
      end
    end

    # Embedded attribute is a special attribute type that has a header
    #
    # This is the base of complex attributes like Hash or Group
    #
    # @private
    class Embedded < Attribute
      include Dry::Equalizer(:name, :key, :type, :header)

      # return [Header] header of an attribute
      #
      # @api private
      attr_reader :header

      # @api private
      def initialize(*)
        super
        @header = meta.fetch(:header)
      end

      # Return tuple keys from the header
      #
      # @return [Array<Symbol>]
      #
      # @api private
      def tuple_keys
        header.tuple_keys
      end

      def pop_keys
        header.pop_keys
      end
    end

    # Array is an embedded attribute type
    Array = Class.new(Embedded)

    # Hash is an embedded attribute type
    Hash = Class.new(Embedded)

    # Combined is an embedded attribute type describing combination of multiple
    # relations
    Combined = Class.new(Embedded)

    # Wrap is a special type of Hash attribute that requires wrapping
    # transformation
    Wrap = Class.new(Hash)

    # Unwrap is a special type of Hash attribute that requires unwrapping
    # transformation
    Unwrap = Class.new(Hash)

    # Group is a special type of Array attribute that requires grouping
    # transformation
    Group = Class.new(Array)

    # Ungroup is a special type of Array attribute that requires ungrouping
    # transformation
    Ungroup = Class.new(Array)

    # Fold is a special type of Array attribute that requires folding
    # transformation
    Fold = Class.new(Array)

    # Unfold is a special type of Array attribute that requires unfolding
    # transformation
    Unfold = Class.new(Array)

    # Exclude is a special type of Attribute to be removed
    Exclude = Class.new(Attribute)

    # TYPE_MAP is a (hash) map of ROM::Header identifiers to ROM::Header types
    #
    # @private
    TYPE_MAP = {
      combine: Combined,
      wrap: Wrap,
      unwrap: Unwrap,
      group: Group,
      ungroup: Ungroup,
      fold: Fold,
      unfold: Unfold,
      hash: Hash,
      array: Array,
      exclude: Exclude
    }
  end
end
