module Spaceship
  class ConnectAPI
    module Model
      def self.included(base)
        Spaceship::ConnectAPI::Models.types ||= []
        Spaceship::ConnectAPI::Models.types << base
        base.extend(Spaceship::ConnectAPI::Model)
      end

      attr_accessor :id
      attr_accessor :reverse_attr_map

      def initialize(id, attributes)
        self.id = id
        update_attributes(attributes)
      end

      def update_attributes(attributes)
        (attributes || []).each do |key, value|
          method = "#{key}=".to_sym
          self.send(method, value) if self.respond_to?(method)
        end
      end

      #
      # Example:
      # { "minOsVersion" => "min_os_version" }
      #
      # Creates attr_write and attr_reader for :min_os_version
      # Creates alias for :minOsVersion to :min_os_version
      #
      def attr_mapping(attr_map)
        self.reverse_attr_map ||= attr_map.invert.transform_keys(&:to_sym)
        attr_map.each do |key, value|
          # Actual
          reader = value.to_sym
          writer = "#{value}=".to_sym

          has_reader = instance_methods.include?(reader)
          has_writer = instance_methods.include?(writer)

          send(:attr_reader, value) unless has_reader
          send(:attr_writer, value) unless has_writer

          # Alias
          key_reader = key.to_sym
          key_writer = "#{key}=".to_sym

          # Alias the API response name to attribute name
          alias_method(key_reader, reader)
          alias_method(key_writer, writer)
        end
      end

      def reverse_attr_mapping(attributes)
        return nil if attributes.nil?

        # allows for getting map from either an instance or class execution
        map = self.reverse_attr_map || self.class.reverse_attr_map

        attributes.each_with_object({}) do |(k, v), memo|
          key = map[k.to_sym] || k.to_sym
          memo[key] = v
        end
      end

      def to_json(*options)
        instance_variables.map do |var|
          [var.to_s[1..-1], instance_variable_get(var)]
        end.to_h.to_json(*options)
      end
    end

    module Models
      class << self
        attr_accessor :types
        attr_accessor :types_cache
      end

      def self.parse(json)
        data = json["data"]
        raise "No data" unless data

        included = json["included"] || []

        if data.kind_of?(Hash)
          inflate_model(data, included)
        elsif data.kind_of?(Array)
          return data.map do |model_data|
            inflate_model(model_data, included)
          end
        else
          raise "'data' is neither a hash nor an array"
        end
      end

      def self.find_class(model_data)
        # Initialize cache
        @types_cache ||= {}

        # Find class in cache
        type_string = model_data["type"]
        type_class = @types_cache[type_string]
        return type_class if type_class

        # Find class in array
        type_class = @types.find do |type|
          type.type == type_string
        end

        # Cache and return class
        @types_cache[type_string] = type_class
        return type_class
      end

      def self.inflate_model(model_data, included)
        # Find class
        type_class = find_class(model_data)
        raise "No type class found for #{model_data['type']}" unless type_class

        # Get id and attributes needed for inflating
        id = model_data["id"]
        attributes = model_data["attributes"]

        # Instantiate object and inflate relationships
        relationships = model_data["relationships"] || []
        type_instance = type_class.new(id, attributes)
        type_instance = inflate_model_relationships(type_instance, relationships, included)

        return type_instance
      end

      def self.inflate_model_relationships(type_instance, relationships, included)
        # Relationship attributes to set
        attributes = {}

        # 1. Iterate over relationships
        # 2. Find id and type
        # 3. Find matching id and type in included
        # 4. Inflate matching data and set in attributes
        relationships.each do |key, value|
          # Validate data exists
          value_data_or_datas = value["data"]
          next unless value_data_or_datas

          # Map an included data object
          map_data = lambda do |value_data|
            id = value_data["id"]
            type = value_data["type"]

            relationship_data = included.find do |included_data|
              id == included_data["id"] && type == included_data["type"]
            end

            inflate_model(relationship_data, included) if relationship_data
          end

          # Map a hash or an array of data
          if value_data_or_datas.kind_of?(Hash)
            attributes[key] = map_data.call(value_data_or_datas)
          elsif value_data_or_datas.kind_of?(Array)
            attributes[key] = value_data_or_datas.map(&map_data)
          end
        end

        type_instance.update_attributes(attributes)

        return type_instance
      end
    end
  end
end
