module KOSapiClient
  module Entity
    module DataMappings

      def self.included(base)
        base.extend(ClassMethods)
      end

      def to_hash
        result = {}
        self.class.attr_mappings.each_key { |k| result[k] = convert_value(send(k)) }
        result
      end

      private
      def convert_value(val)
        if val.respond_to? :to_hash
          val.to_hash
        elsif val.is_a?(Array)
          val.map { |it| convert_value(it) }
        else
          val
        end
      end

      module ClassMethods

        def map_data(name, type=String, opts = {})
          attr_accessor name
          opts[:type] = type
          @data_mappings ||= {}
          @data_mappings[name] = opts
        end

        def attr_mappings
          if self.superclass.respond_to? :attr_mappings
            parent_mappings = self.superclass.attr_mappings
          end
          (parent_mappings || {}).merge(@data_mappings)
        end

        # Parses composed domain type from hash response structure.
        #
        # @param [Hash] content hash structure from API response corresponding to single domain object
        # @return [BaseEntity] parsed domain object
        def parse(content, context = {})
          instance = new()
          set_mapped_attributes(instance, content)
          instance
        end

        # Creates new domain object instance and sets values
        # of mapped domain object attributes from source hash.
        # Attributes are mapped by .map_data method.
        def set_mapped_attributes(instance, source_hash)
          if self.superclass.respond_to? :set_mapped_attributes
            self.superclass.set_mapped_attributes(instance, source_hash)
          end
          raise "Missing data mappings for entity #{self}" unless @data_mappings
          @data_mappings.each do |name, options|
            set_mapped_attribute(instance, name, source_hash, options)
          end
        end

        private
        def set_mapped_attribute(instance, name, source_hash, mapping_options)
          namespace = mapping_options[:namespace]
          src_element = mapping_options[:element] || name
          if namespace
            key = "#{namespace}_#{src_element}".to_sym
          else
            key = src_element
          end
          value = retrieve_value(source_hash, key, mapping_options)
          if value.nil?
            raise "Missing value for attribute #{name}" if mapping_options[:required]
            if mapping_options[:type].is_a?(Array)
              value = []
            else
              return
            end
          else
            value = convert_type(value, mapping_options[:type])
          end
            instance.send("#{name}=".to_sym, value)
        end

        def convert_type(value, type)
          return value.to_i if type == Integer
          return value if type == String
          return convert_array(value, type.first) if type.is_a?(Array)

          return type.parse(value) if type.respond_to? :parse
          raise "Unknown type #{type} to convert value #{value} to."
        end

        # Converts values of array type to proper domain objects.
        # It checks whether the value is really an array, because
        # when API returns a single value it does not get parsed
        # into an array.
        def convert_array(values, type)
          if values.is_a?(Array)
            values.map { |it| convert_type(it, type) }
          else
            [ convert_type(values, type) ]
          end
        end

        def retrieve_value(source_hash, key, mapping_options)
          if (reader = mapping_options[:reader])
            return reader.call(source_hash, key)
          end

          if (path = mapping_options[:path])
            parent_element = source_hash[path]
          else
            parent_element = source_hash
          end
          parent_element[key] if parent_element
        end

      end
    end
  end
end
