# encoding: utf-8
module Mongoid  #:nodoc:
  module Tracking
    # This internal class handles all interaction for a track field.
    class Tracker
      
      def initialize(owner, field, aggregate_data)
        @owner, @for = owner, field
        @for_data = @owner.internal_track_name(@for)
        @data = @owner.read_attribute(@for_data)
        
        # The following is needed if the "field" Mongoid definition for our
        # internal tracking field does not include option ":default => {}"
        if @data.nil?
          @owner.write_attribute(@for_data, {})
          @data = @owner.read_attribute(@for_data)
        end

        @aggregate_data = aggregate_data.first if aggregate_data.first
      end

      # Delegate all missing methods to the aggregate accessors. This enables
      # us to call an aggregation token after the tracking field.
      #
      # Example:
      #
      #   <tt>@object.visits.browsers ...</tt>
      #
      def method_missing(name, *args, &block)
        super unless @owner.aggregate_fields.member?(name)
        @owner.send("#{name}_with_track".to_sym, @for, *args, &block)
      end

      # Update methods
      def add(how_much = 1, date = Date.today)
        raise Errors::ModelNotSaved, "Can't update a new record. Save first!" if @owner.new_record?
        return if how_much == 0

        # Note that the following #update_data method updates our local data
        # and the current value might differ from the actual value on the
        # database. Basically, what we do is update our own copy as a cache
        # but send the command to atomically update the database: we don't
        # read the actual value in return so that we save round trip delays.
        #
        update_data(data_for(date) + how_much, date)
        @owner.collection.update(
            @owner._selector,
            { (how_much > 0 ? "$inc" : "$dec") => update_hash(how_much.abs, date) },
            :upsert => true
        )
        return unless @owner.aggregated?

        @owner.aggregate_fields.each do |(k,v)|
          next unless token = v.call(@aggregate_data)
          fk = @owner.class.name.to_s.foreign_key.to_sym
          selector = { fk => @owner.id, :ns => k, :key => token.to_s }
          @owner.aggregate_klass.collection.update(
              selector,
              { (how_much > 0 ? "$inc" : "$dec") => update_hash(how_much.abs, date) },
              :upsert => true
          )
        end
      end

      def inc(date = Date.today)
        add(1, date)
      end

      def dec(date = Date.today)
        add(-1, date)
      end

      def set(how_much, date = Date.today)
        raise Errors::ModelNotSaved, "Can't update a new record" if @owner.new_record?
        update_data(how_much, date)
        @owner.collection.update(
            @owner._selector,
            { "$set" => update_hash(how_much, date) },
            :upsert => true
        )
        return unless @owner.aggregated?

        @owner.aggregate_fields.each do |(k,v)|
          next unless token = v.call(@aggregate_data)
          fk = @owner.class.name.to_s.foreign_key.to_sym
          selector = { fk => @owner.id, :ns => k, :key => token.to_s }
          @owner.aggregate_klass.collection.update(
              selector,
              { "$set" => update_hash(how_much, date) },
              :upsert => true
          )
        end
      end


      # Access methods
      def today
        data_for(Date.today)
      end

      def yesterday
        data_for(Date.today - 1)
      end

      def first_value
        data_for(first_date)
      end

      def last_value
        data_for(last_date)
      end
      
      def last_days(how_much = 7)
        return [today] unless how_much > 0
        date, values = Date.today, []
        (date - how_much.abs + 1).step(date) {|d| values << data_for(d) }
        values
      end

      def on(date)
        return date.collect {|d| data_for(d)} if date.is_a?(Range)
        data_for(date)
      end

      def all_values
        on(first_date..last_date) if first_date
      end

      # Utility methods
      def first_date
        # We are guaranteed _m and _d to exists unless @data is a malformed
        # hash, so we need to do this nasty "return nil", sorry...
        # TODO: I'm open to change this to a cleaner algorithm :-)
        return nil unless _y = @data.keys.min
        return nil unless _m = @data[_y].keys.min
        return nil unless _d = @data[_y][_m].keys.min
        Date.new(_y.to_i, _m.to_i, _d.to_i)
      end
      
      def last_date
        # We are guaranteed _m and _d to exists unless @data is a malformed
        # hash, so we need to do this nasty "return nil", sorry...
        # TODO: I'm open to change this to a cleaner algorithm :-)
        return nil unless _y = @data.keys.max
        return nil unless _m = @data[_y].keys.max
        return nil unless _d = @data[_y][_m].keys.max
        Date.new(_y.to_i, _m.to_i, _d.to_i)
      end

      # Private methods
      private
      def data_for(date)
        return nil if date.nil?
        date = normalize_date(date)
        @data.try(:[], date.year.to_s).try(:[], date.month.to_s).try(:[], date.day.to_s) || 0
      end

      def update_data(value, date)
        return nil if date.nil?
        date = normalize_date(date)
        [:year, :month].inject(@data) { |data, period|
          data[date.send(period).to_s] ||= {}
        }
        @data[date.year.to_s][date.month.to_s][date.day.to_s] = value
      end

      def year_literal(d);  "#{d.year}"; end
      def month_literal(d); "#{d.year}.#{d.month}"; end
      def date_literal(d);  "#{d.year}.#{d.month}.#{d.day}"; end

      def update_hash(num, date)
        date = normalize_date(date)
        {
          "#{@for_data}.#{date_literal(date)}" => num
        }
      end

      def normalize_date(date)
        case date
        when String
          Date.parse(date)
        else
          date
        end
      end

      # WARNING: This is +only+ for debugging pourposes (rspec y tal)
      def _original_hash
        @data
      end

    end

  end
end