# encoding: utf-8
module Mongoid  #:nodoc:
  module Tracking #:nodoc:
    module Aggregates

      DEPRECATED_TOKENS = ['hour', 'hours']

      # This module includes aggregate data extensions to Trackoid instances
      def self.included(base)
        base.class_eval do
          extend ClassMethods

          class_attribute :aggregate_fields, :aggregate_klass
          self.aggregate_fields = {}
          self.aggregate_klass = nil
          delegate :aggregate_fields, :aggregate_klass, :aggregated?,
                   to: "self.class"
        end
      end

      module ClassMethods
        # Defines an aggregate token to an already tracked model. It defines
        # a new mongoid model named after the original model.
        #
        # Example:
        #
        # <tt>class Page</tt>
        # <tt>  include Mongoid::Document</tt>
        # <tt>  include Mongoid::Tracking</tt>
        # <tt>  track :visits</tt>
        # <tt>  aggregate :browsers do |b|</tt>
        # <tt>    b.split(" ").first</tt>
        # <tt>  end</tt>
        # <tt>end</tt>
        #
        # A new model is defined as <tt>class PageAggregates</tt>
        #
        # This model has the following structure:
        #
        # <tt>belongs_to :page</tt>
        # <tt>field :ns, :type => String</tt>
        # <tt>field :key, :type => String</tt>
        # <tt>index [:page_id, :ns, :key], :unique => true</tt>
        # <tt>track :[original_parent_tracking_data]</tt>
        # <tt>track :...</tt>
        #
        # :ns is the "namespace". It's the name you put along the
        # "aggregate :browsers" in the original model definition.
        #
        # :key is your aggregation key. This is the value you are required to
        # return in the "aggregate" block.
        #
        # With the above structure, you can always query aggregates directly
        # using Mongoid this way:
        #
        # <tt>TestModelAggregates.where(:ns => "browsers", :key => "explorer").first</tt>
        #
        # But you are encouraged to use Trackoid methods whenever possible.
        #
        def aggregate(name, &block)
          raise Errors::AggregationAlreadyDefined.new(self.name, name) if aggregate_fields.has_key? name
          raise Errors::AggregationNameDeprecated.new(name) if DEPRECATED_TOKENS.include? name.to_s

          define_aggregate_model if aggregate_klass.nil?
          has_many internal_accessor_name(name), class_name: aggregate_klass.to_s
          add_aggregate_field(name, block)
          create_aggregation_accessors(name)
        end

        # Return true if this model has aggregated data.
        def aggregated?
          !aggregate_klass.nil?
        end

        protected
        # Returns the internal representation of the aggregates class name
        def internal_aggregates_name
          str = self.to_s.underscore + "_aggregates"
          str.camelize
        end

        def internal_accessor_name(name)
          (name.to_s + "_accessor").to_sym
        end

        # Defines the aggregation model. It checks for class name conflicts
        def define_aggregate_model
          unless defined?(Rails) && Rails.env.development?
            raise Errors::ClassAlreadyDefined.new(internal_aggregates_name) if foreign_class_defined?
          end

          parent = self
          
          define_klass do
            include Mongoid::Document
            include Mongoid::Tracking

            # Make the relation to the original class
            belongs_to parent.name.demodulize.underscore.to_sym, class_name: parent.name

            # Internal fields to track aggregation token and keys
            field :ns, type: String
            field :key, type: String

            index({ 
              parent.name.foreign_key.to_sym => 1,
              ns: 1,
              key: 1
            }, { unique: true, background: true })            

            # Include parent tracking data.
            parent.tracked_fields.each { |track_field| track track_field }
          end

          self.aggregate_klass = internal_aggregates_name.constantize
        end

        # Returns true if there is a class defined with the same name as our
        # aggregate class.
        def foreign_class_defined?
          internal_aggregates_name.constantize && true
        rescue NameError
          false
        end

        # Adds the aggregate field to the array of aggregated fields.
        def add_aggregate_field(name, block)
          aggregate_fields[name] = block
        end

        # Defines the aggregation external class. This class is named after
        # the original class model but with "Aggregates" appended.
        # Example:  TestModel ==> TestModelAggregates
        def define_klass(&block)
          scope = internal_aggregates_name.split('::')
          klass = scope.pop
          scope = scope.inject(Object) do |scope, const_name| 
            scope.const_get(const_name)
          end
          klass = scope.const_set(klass, Class.new)
          klass.class_eval(&block)
        end

        def create_aggregation_accessors(name)
          # Aggregation accessors in the model acts like a named scopes
          define_method(name) do |*args|
            TrackerAggregates.new(self, name, args)
          end

          define_method("#{name}_with_track") do |track_field, *args|
            TrackerAggregates.new(self, name, args, track_field)
          end

          define_method("#{name}=") do
            raise NoMethodError
          end
        end
      end

    end
  end
end
