module ApiResource

  #
  # Error raised when accessing an attribute in an invalid
  # way such as trying to write to a protected attribute
  #
  # @author [ejlangev]
  #
  class AttributeAccessError < NoMethodError
  end

  module Attributes

    extend ActiveSupport::Concern

    include ActiveModel::AttributeMethods
    include ActiveModel::Dirty

    included do

      # include ApiResource::Typecast if it isn't already
      include ApiResource::Typecast

      alias_method_chain :save, :dirty_tracking

      # Set up some class attributes for managing all of
      # this
      class_attribute(
        :attribute_names,
        :public_attribute_names,
        :protected_attribute_names,
        :attribute_types,
        :primary_key,
        :attribute_method_module
      )
      # Initialize those class attributes
      self.attribute_names = []
      self.public_attribute_names = []
      self.protected_attribute_names = []
      self.attribute_types = {}.with_indifferent_access

      self.primary_key = :id
      self.attribute_method_module = Module.new

      include self.attribute_method_module
      # This method is important for reloading an object. If the
      # object has already been loaded, its associations will trip
      # up the load method unless we pass in the internal objects.
      #
      # TODO: This seems like kind of a hack that shouldn't be
      # necessary.  Remove it at some point during the refactoring
      define_method(:attributes_without_proxies) do
        attributes = @attributes

        if attributes.nil?
          attributes = self.class.attribute_names.each do |attr|
            attributes[attr] = self.send("#{attr}")
          end
        end

        attributes.each do |k,v|
          if v.respond_to?(:internal_object)
            if v.internal_object.present?
              internal = v.internal_object
              if internal.is_a?(Array)
                attributes[k] = internal.collect{|item| item.attributes}
              else
                attributes[k] = internal.attributes
              end
            else
              attributes[k] = nil
            end
          end
        end

        attributes
      end

    end

    module ClassMethods
      #
      # Wrapper method to define all of the attributes (public and protected)
      # for this Class.  This should be called with a locked mutex to
      # prevent multiple threads from defining attributes at the same time
      #
      # @return [Boolean] true
      def define_all_attributes
        if self.resource_definition["attributes"]
          # First we need to clear out the old values and clone them
          self.attribute_names = []
          self.public_attribute_names = []
          self.protected_attribute_names = []
          self.attribute_types = {}.with_indifferent_access
          # First define all public attributes
          define_attributes(
            *self.resource_definition['attributes']['public'],
            access_level: :public
          )
          # Then define all private attributes
          define_attributes(
            *self.resource_definition['attributes']['protected'],
            access_level: :protected
          )
        end
        true
      end

      #
      # Sets up the attributes for this class, can be called multiple
      # times to add more attributes.  Again meant to be called with a locked
      # mutex for thread safety
      #
      # @param  *args [Array] List of attributes to define and optional
      # hash as a last parameter
      #
      # @return [Boolean] Always true
      def define_attributes(*args)
        options = args.extract_options!
        options[:access_level] ||= :public
        # Initialize each attribute
        args.each do |arg|
          self.initialize_attribute(
            Array.wrap(arg),
            options[:access_level]
          )
        end

        self.define_attribute_methods(
          args,
          options[:access_level]
        )
      end

      #
      # Adds the attribute into some internal data structures but does
      # not define any methods for it
      #
      # @param  arg [Array] A 1 or 2 element array holding an
      # attribute name and optionally a type for that attribute
      # @param  access_level [Symbol] Either :protected or :public based on
      # the access level for this attribute
      #
      # @return [Boolean] Always true
      def initialize_attribute(attr, access_level)
        attr_name = attr[0].to_sym
        attr_type = (attr[1] || :unknown).to_sym

        # Look for the typecaster, raise an error if one is not found
        typecaster = self.typecasters[attr_type]
        if typecaster.nil?
          raise TypecasterNotFound, "#{attr_type} is an unknown type"
        end
        # Map the attribute name to the typecaster
        self.attribute_types[attr_name] = typecaster
        # Add the attribute to the proper list
        if access_level == :public
          if self.protected_attribute?(attr_name)
            raise ArgumentError, "Illegal change of attribute access level for #{attr_name}"
          end

          self.public_attribute_names << attr_name
        else
          if self.public_attribute?(attr_name)
            raise ArgumentError, "Illegal change of attribute access level for #{attr_name}"
          end

          self.protected_attribute_names << attr_name
        end
        self.attribute_names << attr_name
        true
      end

      #
      # Defines the attribute methods in a new module which
      # is then included in this class.  Meant to be called with
      # a locked mutex
      #
      # @param  args [Array] List of attributes to define
      # @param  access_level [Symbol] :protected or :public
      #
      # @return [Boolean] Always true
      def define_attribute_methods(attrs, access_level)
        self.attribute_method_module.module_eval do
          attrs.each do |attr|
            # Normalize for attributes without types
            attr_name = Array.wrap(attr).first
            # Define reader and huh methods
            self.module_eval <<-EOE, __FILE__, __LINE__ + 1
              def #{attr_name}
                read_attribute(:#{attr_name})
              end

              def #{attr_name}?
                read_attribute(:#{attr_name}).present?
              end
            EOE
            # Define a writer if this is public
            if access_level == :public
              self.module_eval <<-EOE, __FILE__, __LINE__ + 1
                def #{attr_name}=(new_val)
                  write_attribute(:#{attr_name}, new_val)
                end
              EOE
            end
          end
        end

        # Now we get all the attribute names as symbols
        # and define_attribute_methods for dirty tracking
        attrs = attrs.collect do |a|
          Array.wrap(a).first.to_sym
        end

        super(attrs)
        true
      end

      #
      # Returns true if the provided name is an attribute
      # of this class
      #
      # @param  name [Symbol] The name of the potential attribute
      #
      # @return [Boolean] True if an attribute with the given name exists
      def attribute?(name)
        self.attribute_names.include?(name.to_sym)
      end

      #
      # Returns true if the provided name is a protected attribute
      # @param  name [Symbol] Name of the potential attribute
      #
      # @return [Boolean] True if a protected attribute with the given name exists
      def protected_attribute?(name)
        self.protected_attribute_names.include?(name.to_sym)
      end

      #
      # Returns true if the provided name is a public attribute
      # @param  name [Symbol] Name of the potential attribute
      #
      # @return [Boolean] True if a public attribute with the given name exists
      def public_attribute?(name)
        self.public_attribute_names.include?(name.to_sym)
      end

      #
      # Removes all attributes from this class but does _NOT_
      # undefine their methods
      #
      # @return [Boolean] Always true
      def clear_attributes
        self.attribute_names.clear
        self.public_attribute_names.clear
        self.protected_attribute_names.clear

        true
      end

    end

    #
    # Override for initialize to set up attribute and
    # attributes cache
    #
    # @param  *args [Array] Arguments to initialize,
    # ignored in this case
    #
    # @return [Object] The object in question
    def initialize(*args)
      @attributes = HashWithIndifferentAccess[ self.class.attribute_names.zip([]) ]
      @attributes_cache = HashWithIndifferentAccess.new
      @previously_changed = HashWithIndifferentAccess.new
      @changed_attributes = HashWithIndifferentAccess.new
      super()
    end

    #
    # Reads an attribute typecasting if necessary
    # and setting the cache so as to only typecast
    # the one time.  Takes a block which is called if the
    # attribute is not found
    #
    # @param  attr_name [Symbol] The name of the attribute to read
    #
    # @return [Object] The value of the attribute or nil if it
    # is not found
    def read_attribute(attr_name)
      attr_name = attr_name.to_sym

      @attributes_cache[attr_name] || @attributes_cache.fetch(attr_name) do
        data = @attributes.fetch(attr_name) do
          # This statement overrides id to return the primary key
          # if it is set to something other than :id
          if attr_name == :id && self.class.primary_key != attr_name
            return read_attribute(self.class.primary_key)
          end

          # For some reason hashes return false for key? if the value
          # at that key is nil.  It also executes this block for fetch
          # when it really shouldn't.  Detect that error here and give
          # back nil for data
          if @attributes.keys.include?(attr_name)
            nil
          else
            # In this case the attribute was truly not found, if we're
            # given a block execute that otherwise return nil
            return block_given? ? yield(attr_name) : nil
          end
        end
        # This sets and returns the typecasted value
        @attributes_cache[attr_name] = self.typecast_attribute_for_read(
          attr_name,
          data
        )
      end
    end

    #
    # Reads the attribute directly out of the attributes hash
    # without applying any typecasting
    # @param  attr_name [Symbol] The name of the attribute to be read
    #
    # @return [Object] The untypecasted value of the attribute or nil
    # if that attribute is not found
    def read_attribute_before_type_cast(attr_name)
      return @attributes[attr_name.to_sym]
    end

    #
    # Writes an attribute, first typecasting it to the proper type with
    # typecast_attribute_for_write and then setting it in the attributes
    # hash.  Raises MissingAttributeError if no such attribute exists
    #
    # @param  attr_name [Symbol] The name of the attribute to set
    # @param  value [Object] The value to write
    #
    # @return [Object] The value parameter is always returned
    def write_attribute(attr_name, value)
      attr_name = attr_name.to_sym
      # Change a write attribute for id to the primary key
      attr_name = self.class.primary_key if attr_name == :id && self.class.primary_key
      # The value we expect here should be typecasted for going to
      # the api
      typed_value = self.typecast_attribute_for_write(attr_name, value)

      if attribute_changed?(attr_name)
        old = changed_attributes[attr_name]
        changed_attributes.delete(attr_name) if old == typed_value
      else
        old = clone_attribute_value(:read_attribute, attr_name)
        changed_attributes[attr_name] = old if old != typed_value
      end

      # Remove this attribute from the attributes cache
      @attributes_cache.delete(attr_name)
      # Raise an error if this is not an attribute
      if !self.attribute?(attr_name)
        raise ActiveModel::MissingAttributeError.new(
          "can't write unknown attribute #{attr_name}",
          caller(0)
        )
      end
      # Raise another error if this is a protected attribute
      # if self.protected_attribute?(attr_name)
      #   raise ApiResource::AttributeAccessError.new(
      #     "cannot write to protected attribute #{attr_name}",
      #     caller(0)
      #   )
      # end
      @attributes[attr_name] = typed_value
      value
    end

    #
    # Returns the typecasted value of an attribute for being
    # read (calls from_api on the typecaster).  Raises
    # TypecasterNotFound if no typecaster exists for this attribute
    #
    # @param  attr_name [Symbol] The name of the attribute
    # @param  value [Object] The value to be typecasted
    #
    # @return [Object] The typecasted value
    def typecast_attribute_for_read(attr_name, value)
      self
        .find_typecaster(attr_name)
        .from_api(value)
    end

    #
    # Returns the typecasted value of the attribute for being
    # written (calls to_api on the typecaster).  Raises
    # TypecasterNotFound if no typecaster exists for this attribute
    #
    # @param  attr_name [Symbol] The attribute in question
    # @param  value [Object] The value to be typecasted
    #
    # @return [Object] The typecasted value
    def typecast_attribute_for_write(attr_name, value)
      self
        .find_typecaster(attr_name)
        .to_api(value)
    end

    #
    # Returns a hash of attribute names as keys and typecasted values
    # as hash values
    #
    # @return [HashWithIndifferentAccess] Map from attr name to value
    def attributes
      hash = HashWithIndifferentAccess.new

      self.class.attribute_names.each_with_object(hash) do |name, attrs|
        attrs[name] = read_attribute(name)
      end
    end

    #
    # Handles mass assignment of attributes, including sanitizing them
    # for mass assignment. Which by default does nothing but would if you
    # were to use this in rails 4 or with strong_parameters
    #
    # @param  attrs [Hash] Hash of attributes to mass assign
    #
    # @return [Hash] The passed in attrs param (with keys symbolized)
    def attributes=(attrs)
      unless attrs.respond_to?(:symbolize_keys)
        raise ArgumentError, 'You must pass a hash when assigning attributes'
      end

      return if attrs.blank?

      attrs = attrs.symbolize_keys
      # First deal with sanitizing for mass assignment
      # this raises an error if attrs violates mass assignment rules
      attrs = self.sanitize_for_mass_assignment(attrs)

      attrs.each do |name, value|
        self._assign_attribute(name, value)
      end

      attrs
    end

    #
    # Reads an attribute and raises MissingAttributeError
    #
    # @param  attr_name [Symbol] The attribute to read
    #
    # @return [Object] The value of the attribute
    def [](attr_name)
      read_attribute(attr_name) do |n|
        self.missing_attribute(attr_name, caller(0))
      end
    end

    #
    # Write the value to the given attribute
    # @param  attr_name [Symbol] The attribute to write
    # @param  value [Object] The value to be written
    #
    # @return [Object] Returns the value parameter
    def []=(attr_name, value)
      write_attribute(attr_name, value)
    end

    #
    # Wrapper for the class method attribute? that
    # returns true if the name parameter is the name of
    # any attribute
    #
    # @param  name [Symbol] The name to query
    #
    # @return [Boolean] True if the name param is the name
    # of an attribute
    def attribute?(name)
      self.class.attribute?(name)
    end

    #
    # Wrapper for the class method protected_attribute?
    #
    # @param  name [Symbol] The name to query
    #
    # @return [Boolean] True if name is a protected attribute
    def protected_attribute?(name)
      self.class.protected_attribute?(name)
    end

    #
    # Wrapper for the class method public_attribute?
    #
    # @param  name [Symbol] The name to query
    #
    # @return [Boolean] True if name is a public attribute
    def public_attribute?(name)
      self.class.public_attribute?(name)
    end

    #
    # Override for the save method to update our dirty tracking
    # of attributes
    #
    # @param  *args [Array] Used to clear changes on any associations
    # embedded in this save provided it succeeds
    #
    # @return [Boolean] True if the save succeeded, false otherwise
    def save_with_dirty_tracking(*args)
      if save_without_dirty_tracking(*args)
        self.make_changes_current
        if args.first.is_a?(Hash) && args.first[:include_associations]
          args.first[:include_associations].each do |assoc|
            Array.wrap(self.send(assoc).internal_object).each(&:make_changes_current)
          end
        end
        return true
      else
        return false
      end
    end

    #
    # Override to respond_to? for finding attribute methods even
    # if they are not defined
    #
    # @param  sym [Symbol] The method that we may respond to
    # @param  include_private_methods = false [Boolean] Whether or not
    # we should consider private methods
    #
    # @return [Boolean] True if we respond to sym
    def respond_to?(sym, include_private_methods = false)
      if sym =~ /\?$/
        return true if self.attribute?($`)
      elsif sym =~ /=$/
        return true if self.class.public_attribute_names.include?($`)
      else
        return true if self.attribute?(sym.to_sym)
      end
      super
    end

    def method_missing(sym, *args, &block)
      sym = sym.to_sym

      # Maybe the resource definition sucks...
      if self.class.resource_definition_is_invalid?
        self.class.reload_resource_definition
      end

      # If we don't respond by now...
      if self.respond_to?(sym)
        return self.send(sym, *args, &block)
      elsif @attributes.keys.symbolize_array.include?(sym)
        # Try returning the attributes from the attributes hash
        return @attributes[sym]
      end

      # Fall back to class method_missing
      super
    end

    def make_changes_current
      @previously_changed = self.changes
      @changed_attributes.clear
    end

    def clear_changes(*attrs)
      @previously_changed = {}
      @changed_attributes = {}
      true
    end

    def reset_changes
      self.class.attribute_names.each do |attr_name|
        attr_name = attr_name.to_sym

        reset_attribute!(attr_name)
      end
      true
    end

    protected
      #
      # Default implementation that would be overridden by including
      # the behavior from strong parameters
      #
      # @param  attrs [Hash] Attributes to sanitize (by default do nothing)
      #
      # @return [Hash] Unmodified attrs
      def sanitize_for_mass_assignment(attrs)
        return attrs
      end

      #
      # Writes an attribute by proxying to the writer methods.
      # Raises MissingAttributeError if #{name}= does not exist
      #
      # @param  name [Symbol] The attribute to write
      # @param  value [Object] The value to write
      #
      # @return [Boolean] Always true
      def _assign_attribute(name, value)
        # special case if we are assigning a protected attribute
        # since it has no writer method
        if self.protected_attribute?(name)
          return self.write_attribute(name, value)
        end

        begin
          # Call the method only if it is public
          self.public_send("#{name}=", value)
        rescue NoMethodError
          # If we get a no method error we should re-raise it
          # if it wasn't because #{name}= is not defined
          if self.respond_to?("#{name}=")
            raise
          else
            # Otherwise we raise MissingAttributeError
            self.missing_attribute(name, caller(0))
          end
        end
      end

      #
      # Searches for the typecaster for the given attribute name
      # raising ApiResource::TypecasterNotFound if it
      # cannot find one
      #
      # @param  attr_name [Symbol] The attribute whose typecaster you're after
      #
      # @return [ApiResource::Typecaster] An object for typecasting attribute
      # values
      def find_typecaster(attr_name)
        attr_name = attr_name.to_sym

        typecaster = self.class.attribute_types[attr_name]

        if typecaster.nil?
          typecaster = ApiResource::Typecast::UnknownTypecaster
        end

        return typecaster
      end

      #
      # Helper for raising a MissingAttributeError
      #
      # @param  name [Symbol] The missing attribute's name
      # @param  backtrace [Object] The backtrace of where the
      # error occurred
      #
      # @return [type] [description]
      def missing_attribute(name, backtrace)
        raise ActiveModel::MissingAttributeError.new(
          "could not find attribute #{name}",
          backtrace
        )
      end

      def clone_attribute_value(meth, attr_name)
        attr_name = attr_name.to_sym

        result = self.send(meth, attr_name)

        return result.duplicable? ? result.clone : result
      end


    private

      # this is here for compatibility with ActiveModel::AttributeMethods
      # it is the fallback called in method_missing
      #
      # @param  name [Symbol] The attribute to read
      #
      # @return [Object] The value read
      def attribute(name)
        read_attribute(name)
      end

      # this is here for compatibility with ActiveModel::AttributeMethods
      # it is the fallback called in method_missing
      #
      # @param  name [Symbol] The attribute to
      # @param  val [Object] The value to assign
      #
      # @return [Object] val
      def attribute=(name, val)
        write_attribute(name, val)
      end

  end

end