require 'active_support/inflector'

module Frederick
  module Models
    class Model
      def initialize(options = {})
        options.each do |key, value|
          send(:"#{key}=", value)
        end
      end

      def to_hash
        hash = {}
        self.instance_variables.each do |var|
          value = self.instance_variable_get var
          if value.is_a? Array
            new_value = hash_list(value)
          elsif value.is_a? Frederick::Models::Model
            new_value = value.to_hash
          else
            new_value = value
          end
          hash[var[1..-1]] = new_value
        end
        hash
      end

      def to_json
        Oj.dump(to_hash, mode: :compat)
      end

      def attr_values
        attr_values = {}
        self.class::ATTRS.each do |name|
          instance_var_value = instance_variable_get("@#{name}")
          attr_values[name] = instance_var_value unless instance_var_value.nil?
        end
        attr_values
      end

      # Built in Hash#hash method returns diff code across ruby instances
      # See: http://stackoverflow.com/questions/6783811/why-is-ruby-string-hash-inconsistent-across-machines
      # Instead use MD5 to ensure hashes for objects with same state (instance_values) are always the same
      # Use MD5 hashing algorithm over SHA because it is faster
      # Chance of collision using MD5 is still infinitesimally small (1/2^128)

      # WARNING! CHANGING ORDER OF WHEN YOUR INSTANCE VARS ARE DEFINED
      # OR CHANGING WHICH INSTANCE VARS ARE SET PER INSTANCE
      # WILL RETURN NEW HASH CODES
      # AS THIS WILL CHANGE THE INSTANCE VALUES STRING THAT IS PASSED INTO THE DIGEST ROUTINE
      def hash; Digest::MD5.hexdigest(attr_values.to_s); end

      def self.hash_codes_with_index(models)
        hash_code_map = {}
        models.each_with_index { |model, i|  hash_code_map[model.hash] = i }
        hash_code_map
      end

      def self.from_hash(hash)
        model = self.new
        hash.each do |key, value|
          if model.respond_to?(:"#{key}")
            constantized = self.constantize(key)
            if constantized
              if value.is_a? Array
                model.send(:"#{key}=", constantized.from_list(value))
              else
                model.send(:"#{key}=", constantized.from_hash(value))
              end
            else
              model.send(:"#{key}=", value)
            end
          end
        end
        model
      end

      def self.from_list(array)
        list = []
        array.each do |item|
          model = self.from_hash(item)
          list << model
        end
        list
      end

      def self.constantize(key)
        begin
          Frederick::Models.const_get("Frederick::Models::#{ActiveSupport::Inflector.camelize(ActiveSupport::Inflector.singularize("#{key}"))}")
        rescue NameError
          nil
        end
      end

      private

      def hash_list(array)
        list = []
        array.each do |item|
          if item.is_a? Array
            list << hash_list(item)
          elsif item.is_a? Frederick::Models::Model
            list << item.to_hash
          else
            list << item
          end
        end
        list
      end
    end
  end
end
