'use strict';

/* ************************************************************** *
 *                                                                *
 *                          IMPORTANT!                            *
 *                                                                *
 *              This file is used by iOS native app,              *
 *              create potato_prices_calculator_4.js              *
 *                if introducing breaking changes                 *
 *                  Please use nothing external.                  *
 *                                                                *
 * ************************************************************** */

/*
 * CHANGELOG
 *
 * version 3
 * total refactoring, ability to calculate (diff) additional services
 * breaking changes: removed cost.freeThings;
 *                   DISCOUNT.cost is now negative number
 *                   winbackCoupon reduction is now stored in service.winbackReduction
 *
 * version 2
 * total refactoring, removed jQuery, DOM manipulation
 */


/**
 * @typedef {{
 *   type_id:            number|string,
 *   value:              number|null,
 *   quantity:           number,
 *   service_type_id:    number,
 *   writer_category_id: number|null,
 * }} WinbackCoupon
 *
 * @typedef {{
 *   pages:                    number,
 *   slides:                   number,
 *   discount:          		object,
 *   charts:                   number,
 *   winbackCoupons:           Array<WinbackCoupon>,
 *   writerCategoryId:         number|null,
 *   tariffPricePerPage:     number,
 *   tariffHrs:              number,
 *   spacing:                  string|null,
 *   getSamplesOn:             boolean,
 *   getProgressiveDeliveryOn: boolean,
 *   getUsedSourcesOn:         boolean,
 *   writerPercent:            number,
 *   complexAssignmentDiscipline: boolean,
 *   expertProofreading:          boolean,
 * }} OrderInfo
 *
 * @typedef {{
 *   pages:               number,
 *   slides:              number,
 *   charts:              number,
 *   progressiveDelivery: boolean,
 *   copyOfSources:       boolean,
 *   writerSamples:       boolean,
 *   categoriesOfWriter:  Array<number>,
 * }} FreeThings
 *
 * @typedef {{
 *   servicesById: Object.<string, Service>,
 *   totalCost: number,
 * }} CostInfo
 *
 * @typedef {{
 *   quantity:          number,
 *   type_id:           number|string,
 *   title:             string,
 *   cost:              number,
 *   winbackQuantity:   number,
 *   winbackReduction:  number,
 *   priority:          number,
 *   price:             number,
 *   itemPrice:         number,
 *   pricePercent:      number,
 *   forcedReason:      string|undefined,
 *   disabledReason:    string|undefined,
 *   active:            boolean,
 *   operations:        Array,
 * }} Service
 */


(function (definition, global) {
  // Universal Module Definition
  if (typeof exports === "object" && typeof module === "object") {
    // CommonJS
    module.exports = definition();
  } else if (typeof define === "function" && define.amd) {
    // RequireJS
    define(definition);
  } else {
    (typeof window !== 'undefined' && window && window.window === window && window // browser
      || typeof global !== 'undefined' && global && global.global === global && global // node.js
      || typeof self !== 'undefined' && self && self.self === self && self // ecma next
    ).PROXIMCostCalculator = definition();
  }
})(function() {

  var exports = {};

  var ukForm = false;

  /**
   * Approximates number to hundredth
   * @param {number} amount
   * @returns {number}
   */
  var normalizePrice = function(amount) {
    return Math.round(amount * 100) / 100;
  };

  /**
   * Appends `s` to the word if number's last digit is not 1
   * @param {string} word
   * @param {number} number
   * @returns {string}
   */
  var pluralize = function(word, number) {
    return number && (number).toString().substr(-1) === '1' ? word : word + 's';
  };

  /**
   * Poor man's Object.assign
   * @param {Object} target
   * @param {Object} source
   * @returns {Object} Extended object
   */
  var assign = function(target, source) {
    for (var p in source) if (source.hasOwnProperty(p)) {
      target[p] = source[p];
    }
    return target;
  };

  var PROVIDE_SAMPLES_PRICE = 5;
  var PROGRESSIVE_DELIVERY_PERCENT = 10;
  var USED_SOURCES_MIN_PRICE = 14.95;
  var USED_SOURCES_PERCENT = 10;

  var COUPONS_TYPES_IDS = {
    PERCENT:           1,
    AMOUNT:            3,
    FREE_UNIT:         5,
    WRITER_CATEGORY:   7,
    HALF_CUT_DEADLINE: 8,
  };

  var COMPLEX_ASSIGNMENT_PERCENT = 20;
  var EXPERT_PROOFREADING_PERCENT = 25;
  var SERVICES_IDS = {
    WRITING_PAGES:        1,
    WRITING_SLIDES:       2,
    WRITING_CHARTS:       31,
    CHOOSE_WRITER:        3,
    PROVIDE_ME_SAMPLES:   4,
    PROGRESSIVE_DELIVERY: 5,
    DISCOUNT:             6,
    USED_SOURCES:         21,
    COMPLEX_ASSIGNMENT:   37,
    REVISION_MINOR_PAGES:  26,
    REVISION_MINOR_SLIDES: 27,
    REVISION_MINOR_CHARTS: 33,
    REVISION_MAJOR_PAGES:  28,
    REVISION_MAJOR_SLIDES: 29,
    REVISION_MAJOR_CHARTS: 35,
    EXPERT_PROOFREADING: 41,
  };

  var ADD_SERVICES_IDS = {
    ADD_WRITING_PAGES:        'page',
    ADD_WRITING_SLIDES:       'slide',
    ADD_WRITING_CHARTS:       'chart',
    ADD_PROVIDE_ME_SAMPLES:   'samples',
    ADD_PROGRESSIVE_DELIVERY: 'progressive',
    ADD_USED_SOURCES:         'sources',
    ADD_BOOST_CATEGORY:       'boost',
    ADD_SHORTEN_DEADLINE:     'shorten',
    ADD_EXPERT_PROOFREADING:  'expert_proofreading',
  };

  var ADD_SERVICES_ORDER = [
    ADD_SERVICES_IDS.ADD_WRITING_PAGES,
    ADD_SERVICES_IDS.ADD_WRITING_SLIDES,
    ADD_SERVICES_IDS.ADD_WRITING_CHARTS,
    ADD_SERVICES_IDS.ADD_USED_SOURCES,
    ADD_SERVICES_IDS.ADD_PROVIDE_ME_SAMPLES,
    ADD_SERVICES_IDS.ADD_SHORTEN_DEADLINE,
    ADD_SERVICES_IDS.ADD_BOOST_CATEGORY,
    ADD_SERVICES_IDS.ADD_PROGRESSIVE_DELIVERY,
    ADD_SERVICES_IDS.ADD_EXPERT_PROOFREADING
  ];

  var UNMERGEABLE_ADD_SERVICES = [
    ADD_SERVICES_IDS.ADD_SHORTEN_DEADLINE,
    ADD_SERVICES_IDS.ADD_BOOST_CATEGORY
  ];

  /**
   * Validates order values
   * @param {OrderInfo=} order
   * @returns {OrderInfo} valid order
   */
  var orderWithDefaults = function(order) {
    if (!order) {
      order = {};
    }
    return {
      pages:                              parseInt(order.pages)                  || 0,
      slides:                             parseInt(order.slides)                 || 0,
      discount:                    		  order.discount        				 || undefined,
      charts:                             parseInt(order.charts)                 || 0,
      winbackCoupons:                     order.winbackCoupons                   || [],
      writerCategoryId:                   order.writerCategoryId                 || null,
      tariffPricePerPage:                 parseFloat(order.tariffPricePerPage) || 0,
      tariffHrs:                          parseInt(order.tariffHrs)            || 0,
      spacing:                            order.spacing                          || null,
      getSamplesOn:                       Boolean(order.getSamplesOn),
      getProgressiveDeliveryOn:           Boolean(order.getProgressiveDeliveryOn),
      getUsedSourcesOn:                   Boolean(order.getUsedSourcesOn),
      complexAssignmentDiscipline:        Boolean(order.complexAssignmentDiscipline),
      writerPercent:                      parseInt(order.writerPercent)          || 0,
      expertProofreading:                 Boolean(order.expertProofreading),
    };
  };

  /**
   * Validates additional services info
   * @param {AddServicesInfo=} addServicesInfo
   * @param {OrderInfo} originalOrder
   * @returns {AddServicesInfo}
   */
  var addServicesInfoWithDefaults = function(addServicesInfo, originalOrder) {
    addServicesInfo = addServicesInfo || {};
    if (!addServicesInfo.newPagesQuantity) {
      addServicesInfo.newPagesQuantity = originalOrder.pages;
    }
    if (!addServicesInfo.newSlidesQuantity) {
      addServicesInfo.newSlidesQuantity = originalOrder.slides;
    }
    if (!addServicesInfo.newChartsQuantity) {
      addServicesInfo.newChartsQuantity = originalOrder.charts;
    }
    if (!addServicesInfo.getUsedSourcesOn) {
      addServicesInfo.getUsedSourcesOn = originalOrder.getUsedSourcesOn;
    }
    if (!addServicesInfo.getSamplesOn) {
      addServicesInfo.getSamplesOn = originalOrder.getSamplesOn;
    }
    if (!addServicesInfo.expertProofreading) {
      addServicesInfo.expertProofreading = originalOrder.expertProofreading;
    }
    if (!addServicesInfo.getProgressiveDeliveryOn) {
      addServicesInfo.getProgressiveDeliveryOn = originalOrder.getProgressiveDeliveryOn;
    }
    if (!addServicesInfo.shortenDeadlinePricePerPage) {
      addServicesInfo.shortenDeadlinePricePerPage = originalOrder.tariffPricePerPage;
    }
    if (!addServicesInfo.shortenDeadlineHrs) {
      addServicesInfo.shortenDeadlineHrs = originalOrder.tariffHrs;
    }
    if (!addServicesInfo.boostWriterCategoryId) {
      addServicesInfo.boostWriterCategoryId = originalOrder.writerCategoryId;
    }
    if (!addServicesInfo.boostWriterPercent) {
      addServicesInfo.boostWriterPercent = originalOrder.writerPercent;
    }
    return addServicesInfo;
  };

  /**
   * Transforms array of winback coupons to object that is easy to use in UI
   * @param {Array} winbackCoupons
   * @returns {{
   *   pages: number,
   *   slides: number,
   *   charts: number,
   *   progressiveDelivery: boolean,
   *   copyOfSources: boolean,
   *   writerSamples: boolean,
   *   categoriesOfWriter: Array.<number>
   * }}
   * @constructor
   */
  exports.getFreeThingsFromCoupons = function(winbackCoupons) {
    if (!winbackCoupons) { winbackCoupons = []; }
    var couponsMap = {};
    var categoriesOfWriter = [];
    for (var i = 0; i < winbackCoupons.length; i++) {
      if (winbackCoupons[i].service_type_id === SERVICES_IDS.CHOOSE_WRITER) {
        categoriesOfWriter.push(winbackCoupons[i].writer_category_id);
      } else {
        couponsMap[winbackCoupons[i].service_type_id] = winbackCoupons[i];
      }
    }

    return {
      pages: couponsMap[SERVICES_IDS.WRITING_PAGES] && couponsMap[SERVICES_IDS.WRITING_PAGES].quantity || 0,
      slides: couponsMap[SERVICES_IDS.WRITING_SLIDES] && couponsMap[SERVICES_IDS.WRITING_SLIDES].quantity || 0,
      charts: couponsMap[SERVICES_IDS.WRITING_CHARTS] && couponsMap[SERVICES_IDS.WRITING_CHARTS].quantity || 0,
      progressiveDelivery: couponsMap[SERVICES_IDS.PROGRESSIVE_DELIVERY] !== undefined,
      copyOfSources: couponsMap[SERVICES_IDS.USED_SOURCES] !== undefined,
      categoriesOfWriter: categoriesOfWriter,
      writerSamples: couponsMap[SERVICES_IDS.PROVIDE_ME_SAMPLES] !== undefined,
    };
  };


  /**
   * Calculates base services
   * @param {OrderInfo} order
   * @returns {Object.<string, Service>} servicesById
   */
  var calculateBaseServices = function(order) {
    var spacing_factor = (order.spacing === 'single') ? 2 : 1;
    var pricePerPage = order.tariffPricePerPage * spacing_factor;
    var pricePerSlide = normalizePrice(order.tariffPricePerPage * 0.5);
    var pricePerChart = normalizePrice(order.tariffPricePerPage * 0.5);
    var basePagesCost = normalizePrice(order.pages * pricePerPage);
    var baseSlidesCost = normalizePrice(order.slides * pricePerSlide);
    var baseChartsCost = normalizePrice(order.charts * pricePerChart);

    var result = {};

    result[SERVICES_IDS.WRITING_PAGES] = {
      cost: basePagesCost,
      winbackQuantity: 0,
      winbackReduction: 0,
      quantity: order.pages,
      itemPrice: pricePerPage,
      price: 0,
      pricePercent: 0,
    };

    result[SERVICES_IDS.WRITING_SLIDES] = {
      cost: baseSlidesCost,
      winbackQuantity: 0,
      winbackReduction: 0,
      quantity: order.slides,
      itemPrice: pricePerSlide,
      price: 0,
      pricePercent: 0,
    };

    result[SERVICES_IDS.WRITING_CHARTS] = {
      cost: baseChartsCost,
      winbackQuantity: 0,
      winbackReduction: 0,
      quantity: order.charts,
      itemPrice: pricePerChart,
      price: 0,
      pricePercent: 0,
    };

    return result;
  };

  /**
   * Calculates coupons service
   * @param {OrderInfo} order
   * @returns {Object.<string, Service>} servicesById
   */
  var applyBaseCoupons = function(order, servicesById) {
    var spacing_factor = (order.spacing === 'single') ? 2 : 1;
    for (var i = 0; i < order.winbackCoupons.length; i++) {
      var coupon = order.winbackCoupons[i];
      if (coupon.type_id === COUPONS_TYPES_IDS.FREE_UNIT) {
        var service = servicesById[coupon.service_type_id];
        if (service) {
          var quantity = Math.min(coupon.quantity, service.quantity - 1);
          quantity = Math.max(quantity, 0);
            service.winbackQuantity = quantity;
            service.winbackReduction = normalizePrice(service.itemPrice * quantity);
        } else {
          console.error('Cant apply coupon', coupon);
        }
      }
    }
    return servicesById;
  };

  /**
   * Calculates secondary services
   * @param {OrderInfo} order
   * @param {number} baseCost
   * @param {number} baseCouponsReduction
   * @returns {Object.<string, Service>} servicesById
   */
  var calculateSecondaryServices = function(order, baseCost, baseCouponsReduction) {

    var baseCostWithCoupons = baseCost - baseCouponsReduction;

    var pdDisabled = (baseCost < 200 || order.tariffHrs < 120)
      ? 'Available for orders with a deadline of 5 days and longer, and with the value of $200 and more.'
      : undefined;
    var pdForced = (baseCost >= 600 && order.tariffHrs >= 168)
      ? 'Mandatory for 7 days and longer, with the value of $600 and more.'
      : undefined;

    var progressiveDeliveryPrice = pdDisabled
      ? 0
      : normalizePrice(baseCostWithCoupons * PROGRESSIVE_DELIVERY_PERCENT / 100);

    var usedSourcesPrice =  normalizePrice(
      Math.max(USED_SOURCES_MIN_PRICE, baseCostWithCoupons * USED_SOURCES_PERCENT / 100)
    );

    var complexAssignmentPrice = normalizePrice(
      (baseCostWithCoupons * COMPLEX_ASSIGNMENT_PERCENT) / 100
    );

    var expertProofreadingPrice = normalizePrice(
      (baseCostWithCoupons * EXPERT_PROOFREADING_PERCENT) / 100
    );

    var getSamplesPrice = PROVIDE_SAMPLES_PRICE;

    var result = {};

    result[SERVICES_IDS.PROVIDE_ME_SAMPLES] = {
      cost: order.getSamplesOn ? getSamplesPrice : 0,
      winbackQuantity: 0,
      winbackReduction: 0,
      quantity: 0,
      itemPrice: 0,
      price: getSamplesPrice,
      pricePercent: 0,
    };

    result[SERVICES_IDS.PROGRESSIVE_DELIVERY] = {
      cost: (pdForced || order.getProgressiveDeliveryOn) ? progressiveDeliveryPrice : 0,
      winbackQuantity: 0,
      winbackReduction: 0,
      quantity: 0,
      itemPrice: 0,
      price: progressiveDeliveryPrice,
      pricePercent: PROGRESSIVE_DELIVERY_PERCENT,
      disabledReason: pdDisabled,
      forcedReason: pdForced,
    };

    result[SERVICES_IDS.CHOOSE_WRITER] = {
      cost: normalizePrice(baseCostWithCoupons * order.writerPercent / 100),
      winbackQuantity: 0,
      winbackReduction: 0,
      quantity: 0,
    };

    result[SERVICES_IDS.USED_SOURCES] = {
      cost: order.getUsedSourcesOn ? usedSourcesPrice : 0,
      winbackQuantity: 0,
      winbackReduction: 0,
      quantity: 0,
      itemPrice: 0,
      price: usedSourcesPrice,
      pricePercent: usedSourcesPrice > USED_SOURCES_MIN_PRICE ? USED_SOURCES_PERCENT : 0,
    };

    result[SERVICES_IDS.COMPLEX_ASSIGNMENT] = {
      cost: order.complexAssignmentDiscipline ? complexAssignmentPrice : 0,
      winbackQuantity: 0,
      winbackReduction: 0,
      quantity: 0,
      itemPrice: 0,
      price: complexAssignmentPrice,
      pricePercent: COMPLEX_ASSIGNMENT_PERCENT,
    };

    result[SERVICES_IDS.EXPERT_PROOFREADING] = {
      cost: order.expertProofreading ? expertProofreadingPrice : 0,
      winbackQuantity: 0,
      winbackReduction: 0,
      quantity: 0,
      itemPrice: 0,
      price: expertProofreadingPrice,
      pricePercent: EXPERT_PROOFREADING_PERCENT,
    };

    /**** Secondary coupons ****/

    for (var i = 0; i < order.winbackCoupons.length; i++) {
      var coupon = order.winbackCoupons[i];
      if (coupon.type_id === COUPONS_TYPES_IDS.PERCENT) {
        var service = result[coupon.service_type_id];
        if (service !== undefined) {
          result[coupon.service_type_id].winbackReduction =
            normalizePrice(result[coupon.service_type_id].cost / 100 * coupon.value);
        } else {
          throw new Error('Unable to apply coupon for service id=`' + coupon.service_type_id + '`');
        }
      } else if (coupon.type_id === COUPONS_TYPES_IDS.WRITER_CATEGORY) {
        if (order.writerCategoryId === coupon.writer_category_id) {
          result[SERVICES_IDS.CHOOSE_WRITER].winbackReduction = result[SERVICES_IDS.CHOOSE_WRITER].cost;
        }
      }
    }

    return result;

  };


  /**
   * @param {OrderInfo} order
   * @param {number} rawCost
   * @param {number} couponsReduction
   * @returns {Object.<string, Service>} servicesById
   */
  var calculateDiscountServices = function(order, rawCost, couponsReduction) {
	var result = {};
	const coupon = order.discount;

	if (coupon === undefined) return 0;

	let discount = 0;
	if (coupon.type_id === COUPONS_TYPES_IDS.AMOUNT) {
		discount = -coupon.value
	} else {
		discount = -normalizePrice((rawCost - couponsReduction) * coupon.value / 100)
	}

	result[SERVICES_IDS.DISCOUNT] = {
		cost: discount,
		winbackQuantity: 0,
		winbackReduction: 0,
		quantity: 0
	};

    return result;
  };

  // var formatQuantitiveOperation = function format(operation, itemName, skipSign) {
  //   if (operation.operations && operation.operations.length > 0) {
  //     var formattedOperations = [];
  //     for (var i = 0; i < operation.operations.length; i++) {
  //       var subOperation = operation.operations[i];
  //       formattedOperations.push(format(subOperation, itemName, i === 0));
  //     }
  //     return formattedOperations.join(' ');
  //   }
  //   var formatted = operation.quantity +
  //     ' ' + pluralize(itemName, operation.quantity)
  //     + ' × $' + normalizePrice(operation.itemPrice);
  //   if (skipSign) return formatted;
  //   if (operation.sign < 0) return '− ' + formatted;
  //   if (operation.sign > 0) return '+ ' + formatted;
  //   return formatted;
  // };


  // /**
  //  * Returns title for given service & service id
  //  * @param {Service} service
  //  * @param {number|string} serviceId
  //  * @returns {string} Title
  //  */
  // var getServiceTitle = function(service, serviceId) {
  //   switch (serviceId) {
  //     case SERVICES_IDS.PROVIDE_ME_SAMPLES:
  //       return 'Order writer’s samples';
  //     case SERVICES_IDS.PROGRESSIVE_DELIVERY:
  //       return 'Progressive delivery';
  //     case SERVICES_IDS.WRITING_PAGES:
  //       return formatQuantitiveOperation(service, 'page');
  //     case SERVICES_IDS.WRITING_SLIDES:
  //       return formatQuantitiveOperation(service, 'PowerPoint slide');
  //     case SERVICES_IDS.WRITING_CHARTS:
  //       return formatQuantitiveOperation(service, 'chart');
  //     case SERVICES_IDS.CHOOSE_WRITER:
  //       if (ukForm) return 'Desirable grade';
  //       return 'Category of the writer';
  //     case SERVICES_IDS.COMPLEX_ASSIGNMENT:
  //       return 'Complex assignment';
  //     case SERVICES_IDS.DISCOUNT:
  //       return 'Discount';
  //     case SERVICES_IDS.USED_SOURCES:
  //       return 'Copy of sources used';
  //     case ADD_SERVICES_IDS.ADD_BOOST_CATEGORY:
  //       return 'Boost category';
  //     case ADD_SERVICES_IDS.ADD_SHORTEN_DEADLINE:
  //       return 'Shortening of the deadline';
  //     default: return String(serviceId);
  //   }
  // };

  /**
   * Returns title for given service & service id
   * @param {number|string} serviceId
   * @returns {number} Priority
   */
  var getServicePriority = function(serviceId) {
    switch (serviceId) {
      case SERVICES_IDS.WRITING_PAGES: return 1;
      case SERVICES_IDS.WRITING_SLIDES: return 2;
      case SERVICES_IDS.WRITING_CHARTS: return 3;
      case SERVICES_IDS.PROGRESSIVE_DELIVERY: return 4;
      case SERVICES_IDS.CHOOSE_WRITER: return 5;
      case SERVICES_IDS.USED_SOURCES: return 6;
      case SERVICES_IDS.COMPLEX_ASSIGNMENT: return 7;
      case SERVICES_IDS.PROVIDE_ME_SAMPLES: return 8;
      case SERVICES_IDS.ADD_BOOST_CATEGORY: return 10;
      case SERVICES_IDS.ADD_SHORTEN_DEADLINE: return 11;
      case SERVICES_IDS.EXPERT_PROOFREADING: return 12;
      case ADD_SERVICES_IDS.ADD_WRITING_PAGES: return 50;
      case ADD_SERVICES_IDS.ADD_WRITING_SLIDES: return 51;
      case ADD_SERVICES_IDS.ADD_WRITING_CHARTS: return 52;
      case ADD_SERVICES_IDS.ADD_USED_SOURCES: return 53;
      case ADD_SERVICES_IDS.ADD_PROVIDE_ME_SAMPLES: return 54;
      case ADD_SERVICES_IDS.ADD_PROGRESSIVE_DELIVERY: return 55;
      case ADD_SERVICES_IDS.ADD_SHORTEN_DEADLINE: return 56;
      case ADD_SERVICES_IDS.ADD_BOOST_CATEGORY: return 57;
      case ADD_SERVICES_IDS.ADD_EXPERT_PROOFREADING: return 59;
      case SERVICES_IDS.DISCOUNT: return 100;
      default: return 0;
    }
  };

  /**
   * @param {Object.<string, Service>} servicesById Base services
   * @returns {number} Base cost
   */
  var getBaseCost = function(servicesById) {
    return normalizePrice(
      servicesById[SERVICES_IDS.WRITING_PAGES].cost +
      servicesById[SERVICES_IDS.WRITING_SLIDES].cost +
      servicesById[SERVICES_IDS.WRITING_CHARTS].cost
    );
  };

  /**
   * @param {Object.<string, Service>} servicesById Base+base coupons services
   * @returns {number} Base cost
   */
  var getBaseCouponsReduction = function(servicesById) {
    return normalizePrice(
      servicesById[SERVICES_IDS.WRITING_PAGES].winbackReduction +
      servicesById[SERVICES_IDS.WRITING_SLIDES].winbackReduction +
      servicesById[SERVICES_IDS.WRITING_CHARTS].winbackReduction
    );
  };

  /**
   * @param {Object.<string, Service>} servicesById Base+secondary services
   * @returns {number} Secondary cost
   */
  var getSecondaryCost = function(servicesById) {
    return normalizePrice(
      servicesById[SERVICES_IDS.USED_SOURCES].cost +
      servicesById[SERVICES_IDS.PROVIDE_ME_SAMPLES].cost +
      servicesById[SERVICES_IDS.PROGRESSIVE_DELIVERY].cost +
      servicesById[SERVICES_IDS.CHOOSE_WRITER].cost +
      servicesById[SERVICES_IDS.COMPLEX_ASSIGNMENT].cost +
      servicesById[SERVICES_IDS.EXPERT_PROOFREADING].cost
    );
  };

  /**
   * @param {Object.<string, Service>} servicesById any set of services
   * @returns {number} Total cost
   */
  var getTotalCost = function (servicesById) {
    var totalCost = 0;
    for (var serviceId in servicesById) if (servicesById.hasOwnProperty(serviceId)) {
      var service = servicesById[serviceId];
      totalCost = normalizePrice(totalCost + service.cost - service.winbackReduction);
    }
    return totalCost;
  };


  /**
   * @param {Object.<string, Service>} servicesById Base+secondary services
   * @returns {number} Winback coupons reduction
   */
  var getCouponsReduction = function(servicesById) {
    return normalizePrice(
      getBaseCouponsReduction(servicesById) +
      servicesById[SERVICES_IDS.PROVIDE_ME_SAMPLES].winbackReduction +
      servicesById[SERVICES_IDS.PROGRESSIVE_DELIVERY].winbackReduction  +
      servicesById[SERVICES_IDS.USED_SOURCES].winbackReduction  +
      servicesById[SERVICES_IDS.CHOOSE_WRITER].winbackReduction +
      servicesById[SERVICES_IDS.COMPLEX_ASSIGNMENT].winbackReduction +
      servicesById[SERVICES_IDS.EXPERT_PROOFREADING].winbackReduction
    );
  };

  /**
   * @param {Object.<string, Service>} servicesById services
   * @returns {number} Virtual services cost cost
   */
  var getAdditionalServicesRawCost = function(servicesById) {
    var result = 0;
    for (var p in ADD_SERVICES_IDS) if (ADD_SERVICES_IDS.hasOwnProperty(p)) {
      if (servicesById.hasOwnProperty(ADD_SERVICES_IDS[p])) {
        result = normalizePrice(result + servicesById[ADD_SERVICES_IDS[p]].rawCost);
      }
    }
    return result;
  };

  /**
   * Returns complete set of services for order
   * @param {OrderInfo} order
   * @returns {Object.<string, Service>} servicesById
   */
  function calculateServices(order) {
    order = orderWithDefaults(order);
    var servicesById = {};
    assign(servicesById, calculateBaseServices(order));
    applyBaseCoupons(order, servicesById);
    var baseCost = getBaseCost(servicesById);
    var baseCouponsReduction = getBaseCouponsReduction(servicesById);
    assign(servicesById, calculateSecondaryServices(order, baseCost, baseCouponsReduction));
    var secondaryCost = getSecondaryCost(servicesById);
    var rawCost = normalizePrice(baseCost + secondaryCost);
    var couponsReduction = getCouponsReduction(servicesById);
    assign(servicesById, calculateDiscountServices(order, rawCost, couponsReduction));
    return formatServicesById(servicesById);
  }


  /**
   * Formats calculator's output for the end user
   * @param {Object.<string, Service>} servicesById
   * @returns {Object.<string, Service>} Services map
   */
  var formatServicesById = function(servicesById) {
    for (var serviceId in servicesById) if (servicesById.hasOwnProperty(serviceId)) {
      if (!isNaN(+serviceId)) {
        serviceId = +serviceId;
      }
      var service = servicesById[serviceId];
      if (service.$isFormatted) {
        continue;
      } else {
        Object.defineProperty(service, '$isFormatted', { value: true });
      }
      service.winbackReduction = service.winbackReduction || 0;
      // service.title = getServiceTitle(servicesById[serviceId], serviceId);
      service.free = service.cost === service.winbackReduction && service.cost > 0;
      service.type_id = serviceId;
      service.priority = getServicePriority(serviceId);
      service.active = service.cost !== 0 || service.winbackReduction !== 0;
    }

    return servicesById;
  };

  /**
   * Basic orderform calculator
   * @param {OrderInfo} order
   * @returns {CostInfo}
   */
  exports.calculate = function(order) {
    order = orderWithDefaults(order);
    var servicesById = calculateServices(order);
    var output = { servicesById: servicesById, };
    output.baseCost = getBaseCost(servicesById);
    output.secondaryCost = getSecondaryCost(servicesById);
    output.rawCost = normalizePrice(output.baseCost + output.secondaryCost);
    output.couponsReduction = getCouponsReduction(servicesById);
    output.totalCost = getTotalCost(servicesById);
    return output;
  };

  exports.version = 3;

  var getAddServiceOrderInfoPatch = function (addServicesInfo, type_id) {
    switch(type_id) {
      case ADD_SERVICES_IDS.ADD_SHORTEN_DEADLINE: return {
        tariffHrs: addServicesInfo.shortenDeadlineHrs,
        tariffPricePerPage: addServicesInfo.shortenDeadlinePricePerPage,
      };
      case ADD_SERVICES_IDS.ADD_WRITING_PAGES: return {
        pages: addServicesInfo.newPagesQuantity
      };
      case ADD_SERVICES_IDS.ADD_WRITING_SLIDES: return {
        slides: addServicesInfo.newSlidesQuantity
      };
      case ADD_SERVICES_IDS.ADD_WRITING_CHARTS: return {
        charts: addServicesInfo.newChartsQuantity
      };
      case ADD_SERVICES_IDS.ADD_USED_SOURCES: return {
        getUsedSourcesOn: addServicesInfo.getUsedSourcesOn
      };
      case ADD_SERVICES_IDS.ADD_PROVIDE_ME_SAMPLES: return {
        getSamplesOn: addServicesInfo.getSamplesOn,
      };
      case ADD_SERVICES_IDS.ADD_EXPERT_PROOFREADING: return {
        expertProofreading: addServicesInfo.expertProofreading,
      };
      case ADD_SERVICES_IDS.ADD_PROGRESSIVE_DELIVERY: return {
        getProgressiveDeliveryOn: addServicesInfo.getProgressiveDeliveryOn
      };
      case ADD_SERVICES_IDS.ADD_BOOST_CATEGORY: return {
        writerCategoryId: addServicesInfo.boostWriterCategoryId,
        writerPercent: addServicesInfo.boostWriterPercent,
      };
    }
  };

  function calculateAddService(addServicesInfo, mutableOrder, type_id) {
    var patch = getAddServiceOrderInfoPatch(addServicesInfo, type_id);
    var originalServices = calculateServices(mutableOrder);
    assign(mutableOrder, patch);
    var nextServices = calculateServices(mutableOrder);
    var originalRawCost = normalizePrice(getBaseCost(originalServices) + getSecondaryCost(originalServices));
    var nextRawCost = normalizePrice(getBaseCost(nextServices) + getSecondaryCost(nextServices));
    var originalCost = normalizePrice(getTotalCost(originalServices));
    var nextCost = normalizePrice(getTotalCost(nextServices));
    var servicesDiff = subtractServices(nextServices, originalServices, type_id);

    var result = {
      rawCost: normalizePrice(nextRawCost - originalRawCost),
      cost: normalizePrice(nextCost - originalCost),
      winbackQuantity: 0,
      winbackReduction: 0,
    };

    Object.defineProperty(result, 'underlyingServicesById', { value: formatServicesById(servicesDiff), enumerable: false });

    return result;
  }

  /**
   * Subtracts values of cost, winbackReduction and quantity from the minuend map of services
   * @param {Object} nextServices - minuend
   * @param {Object} prevServices - subtrahend
   * @returns {Object.<string, Service>} Services map
   */
  var subtractServices = function(nextServices, prevServices, causedByServiceId) {
    var finalServices = {};
    for (var serviceId in nextServices) if (nextServices.hasOwnProperty(serviceId)) {
      var nextService = nextServices[serviceId];
      var finalService = {
      };
      var prevService = prevServices[serviceId];

      if (prevService) {
        finalService.cost = normalizePrice(nextService.cost - prevService.cost);
        finalService.quantity = nextService.quantity - prevService.quantity;
        finalService.winbackQuantity = nextService.winbackQuantity - prevService.winbackQuantity;
        finalService.winbackReduction = normalizePrice(nextService.winbackReduction - prevService.winbackReduction);

        // Useless metrics, just copy the later values (for consistency) (use service.operations)
        // Todo: remove
        if ('price' in nextService) { finalService.price = nextService.price; }
        if ('itemPrice' in nextService) { finalService.itemPrice = nextService.itemPrice; }
        if ('pricePercent' in nextService) { finalService.pricePercent = nextService.pricePercent; }

        if ('disabledReason' in nextService) { finalService.disabledReason = nextService.disabledReason; }
        if ('forcedReason' in nextService) { finalService.forcedReason = nextService.forcedReason; }

        if (!finalService.operations) {
          Object.defineProperty(finalService, 'operations', { value: [] });
        }
        if (
          nextService.quantity !== prevService.quantity ||
          nextService.price !== prevService.price ||
          nextService.itemPrice !== prevService.itemPrice ||
          nextService.pricePercent !== prevService.pricePercent
        ) {
          // Quantitative information is differs, calculate operations
          if (
            nextService.price === prevService.price &&
            nextService.itemPrice === prevService.itemPrice &&
            nextService.pricePercent === prevService.pricePercent
          ) {
            // Only quantity is different
            finalService.operations.push({
              sign: 1,
              quantity: nextService.quantity - prevService.quantity,
              price: nextService.price,
              itemPrice: nextService.itemPrice,
              pricePercent: nextService.pricePercent,
              causedByServiceId: causedByServiceId,
            })
          } else {
            finalService.operations.push(
              {
                sign: 1,
                quantity: nextService.quantity,
                price: nextService.price,
                itemPrice: nextService.itemPrice,
                pricePercent: nextService.pricePercent,
                causedByServiceId: causedByServiceId,
              },
              {
                sign: -1,
                quantity: prevService.quantity,
                price: prevService.price,
                itemPrice: prevService.itemPrice,
                pricePercent: prevService.pricePercent,
                causedByServiceId: causedByServiceId,
              }
            );
          }
        }
      }
      finalServices[serviceId] = finalService;

    }
    return finalServices;
  };

  var mergeServices = function(aServices, bServices) {
    var mergedServicesById = {};
    for (var serviceId in aServices) if (aServices.hasOwnProperty(serviceId)) {
      var aService = aServices[serviceId];
      var bService = bServices[serviceId];
      var mergedService = assign({}, aService);
      if (aService.operations) {
        Object.defineProperty(mergedService, 'operations', { value: aService.operations.slice() });
      }
      if (bService) {
        mergedService.quantity = mergedService.quantity + bService.quantity;
        mergedService.cost = normalizePrice(mergedService.cost + bService.cost);
        mergedService.winbackQuantity = mergedService.winbackQuantity + bService.winbackQuantity;
        mergedService.winbackReduction = normalizePrice(mergedService.winbackReduction + bService.winbackReduction);

        // Useless metrics, just copy the later values (for consistency) (use service.operations)
        if ('price' in bService) { mergedService.price = bService.price; }
        if ('itemPrice' in bService) { mergedService.itemPrice = bService.itemPrice; }
        if ('pricePercent' in bService) { mergedService.pricePercent = bService.pricePercent; }

        if ('disabledReason' in bService) { mergedService.disabledReason = bService.disabledReason; }
        if ('forcedReason' in bService) { mergedService.forcedReason = bService.forcedReason; }

        if (bService.operations) {
          if (!mergedService.operations) {
            Object.defineProperty(mergedService, 'operations', { value: [], enumerable: false });
          }
          Array.prototype.push.apply(mergedService.operations, bService.operations);
        }
      }
      mergedServicesById[serviceId] = mergedService;
    }
    return mergedServicesById;
  };

  /**
   * @typedef {{
   *   shortenDeadlineHrs: number,
   *   shortenDeadlinePricePerPage: number,
   *   boostWriterCategoryId: number,
   *   boostWriterPercent: number,
   *   newPagesQuantity: number,
   *   newSlidesQuantity: number,
   *   newChartsQuantity: number,
   *   getUsedSourcesOn: boolean,
   *   getSamplesOn: boolean,
   *   getProgressiveDeliveryOn: boolean,
   *   expertProofreading: boolean,
   * }} AddServicesInfo
   */


  /**
   * Calculates additional services by extracting original services
   * @param {AddServicesInfo} addServicesInfo
   * @param {OrderInfo} originalOrder
   * @returns {CostInfo}
   */
  exports.calculateAddServices = function (addServicesInfo, originalOrder) {
    originalOrder = orderWithDefaults(originalOrder);
    addServicesInfo = addServicesInfoWithDefaults(addServicesInfo, originalOrder);

    var mutableOrder = assign({}, originalOrder);

    var addServicesById = {};

    for (var i = 0; i < ADD_SERVICES_ORDER.length; i += 1) {
      var type_id = ADD_SERVICES_ORDER[i];
      var addService = calculateAddService(addServicesInfo, mutableOrder, type_id);
      addServicesById[type_id] = addService;
    }

    return formatServicesById(addServicesById);
  };

  exports.extrudeServices = function(addServicesById, order) {
    order = orderWithDefaults(order);

    // Now the craziness begins
    // We merge all the underlying services except those, which belong to UNMERGEABLE_ADD_SERVICES

    var mergedServicesById = calculateServices(orderWithDefaults());
    var extrudedServicesById = {};

    for (var i = 0; i < ADD_SERVICES_ORDER.length; i += 1) {
      var type_id = ADD_SERVICES_ORDER[i];
      if (UNMERGEABLE_ADD_SERVICES.indexOf(type_id) === -1) {
        var addService = addServicesById[type_id];
        mergedServicesById = mergeServices(mergedServicesById, addService.underlyingServicesById);
      } else {
        extrudedServicesById[type_id] = addServicesById[type_id];
      }
    }

    assign(extrudedServicesById, mergedServicesById);

    var rawCost = getAdditionalServicesRawCost(addServicesById);

    var couponsReduction = 0;
    for (var k in mergedServicesById) if (mergedServicesById.hasOwnProperty(k)) {
      couponsReduction += mergedServicesById[k].winbackReduction || 0;
    }

    var discountServices = calculateDiscountServices(order, rawCost, couponsReduction);
    assign(extrudedServicesById, discountServices);

    return formatServicesById(extrudedServicesById);
  };

  exports.getLastCalcServForAddServices = function(addServicesById) {
    var lastAddServiceId = ADD_SERVICES_ORDER[ADD_SERVICES_ORDER.length - 1];
    return addServicesById[lastAddServiceId].underlyingServicesById;
  };

  exports.getBaseCost = getBaseCost;
  exports.getTotalCost = getTotalCost;
  exports.calculateServices = calculateServices;
  exports.setUkForm = function() { ukForm = true; };

  return exports;
});
