module Spree
  module Adjustable
    class PromotionAccumulator
      attr_reader :adjustments, :sources, :promotions

      # Adds accumulator as an attribute of adjustable to
      # avoid changing number of passed arguments to
      # adjustment#update! and concequential methods
      def self.add_to(adjustable)
        class << adjustable
          attr_accessor :promotion_accumulator
        end

        adjustable.promotion_accumulator = new(adjustable)
      end

      def initialize(adjustable)
        @adjustable = adjustable
        @adjustments = []
        @sources = []
        @promotions = []
        all_adjustments.each { |a| add_adjustment(a) }
      end

      def add_adjustment(adjustment, opts = {})
        return unless adjustment.promotion?
        source = opts[:source] || adjustment.source
        promotion = opts[:promotion] || source.promotion

        add(adjustments, adjustment, adjustment.id)
        add(sources, source, adjustment.source_id)
        add(promotions, promotion, source.promotion_id)
      end

      def promotions_adjustments(promotion_id, adjustments = adjustments)
        where(sources, promotion_id: promotion_id).map do |source|
          where(adjustments, source_id: source.id)
        end.flatten
      end

      def promo_total(*args)
        promotions_adjustments(*args).map(&:amount).reduce(0, &:+)
      end

      def total_with_promotion(promotion_id)
        amount + ship_total + promo_total(promotion_id)
      end

      def item_total_with_promotion(promotion_id)
        amount + promo_total(promotion_id, item_adjustments)
      end

      private

      attr_reader :adjustable
      delegate :order, to: :adjustable
      delegate :amount, :ship_total, to: :order

      def all_adjustments
        order.all_adjustments.promotion.includes(source: [:promotion]).where.
          not('adjustable_id = ? AND adjustable_type = ?', adjustable.id, adjustable.class.to_s)
      end

      def add(array, object, id)
        array << object if array.none? { |a| a.id == id }
      end

      def item_adjustments
        adjustments.reject { |a| a.adjustable_type == 'Spree::Shipment' }
      end

      def where(array, opts = {})
        array.select { |a| opts.all? { |k, v| a.respond_to?(k) && a.send(k) == v } }
      end
    end
  end
end
