require 'active_support/inflector'
require 'friendly/associations'

module Friendly
  module Document
    class << self
      attr_writer :documents

      def included(klass)
        documents << klass
        klass.class_eval do
          extend ClassMethods
          attribute :id,         UUID
          attribute :created_at, Time
          attribute :updated_at, Time
        end
      end

      def documents
        @documents ||= []
      end

      def create_tables!
        documents.each { |d| d.create_tables! }
      end
    end

    module ClassMethods
      attr_writer :storage_proxy, :query_klass, 
                  :table_name,    :collection_klass,
                  :scope_proxy,   :association_set

      def create_tables!
        storage_proxy.create_tables!
      end

      def attribute(name, type = nil, options = {})
        attributes[name] = Attribute.new(self, name, type, options)
      end

      def storage_proxy
        @storage_proxy ||= StorageProxy.new(self)
      end

      def query_klass
        @query_klass ||= Query
      end

      def collection_klass
        @collection_klass ||= WillPaginate::Collection
      end

      def indexes(*args)
        storage_proxy.add(args)
      end

      def caches_by(*fields)
        options = fields.last.is_a?(Hash) ? fields.pop : {}
        storage_proxy.cache(fields, options)
      end

      def attributes
        @attributes ||= {}
      end

      def first(query)
        storage_proxy.first(query(query))
      end

      def all(query)
        storage_proxy.all(query(query))
      end

      def find(id)
        doc = first(:id => id)
        raise RecordNotFound, "Couldn't find #{name}/#{id}" if doc.nil?
        doc
      end

      def count(conditions)
        storage_proxy.count(query(conditions))
      end

      def paginate(conditions)
        query      = query(conditions)
        count      = count(query)
        collection = collection_klass.new(query.page, query.per_page, count)
        collection.replace(all(query))
      end

      def create(attributes = {})
        doc = new(attributes)
        doc.save
        doc
      end

      def table_name
        @table_name ||= name.pluralize.underscore
      end

      def scope_proxy
        @scope_proxy ||= ScopeProxy.new(self)
      end

      # Add a named scope to this Document.
      #
      # e.g.
      #     
      #     class Post
      #       indexes     :created_at
      #       named_scope :recent, :order! => :created_at.desc
      #     end
      #
      # Then, you can access the recent posts with:
      #
      #     Post.recent.all
      # ...or...
      #     Post.recent.first
      #
      # Both #all and #first also take additional parameters:
      #
      #     Post.recent.all(:author_id => @author.id)
      #
      # Scopes are also chainable. See the README or Friendly::Scope docs for details.
      #
      # @param [Symbol] name the name of the scope.
      # @param [Hash] parameters the query that this named scope will perform.
      #
      def named_scope(name, parameters)
        scope_proxy.add_named(name, parameters)
      end

      # Returns boolean based on whether the Document has a scope by a particular name.
      #
      # @param [Symbol] name The name of the scope in question.
      #
      def has_named_scope?(name)
        scope_proxy.has_named_scope?(name)
      end

      # Create an ad hoc scope on this Document.
      #
      # e.g.
      #     
      #     scope = Post.scope(:order! => :created_at)
      #     scope.all # => [#<Post>, #<Post>]
      #
      # @param [Hash] parameters the query parameters to create the scope with.
      #
      def scope(parameters)
        scope_proxy.ad_hoc(parameters)
      end

      def association_set
        @association_set ||= Associations::Set.new(self)
      end

      # Add a has_many association.
      #
      # e.g.
      #
      #     class Post
      #       attribute :user_id, Friendly::UUID
      #       indexes   :user_id
      #     end
      #      
      #     class User
      #       has_many :posts
      #     end
      #     
      #     @user = User.create
      #     @post = @user.posts.create
      #     @user.posts.all == [@post] # => true
      #
      # _Note: Make sure that the target model is indexed on the foreign key. If it isn't, querying the association will raise Friendly::MissingIndex._
      #
      # Friendly defaults the foreign key to class_name_id just like ActiveRecord.
      # It also converts the name of the association to the name of the target class just like ActiveRecord does.
      #
      # The biggest difference in semantics between Friendly's has_many and active_record's is that Friendly's just returns a Friendly::Scope object. If you want all the associated objects, you have to call #all to get them. You can also use any other Friendly::Scope method.
      #
      # @param [Symbol] name The name of the association and plural name of the target class.
      # @option options [String] :class_name The name of the target class of this association if it is different than the name would imply.
      # @option options [Symbol] :foreign_key Override the foreign key.
      # 
      def has_many(name, options = {})
        association_set.add(name, options)
      end

      protected
        def query(conditions)
          conditions.is_a?(Query) ? conditions : query_klass.new(conditions)
        end
    end

    def initialize(opts = {})
      self.attributes = opts
    end

    def attributes=(attrs)
      assert_no_duplicate_keys(attrs)
      attrs.each { |name, value| send("#{name}=", value) }
    end

    def save
      new_record? ? storage_proxy.create(self) : storage_proxy.update(self)
    end

    def update_attributes(attributes)
      self.attributes = attributes
      save
    end

    def destroy
      storage_proxy.destroy(self)
    end

    def to_hash
      Hash[*self.class.attributes.keys.map { |n| [n, send(n)] }.flatten]
    end

    def table_name
      self.class.table_name
    end

    def new_record?
      new_record
    end

    def new_record
      @new_record = true if @new_record.nil?
      @new_record
    end

    def new_record=(value)
      @new_record = value
    end

    def storage_proxy
      self.class.storage_proxy
    end

    def ==(comparison_object)
      comparison_object.equal?(self) ||
        (comparison_object.is_a?(self.class) &&
          !comparison_object.new_record? && 
            comparison_object.id == id)
    end

    protected
      def assert_no_duplicate_keys(hash)
        if hash.keys.map { |k| k.to_s }.uniq.length < hash.keys.length
          raise ArgumentError, "Duplicate keys: #{hash.inspect}"
        end
      end
  end
end