require File.dirname(__FILE__) + '/simple_property'
require File.dirname(__FILE__) + '/deep_tracked_property'

module CouchPotato
  module Persistence
    module Properties
      class PropertyList
        include Enumerable

        attr_accessor :list

        def initialize(clazz)
          @clazz = clazz
          @list = []
          @hash = {}
        end

        def each(&block)
          (list + inherited_properties).each(&block)
        end

        def <<(property)
          @hash[property.name] = property
          @list << property
        end

        def find_property(name)
          @hash[name] || @clazz.superclass.properties.find_property(name)
        end

        # XXX
        def inspect
          list.map(&:name).inspect
        end

        def inherited_properties
          superclazz = @clazz.superclass
          properties = []
          while superclazz && superclazz.respond_to?(:properties)
            properties << superclazz.properties.list
            superclazz = superclazz.superclass
          end
          properties.flatten
        end
      end

      def self.included(base) #:nodoc:
        base.extend ClassMethods
        base.class_eval do
          def self.properties
            @properties ||= {}
            @properties[name] ||= PropertyList.new(self)
            @properties[name]
          end
        end
      end

      def type_caster #:nodoc:
        @type_caster ||= TypeCaster.new
      end

      module ClassMethods
        # returns all the property names of a model class that have been defined using the #property method
        #
        # example:
        #  class Book
        #    property :title
        #    property :year
        #  end
        #  Book.property_names # => [:title, :year]
        def property_names
          properties.map(&:name)
        end

        # Declare a property on a model class. Properties are not typed by default.
        # You can store anything in a property that can be serialized into JSON.
        # If you want a property to be of a custom class you have to define it using the :type option.
        #
        # example:
        #  class Book
        #    property :title
        #    property :year
        #    property :publisher, type: Publisher
        #    property :published_at, default: -> { Date.current }
        #    property :next_year, default: ->(book) { book.year + 1 }
        #  end
        def property(name, options = {})
          # ActiveModel::AttributeMethods.generated_attribute_methods
          active_support_module = send(:generated_attribute_methods)

          # Mimic ActiveModel::AttributeMethods.undefine_attribute_methods, but only for this
          # property's accessor method
          active_support_module.module_eval do
            undef_method(name) if instance_methods.include?(name)
          end
          cache = send(:attribute_method_matchers_cache)
          cache.delete(name)

          define_attribute_method name
          properties << SimpleProperty.new(self, name, options)

          # Remove the default ActiveModel::AttributeMethods accessor
          if active_support_module.instance_methods.include?(name)
            active_support_module.send(:remove_method, name)
          end
        end
      end
    end
  end
end
