<?php 
/**
 * The base configuration for Proxim
 * @package    Proxim
 * @author     Davison Pro <davisonpro.coder@gmail.com | https://davisonpro.dev>
 * @copyright  2019 Proxim
 * @version    1.0.0
 * @since      File available since Release 1.0.0
 */

namespace Proxim;

use Exception;
use Proxim\Util\ArrayUtils;

/**
 * @property {{
 *   type_id:            number|string,
 *   value:              number|null,
 *   quantity:           number,
 *   service_type_id:    number,
 *   writer_category_id: number|null,
 * }} WinbackCoupon
 *
 * @property {{
 *   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
 *
 * @property {{
 *   pages:               number,
 *   slides:              number,
 *   charts:              number,
 *   progressiveDelivery: boolean,
 *   copyOfSources:       boolean,
 *   writerSamples:       boolean,
 *   categoriesOfWriter:  Array<number>,
 * }} FreeThings
 *
 * @property {{
 *   servicesById: Object.<string, Service>,
 *   totalCost: number,
 * }} CostInfo
 *
 * @property {{
 *   quantity:          number,
 *   type_id:           number|string,
 *   title:             string,
 *   cost:              number,
 *   winbackQuantity:   number,
 *   winbackReduction:  number,
 *   priority:          number,
 *   price:             number,
 *   itemPrice:         number,
 *   pricePercent:      number,
 *   forcedReason:      string|null,
 *   disabledReason:    string|null,
 *   active:            boolean,
 *   operations:        Array,
 * }} Service
 */
class PriceCalculator {

    const ukForm = false;

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

    const COUPONS_TYPES_IDS = [
        'PERCENT' =>           1,
        'AMOUNT' =>            3,
        'FREE_UNIT' =>         5,
        'WRITER_CATEGORY' =>   7,
        'HALF_CUT_DEADLINE' => 8,
        'PRICE_PER_PAGE' => 9
    ];

    const COMPLEX_ASSIGNMENT_PERCENT = 20;
    const EXPERT_PROOFREADING_PERCENT = 25;
    const DRAFT_OUTLINE_PERCENT = 25;
    const PLAGIARISM_REPORT_PRICE = 9.99;
    const VIP_SUPPORT_PRICE = 9.99; 

    const 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,
        'ADD_BOOST_CATEGORY' =>     42,
        'ADD_SHORTEN_DEADLINE' =>   43,
        'VIP_SUPPORT' =>            44,
        'PLAGIARISM_REPORT' =>      45,
        'DRAFT_OUTLINE' =>          46
    ];

    const 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',
        'ADD_VIP_SUPPORT' =>          'vip_support',
        'ADD_PLAGIARISM_REPORT' =>    'plagiarism_report',
        'ADD_DRAFT_OUTLINE' =>        'draft_outline'
    ];

    const ADD_SERVICES_ORDER = [
        self::ADD_SERVICES_IDS['ADD_WRITING_PAGES'],
        self::ADD_SERVICES_IDS['ADD_WRITING_SLIDES'],
        self::ADD_SERVICES_IDS['ADD_WRITING_CHARTS'],
        self::ADD_SERVICES_IDS['ADD_USED_SOURCES'],
        self::ADD_SERVICES_IDS['ADD_PROVIDE_ME_SAMPLES'],
        self::ADD_SERVICES_IDS['ADD_SHORTEN_DEADLINE'],
        self::ADD_SERVICES_IDS['ADD_BOOST_CATEGORY'],
        self::ADD_SERVICES_IDS['ADD_PROGRESSIVE_DELIVERY'],
        self::ADD_SERVICES_IDS['ADD_EXPERT_PROOFREADING'],
        self::ADD_SERVICES_IDS['ADD_VIP_SUPPORT'],
        self::ADD_SERVICES_IDS['ADD_PLAGIARISM_REPORT'],
        self::ADD_SERVICES_IDS['ADD_DRAFT_OUTLINE']
    ];

    const UNMERGEABLE_ADD_SERVICES = [
        self::ADD_SERVICES_IDS['ADD_SHORTEN_DEADLINE'],
        self::ADD_SERVICES_IDS['ADD_BOOST_CATEGORY']
    ];

    /**
     * Approximates number to hundredth
     * @param {number} amount
     * @returns {number}
     */
    public function normalizePrice( $amount ) {
        return 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}
     */
    public function pluralize( $word, $number ) {
        return $number == 1 ? $word : $word . 's';
    }

    /**
     * Validates order values
     * @param {OrderInfo=} order
     * @returns {OrderInfo} valid order
     */
    public function orderWithDefaults( $order ) {
        if ( !$order ) $order = array();
 
        $tariffPricePerPage = (int) ArrayUtils::get($order, 'tariffPricePerPage', 0);
        $coupon = ArrayUtils::get($order, 'discount', null);
        if ( $coupon ) {
            if ($coupon['type_id'] == self::COUPONS_TYPES_IDS['PRICE_PER_PAGE']) {
                $tariffPricePerPage = (float) $coupon['value'];
            }
        }

        $defaults = array(
            'pages' =>                              (int) ArrayUtils::get($order, 'pages', 0),
            'slides' =>                             (int) ArrayUtils::get($order, 'slides', 0),
            'discount' =>                           $coupon,
            'charts' =>                             (int) ArrayUtils::get($order, 'charts', 0),
            'winbackCoupons' =>                     ArrayUtils::get($order, 'winbackCoupons', array()),
            'writerCategoryId' =>                   (int) ArrayUtils::get($order, 'writerCategoryId', null),
            'tariffPricePerPage' =>                 $tariffPricePerPage,
            'tariffHrs' =>                          (int) ArrayUtils::get($order, 'tariffHrs', 0),
            'spacing' =>                            ArrayUtils::get($order, 'spacing', null),
            'getSamplesOn' =>                       (bool) ArrayUtils::get($order, 'getSamplesOn'),
            'getProgressiveDeliveryOn' =>           (bool) ArrayUtils::get($order, 'getProgressiveDeliveryOn'),
            'getUsedSourcesOn' =>                   (bool) ArrayUtils::get($order, 'getUsedSourcesOn'),
            'complexAssignmentDiscipline' =>        (bool) ArrayUtils::get($order, 'complexAssignmentDiscipline'),
            'writerPercent' =>                      ArrayUtils::get($order, 'writerPercent'),
            'expertProofreading' =>                 (bool) ArrayUtils::get($order, 'expertProofreading'),
            'vipSupport' =>                         (bool) ArrayUtils::get($order, 'vipSupport'),
            'plagiarismReport' =>                   (bool) ArrayUtils::get($order, 'plagiarismReport'),
            'draftOutline' =>                       (bool) ArrayUtils::get($order, 'draftOutline')
        );

        return $defaults;
    }

    /**
     * Validates additional services info
     * @param {AddServicesInfo=} addServicesInfo
     * @param {OrderInfo} originalOrder
     * @returns {AddServicesInfo}
     */
    public function addServicesInfoWithDefaults( $addServicesInfo, $originalOrder ) {
        if (!ArrayUtils::get($addServicesInfo, 'newPagesQuantity')) {
            $addServicesInfo['newPagesQuantity'] = ArrayUtils::get($originalOrder, 'pages');
        }
        if ( !ArrayUtils::get($addServicesInfo, 'newSlidesQuantity') ) {
            $addServicesInfo['newSlidesQuantity'] = ArrayUtils::get($originalOrder, 'slides');
        }
        if (!ArrayUtils::get($addServicesInfo, 'newChartsQuantity')) {
            $addServicesInfo['newChartsQuantity'] = ArrayUtils::get($originalOrder, 'charts');
        }
        if (!ArrayUtils::get($addServicesInfo, 'getUsedSourcesOn')) {
            $addServicesInfo['getUsedSourcesOn'] = ArrayUtils::get($originalOrder, 'getUsedSourcesOn');
        }
        if (!ArrayUtils::get($addServicesInfo, 'getSamplesOn')) {
            $addServicesInfo['getSamplesOn'] = ArrayUtils::get($originalOrder, 'getSamplesOn');
        }
        if (!ArrayUtils::get($addServicesInfo, 'expertProofreading')) {
            $addServicesInfo['expertProofreading'] = ArrayUtils::get($originalOrder, 'expertProofreading');
        }
        if (!ArrayUtils::get($addServicesInfo, 'vipSupport')) {
            $addServicesInfo['vipSupport'] = ArrayUtils::get($originalOrder, 'vipSupport');
        }
        if (!ArrayUtils::get($addServicesInfo, 'plagiarismReport')) {
            $addServicesInfo['plagiarismReport'] = ArrayUtils::get($originalOrder, 'plagiarismReport');
        }
        if (!ArrayUtils::get($addServicesInfo, 'draftOutline')) {
            $addServicesInfo['draftOutline'] = ArrayUtils::get($originalOrder, 'draftOutline');
        }
        if (!ArrayUtils::get($addServicesInfo, 'getProgressiveDeliveryOn')) {
            $addServicesInfo['getProgressiveDeliveryOn'] = ArrayUtils::get($originalOrder, 'getProgressiveDeliveryOn');
        }
        if ( !ArrayUtils::get($addServicesInfo, 'shortenDeadlinePricePerPage') ) {
            $addServicesInfo['shortenDeadlinePricePerPage'] = (int) ArrayUtils::get($originalOrder, 'tariffPricePerPage');
        }
        if ( !ArrayUtils::get($addServicesInfo, 'shortenDeadlineHrs') ) {
            $addServicesInfo['shortenDeadlineHrs'] = (int) ArrayUtils::get($originalOrder, 'tariffHrs');
        }
        if (!ArrayUtils::get($addServicesInfo, 'boostWriterCategoryId')) {
            $addServicesInfo['boostWriterCategoryId'] = ArrayUtils::get($originalOrder, 'writerCategoryId');
        }

        if ( !ArrayUtils::get($addServicesInfo, 'boostWriterPercent')) {
            $addServicesInfo['boostWriterPercent'] = ArrayUtils::get($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
     */
    public function getFreeThingsFromCoupons( $winbackCoupons ) {
        if (!$winbackCoupons) { $winbackCoupons = []; }

        $couponsMap = [];
        $categoriesOfWriter = [];
        for ($i = 0; $i < count($winbackCoupons); $i++) {
            if ($winbackCoupons[$i]['service_type_id'] == self::SERVICES_IDS['CHOOSE_WRITER'] ) {
                $categoriesOfWriter[] = $winbackCoupons[$i]['writer_category_id'];
            } else {
                $couponsMap[$winbackCoupons[$i]['service_type_id']] = $winbackCoupons[$i];
            }
        }

        return array(
            'pages' => $couponsMap[self::SERVICES_IDS['WRITING_PAGES']] && 
                            $couponsMap[self::SERVICES_IDS['WRITING_PAGES']]['quantity'] || 0,
            'slides' => $couponsMap[self::SERVICES_IDS['WRITING_SLIDES']] && 
                            $couponsMap[self::SERVICES_IDS['WRITING_SLIDES']]['quantity'] || 0,
            'charts' => $couponsMap[self::SERVICES_IDS['WRITING_CHARTS']] && 
                            $couponsMap[self::SERVICES_IDS['WRITING_CHARTS']]['quantity'] || 0,
            'progressiveDelivery' => (bool) $couponsMap[self::SERVICES_IDS['PROGRESSIVE_DELIVERY']],
            'copyOfSources' => (bool) $couponsMap[self::SERVICES_IDS['USED_SOURCES']],
            'categoriesOfWriter' => $categoriesOfWriter,
            'writerSamples' => (bool) $couponsMap[self::SERVICES_IDS['PROVIDE_ME_SAMPLES']],
        );
    }

    /**
     * Calculates base services
     * @param {OrderInfo} order
     * @returns {Object.<string, Service>} servicesById
     */
    public function calculateBaseServices( $order ) {
        $spacing_factor = ArrayUtils::get($order, 'spacing') == 'single' ? 2 : 1;
        $pricePerPage = ArrayUtils::get($order, 'tariffPricePerPage') * $spacing_factor;
        $pricePerSlide = $this->normalizePrice(ArrayUtils::get($order, 'tariffPricePerPage') * 0.5);
        $pricePerChart = $this->normalizePrice(ArrayUtils::get($order, 'tariffPricePerPage') * 0.5);
        
        $pages = (int) ArrayUtils::get($order, 'pages');
        $slides = (int) ArrayUtils::get($order, 'slides');
        $charts = (int) ArrayUtils::get($order, 'charts');

        ## pages
        $basePagesCost = $this->normalizePrice($pages * $pricePerPage);
        ## slides
        $baseSlidesCost = $this->normalizePrice($slides * $pricePerSlide);
        ## charts
        $baseChartsCost = $this->normalizePrice($charts * $pricePerChart);

        $writerCostPerPageType = Configuration::get('WRITER_CPP_TYPE', PROX_ADMIN_SITE_ID, "percentage");
        if ($writerCostPerPageType == "fixed") {
            $writerPricePerPage = (float) Configuration::get('WRITER_AMOUNT_CPP', PROX_ADMIN_SITE_ID, 0);
            $writerPricePerPage = $writerPricePerPage * $spacing_factor;
            $writerPricePerSlide = $this->normalizePrice($writerPricePerPage * 0.5);
            $writerPricePerChart = $this->normalizePrice($writerPricePerPage * 0.5);
        
            $writerPagesCost = $this->normalizePrice($pages * $writerPricePerPage);
            $writerSlidesCost = $this->normalizePrice($slides * $writerPricePerSlide);
            $writerChartsCost = $this->normalizePrice($charts * $writerPricePerChart);
        } else {
            $writerPercentageCpp = (float) Configuration::get('WRITER_PERCENTAGE_CPP', PROX_ADMIN_SITE_ID, 0);
            $writerPricePerPage = $this->normalizePrice(
                ($pricePerPage * $writerPercentageCpp) / 100
            );
            $writerPricePerPage = $writerPricePerPage * $spacing_factor;

            $writerPricePerSlide = $this->normalizePrice($writerPricePerPage * 0.5);
            $writerPricePerChart = $this->normalizePrice($writerPricePerPage * 0.5);

            $writerPagesCost = $this->normalizePrice($pages * $writerPricePerPage);
            $writerSlidesCost = $this->normalizePrice($slides * $writerPricePerSlide);
            $writerChartsCost = $this->normalizePrice($charts * $writerPricePerChart);
        }

        $editorCostPerPageType = Configuration::get('EDITOR_CPP_TYPE', PROX_ADMIN_SITE_ID, "percentage");
        if ($editorCostPerPageType == "fixed") {
            $editorPricePerPage = (float) Configuration::get('EDITOR_AMOUNT_CPP', PROX_ADMIN_SITE_ID, 0);
            $editorPricePerPage = $editorPricePerPage * $spacing_factor;
            $editorPricePerSlide = $this->normalizePrice($editorPricePerPage * 0.5);
            $editorPricePerChart = $this->normalizePrice($editorPricePerPage * 0.5);
            
            $editorPagesCost = $this->normalizePrice($pages * $editorPricePerPage);
            $editorSlidesCost = $this->normalizePrice($slides * $editorPricePerSlide);
            $editorChartsCost = $this->normalizePrice($charts * $editorPricePerChart);
        } else {
            $editorPercentageCpp = (float) Configuration::get('EDITOR_PERCENTAGE_CPP', PROX_ADMIN_SITE_ID, 0);
            $editorPricePerPage = $this->normalizePrice(
                ($pricePerPage * $editorPercentageCpp) / 100
            );
            $editorPricePerPage = $editorPricePerPage * $spacing_factor;

            $editorPricePerSlide = $this->normalizePrice($editorPricePerPage * 0.5);
            $editorPricePerChart = $this->normalizePrice($editorPricePerPage * 0.5);

            $editorPagesCost = $this->normalizePrice($pages * $editorPricePerPage);
            $editorSlidesCost = $this->normalizePrice($slides * $editorPricePerSlide);
            $editorChartsCost = $this->normalizePrice($charts * $editorPricePerChart);
        }


        $result = [];

        $result[self::SERVICES_IDS['WRITING_PAGES']] = [
            'cost' => $basePagesCost,
            'writerCost' => $writerPagesCost,
            'editorCost' => $editorPagesCost,
            'winbackQuantity' => 0,
            'winbackReduction' => 0,
            'quantity' => ArrayUtils::get($order, 'pages'),
            'itemPrice' => $pricePerPage,
            'price' => 0,
            'pricePercent' => 0,
        ];
    
        $result[self::SERVICES_IDS['WRITING_SLIDES']] = [
            'cost' => $baseSlidesCost,
            'writerCost' => $writerSlidesCost,
            'editorCost' => $editorSlidesCost,
            'winbackQuantity' => 0,
            'winbackReduction' => 0,
            'quantity' => ArrayUtils::get($order, 'slides'),
            'itemPrice' => $pricePerSlide,
            'price' => 0,
            'pricePercent' => 0,
        ];
    
        $result[self::SERVICES_IDS['WRITING_CHARTS']] = [
            'cost' => $baseChartsCost,
            'writerCost' => $writerChartsCost,
            'editorCost' => $editorChartsCost,
            'winbackQuantity' => 0,
            'winbackReduction' => 0,
            'quantity' => ArrayUtils::get($order, 'charts'),
            'itemPrice' => $pricePerChart,
            'price' => 0,
            'pricePercent' => 0,
        ];
    
        return $result;
    }

    /**
     * Calculates coupons service
     * @param {OrderInfo} order
     * @returns {Object.<string, Service>} servicesById
     */
    public function applyBaseCoupons($order, $servicesById) {
        $spacing_factor = (ArrayUtils::get($order, 'spacing') == 'single') ? 2 : 1;
        for ($i = 0; $i < count(ArrayUtils::get($order, 'winbackCoupons')); $i++) {
            $winbackCoupons = ArrayUtils::get($order, 'winbackCoupons');
            $coupon = $winbackCoupons[$i];
            if ( $coupon->type_id == self::COUPONS_TYPES_IDS['FREE_UNIT'] ) {
                $service = $servicesById[$coupon->service_type_id];
                if ($service) {
                    $quantity = min($coupon->quantity, $service->quantity - 1);
                    $quantity = max($quantity, 0);
                    $service['winbackQuantity'] = $quantity;
                    $service['winbackReduction'] = $this->normalizePrice($service['itemPrice'] * $quantity);
                } else {
                    throw new Exception("Cant apply coupon");
                }
            }
        }

        return $servicesById;
    }

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

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

        $progressiveDeliveryPrice = $pdDisabled
            ? 0
            : $this->normalizePrice($baseCostWithCoupons * self::PROGRESSIVE_DELIVERY_PERCENT / 100);

        $writerProgressiveDeliveryPercentage = (float) Configuration::get('WRITER_PROGRESSIVE_DELIVERY_COST', PROX_ADMIN_SITE_ID, 0);
        $writerProgressiveDeliveryPrice = $this->normalizePrice(
            ($progressiveDeliveryPrice * $writerProgressiveDeliveryPercentage) / 100
        );

        $editorProgressiveDeliveryPercentage = (float) Configuration::get('EDITOR_PROGRESSIVE_DELIVERY_COST', PROX_ADMIN_SITE_ID, 0);
        $editorProgressiveDeliveryPrice = $this->normalizePrice(
            ($progressiveDeliveryPrice * $editorProgressiveDeliveryPercentage) / 100
        );

        /** Used Sources **/
        $usedSourcesPrice =  $this->normalizePrice(
            max( self::USED_SOURCES_MIN_PRICE, $baseCostWithCoupons * self::USED_SOURCES_PERCENT / 100)
        );

        $writerUsedSourcesPercentage = (float) Configuration::get('WRITER_USED_SOURCES_COST', PROX_ADMIN_SITE_ID, 0);
        $writerUsedSourcesPrice =  $this->normalizePrice(
            ($usedSourcesPrice * $writerUsedSourcesPercentage) / 100
        );

        $editorUsedSourcesPercentage = (float) Configuration::get('EDITOR_USED_SOURCES_COST', PROX_ADMIN_SITE_ID, 0);
        $editorUsedSourcesPrice =  $this->normalizePrice(
            ($usedSourcesPrice * $editorUsedSourcesPercentage) / 100
        );
    
        /** Complex Assignment **/
        $complexAssignmentPrice = $this->normalizePrice(
            ($baseCostWithCoupons * self::COMPLEX_ASSIGNMENT_PERCENT) / 100
        );
 
        $writerComplexAssignmentPercentage = (float) Configuration::get('WRITER_COMPLEX_ASSIGNMENT_COST', PROX_ADMIN_SITE_ID, 0);
        $writerComplexAssignmentPrice =  $this->normalizePrice(
            ($complexAssignmentPrice * $writerComplexAssignmentPercentage) / 100
        );

        $editorComplexAssignmentPercentage = (float) Configuration::get('EDITOR_COMPLEX_ASSIGNMENT_COST', PROX_ADMIN_SITE_ID, 0);
        $editorComplexAssignmentPrice =  $this->normalizePrice(
            ($complexAssignmentPrice * $editorComplexAssignmentPercentage) / 100
        );
        
        /** Expert Proofreading **/
        $expertProofreadingPrice = $this->normalizePrice(
            ($baseCostWithCoupons * self::EXPERT_PROOFREADING_PERCENT) / 100
        );

        $writerExpertProofreadingPercentage = (float) Configuration::get('WRITER_EXPERT_PROOFREADING_COST', PROX_ADMIN_SITE_ID, 0);
        $writerExpertProofreadingPrice =  $this->normalizePrice(
            ($expertProofreadingPrice * $writerExpertProofreadingPercentage) / 100
        );

        $editorExpertProofreadingPercentage = (float) Configuration::get('EDITOR_EXPERT_PROOFREADING_COST', PROX_ADMIN_SITE_ID, 0);
        $editorExpertProofreadingPrice =  $this->normalizePrice(
            ($expertProofreadingPrice * $editorExpertProofreadingPercentage) / 100
        );

        /** Draft Outline **/
        $draftOutlinePrice = $this->normalizePrice(
            ($baseCostWithCoupons * self::DRAFT_OUTLINE_PERCENT) / 100
        );

        $writerDraftOutlinePrice = (float) Configuration::get('WRITER_DRAFT_OUTLINE_COST', PROX_ADMIN_SITE_ID, 0);
        $writerDraftOutlinePrice =  $this->normalizePrice(
            ($draftOutlinePrice * $writerDraftOutlinePrice) / 100
        );

        $editorDraftOutlinePrice = (float) Configuration::get('EDITOR_DRAFT_OUTLINE_COST', PROX_ADMIN_SITE_ID, 0);
        $editorDraftOutlinePrice =  $this->normalizePrice(
            ($draftOutlinePrice * $editorDraftOutlinePrice) / 100
        );

        /** Writer Samples **/
        $getSamplesPrice = self::PROVIDE_SAMPLES_PRICE;

        $writerGetSamplesPercentage = (float) Configuration::get('WRITER_PROVIDE_ME_SAMPLES_COST', PROX_ADMIN_SITE_ID, 0);
        $writerGetSamplesPrice =  $this->normalizePrice(
            ($getSamplesPrice * $writerGetSamplesPercentage) / 100
        );

        $editorGetSamplesPercentage = (float) Configuration::get('EDITOR_PROVIDE_ME_SAMPLES_COST', PROX_ADMIN_SITE_ID, 0);
        $editorGetSamplesPrice =  $this->normalizePrice(
            ($getSamplesPrice * $editorGetSamplesPercentage) / 100
        );

        /** VIP Support **/
        $vipSupportPrice = self::VIP_SUPPORT_PRICE;

        $writerVipSupportPercentage = (float) Configuration::get('WRITER_VIP_SUPPORT_COST', PROX_ADMIN_SITE_ID, 0);
        $writerVipSupportPrice =  $this->normalizePrice(
            ($vipSupportPrice * $writerVipSupportPercentage) / 100
        );

        $editorVipSupportPercentage = (float) Configuration::get('EDITOR_VIP_SUPPORT_COST', PROX_ADMIN_SITE_ID, 0);
        $editorVipSupportPrice =  $this->normalizePrice(
            ($vipSupportPrice * $editorVipSupportPercentage) / 100
        );

        /** Plagiarism Report **/
        $plagiarismReportPrice = self::PLAGIARISM_REPORT_PRICE;

        $writerPlagiarismReportPercentage = (float) Configuration::get('WRITER_PLAGIARISM_REPORT_COST', PROX_ADMIN_SITE_ID, 0);
        $writerPlagiarismReportPrice =  $this->normalizePrice(
            ($plagiarismReportPrice * $writerPlagiarismReportPercentage) / 100
        );

        $editorPlagiarismReportPercentage = (float) Configuration::get('EDITOR_PLAGIARISM_REPORT_COST', PROX_ADMIN_SITE_ID, 0);
        $editorPlagiarismReportPrice =  $this->normalizePrice(
            ($plagiarismReportPrice * $editorPlagiarismReportPercentage) / 100
        );

        /** Writer Percent **/
        $writerPercentPrice = $this->normalizePrice($baseCostWithCoupons * ArrayUtils::get($order, 'writerPercent') / 100);

        $writerCategoryPricePercentage = (float) Configuration::get('WRITER_W_CATEGORY_COST', PROX_ADMIN_SITE_ID, 0);
        $writerCategoryPrice =  $this->normalizePrice(
            ($writerPercentPrice * $writerCategoryPricePercentage) / 100
        );

        $editorCategoryPricePercentage = (float) Configuration::get('EDITOR_W_CATEGORY_COST', PROX_ADMIN_SITE_ID, 0);
        $editorCategoryPrice =  $this->normalizePrice(
            ($writerPercentPrice * $editorCategoryPricePercentage) / 100
        );

        $result = [];

        $result[self::SERVICES_IDS['PROVIDE_ME_SAMPLES']] = [
            'cost' => ArrayUtils::get($order, 'getSamplesOn') ? $getSamplesPrice : 0,
            'writerCost' => ArrayUtils::get($order, 'getSamplesOn') ? $writerGetSamplesPrice : 0,
            'editorCost' => ArrayUtils::get($order, 'getSamplesOn') ? $editorGetSamplesPrice : 0,
            'winbackQuantity' => 0,
            'winbackReduction' => 0,
            'quantity' => 0,
            'itemPrice' => 0,
            'price' => $getSamplesPrice,
            'pricePercent' => 0,
        ];

        $result[self::SERVICES_IDS['PROGRESSIVE_DELIVERY']] = [
            'cost' => ($pdForced || ArrayUtils::get($order, 'getProgressiveDeliveryOn')) ? $progressiveDeliveryPrice : 0,
            'writerCost' => ($pdForced || ArrayUtils::get($order, 'getProgressiveDeliveryOn')) ? $writerProgressiveDeliveryPrice : 0,
            'editorCost' => ($pdForced || ArrayUtils::get($order, 'getProgressiveDeliveryOn')) ? $editorProgressiveDeliveryPrice : 0,
            'winbackQuantity' => 0,
            'winbackReduction' => 0,
            'quantity' => 0,
            'itemPrice' => 0,
            'price' => $progressiveDeliveryPrice,
            'pricePercent' => self::PROGRESSIVE_DELIVERY_PERCENT,
            'disabledReason' => $pdDisabled,
            'forcedReason' => $pdForced,
        ];

        $result[self::SERVICES_IDS['CHOOSE_WRITER']] = [
            'cost' => $writerPercentPrice,
            'writerCost' => $writerCategoryPrice,
            'editorCost' => 0,
            'winbackQuantity' => 0,
            'winbackReduction' => 0,
            'quantity' => 0,
        ];

        $result[self::SERVICES_IDS['USED_SOURCES']] = [
            'cost' => ArrayUtils::get($order, 'getUsedSourcesOn') ? $usedSourcesPrice : 0,
            'writerCost' => ArrayUtils::get($order, 'getUsedSourcesOn') ? $writerUsedSourcesPrice : 0,
            'editorCost' => ArrayUtils::get($order, 'getUsedSourcesOn') ? $editorUsedSourcesPrice : 0,
            'winbackQuantity' => 0,
            'winbackReduction' => 0,
            'quantity' => 0,
            'itemPrice' => 0,
            'price' => $usedSourcesPrice,
            'pricePercent' => $usedSourcesPrice > self::USED_SOURCES_MIN_PRICE ? self::USED_SOURCES_PERCENT : 0,
        ];

        $result[self::SERVICES_IDS['COMPLEX_ASSIGNMENT']] = [
            'cost' => ArrayUtils::get($order, 'complexAssignmentDiscipline') ? $complexAssignmentPrice : 0,
            'writerCost' => ArrayUtils::get($order, 'complexAssignmentDiscipline') ? $writerComplexAssignmentPrice : 0,
            'editorCost' => ArrayUtils::get($order, 'complexAssignmentDiscipline') ? $editorComplexAssignmentPrice : 0,
            'winbackQuantity' => 0,
            'winbackReduction' => 0,
            'quantity' => 0,
            'itemPrice' => 0,
            'price' => $complexAssignmentPrice,
            'pricePercent' => self::COMPLEX_ASSIGNMENT_PERCENT,
        ];

        $result[self::SERVICES_IDS['EXPERT_PROOFREADING']] = [
            'cost' => ArrayUtils::get($order, 'expertProofreading') ? $expertProofreadingPrice : 0,
            'writerCost' => ArrayUtils::get($order, 'expertProofreading') ? $writerExpertProofreadingPrice : 0,
            'editorCost' => ArrayUtils::get($order, 'expertProofreading') ? $editorExpertProofreadingPrice : 0,
            'winbackQuantity' => 0,
            'winbackReduction' => 0,
            'quantity' => 0,
            'itemPrice' => 0,
            'price' => $expertProofreadingPrice,
            'pricePercent' => self::EXPERT_PROOFREADING_PERCENT,
        ];

        $result[self::SERVICES_IDS['VIP_SUPPORT']] = [
            'cost' => ArrayUtils::get($order, 'vipSupport') ? $vipSupportPrice : 0,
            'writerCost' => ArrayUtils::get($order, 'vipSupport') ? $writerVipSupportPrice : 0,
            'editorCost' => ArrayUtils::get($order, 'vipSupport') ? $editorVipSupportPrice : 0,
            'winbackQuantity' => 0,
            'winbackReduction' => 0,
            'quantity' => 0,
            'itemPrice' => 0,
            'price' => $vipSupportPrice,
            'pricePercent' => 0
        ];

        $result[self::SERVICES_IDS['PLAGIARISM_REPORT']] = [
            'cost' => ArrayUtils::get($order, 'plagiarismReport') ? $plagiarismReportPrice : 0,
            'writerCost' => ArrayUtils::get($order, 'plagiarismReport') ? $writerPlagiarismReportPrice : 0,
            'editorCost' => ArrayUtils::get($order, 'plagiarismReport') ? $editorPlagiarismReportPrice : 0,
            'winbackQuantity' => 0,
            'winbackReduction' => 0,
            'quantity' => 0,
            'itemPrice' => 0,
            'price' => $plagiarismReportPrice,
            'pricePercent' => 0,
        ];

        $result[self::SERVICES_IDS['DRAFT_OUTLINE']] = [
            'cost' => ArrayUtils::get($order, 'draftOutline') ? $draftOutlinePrice : 0,
            'writerCost' => ArrayUtils::get($order, 'draftOutline') ? $writerDraftOutlinePrice : 0,
            'editorCost' => ArrayUtils::get($order, 'draftOutline') ? $editorDraftOutlinePrice : 0,
            'winbackQuantity' => 0,
            'winbackReduction' => 0,
            'quantity' => 0,
            'itemPrice' => 0,
            'price' => $draftOutlinePrice,
            'pricePercent' => self::DRAFT_OUTLINE_PERCENT,
        ];

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

        for ($i = 0; $i < count(ArrayUtils::get($order, 'winbackCoupons')); $i++) {
            $winbackCoupons = ArrayUtils::get($order, 'winbackCoupons');
            $coupon = $winbackCoupons[$i];
            if ($coupon['type_id'] == self::COUPONS_TYPES_IDS['PERCENT'] ) {
                $service = $result[ $coupon['service_type_id'] ];
                $result[$coupon['service_type_id']]['winbackReduction'] = $this->normalizePrice($result[$coupon['service_type_id']]['cost'] / 100 * $coupon['value'] );
                
            } else if ($coupon['type_id'] == self::COUPONS_TYPES_IDS['WRITER_CATEGORY'] ) {
                if (ArrayUtils::get($order, 'writerCategoryId') == $coupon['writer_category_id'] ) {
                    $result[self::SERVICES_IDS['CHOOSE_WRITER']]['winbackReduction'] = $result[self::SERVICES_IDS['CHOOSE_WRITER']]['cost'];
                }
            }
        }

        return $result;
    }

    /**
     * @param {OrderInfo} order
     * @param {number} rawCost
     * @param {number} couponsReduction
     * @returns {Object.<string, Service>} servicesById
     */
    public function calculateDiscountServices( $order, $rawCost, $couponsReduction ) {
        $result = [];
        $coupon = ArrayUtils::get($order, 'discount');
    
        if (!is_array($coupon))  return [];

        $discount = 0;
        if ( $coupon['type_id'] == self::COUPONS_TYPES_IDS['AMOUNT'] ) {
            $discount = -$coupon['value'];
        } else {
            $discount = -$this->normalizePrice(($rawCost - $couponsReduction) * $coupon['value'] / 100);
        }
    
        $result[ self::SERVICES_IDS['DISCOUNT'] ] = [
            'cost' => $discount,
            'writerCost' => 0,
            'editorCost' => 0,
            'winbackQuantity' => 0,
            'winbackReduction' => 0,
            'quantity' => 0
        ];
    
        return $result;
    }

    public function formatQuantitiveOperation( $operation, $itemName, $skipSign = true ) {
        $operations = ArrayUtils::get($operation, 'operations');

        if ( is_array($operations) ) {
            $formattedOperations = [];
            for ($i = 0; $i < count($operations); $i++) {
                $subOperation = ArrayUtils::get($operations, $i);
                $formattedOperations[] = $this->formatQuantitiveOperation($subOperation, $itemName, $i === 0);
            }

            return implode(' ', $formattedOperations);
        }

        $quantity = ArrayUtils::get($operation, 'quantity');
        $itemPrice = ArrayUtils::get($operation, 'itemPrice');
        $sign = ArrayUtils::get($operation, 'sign');

        $formatted = $quantity . ' ' . $this->pluralize($itemName, $quantity) . ' × $' . $this->normalizePrice($itemPrice);
        if ($skipSign) return $formatted;
        if ($sign < 0) return '− ' . $formatted;
        if ($sign > 0) return '+ ' . $formatted;

        return $formatted;
    }   

    /**
     * Returns title for given service & service id
     * @param {Service} service
     * @param {number|string} serviceId
     * @returns {string} Title
     */
    public function getServiceTitle( $service, $serviceId ) {
        switch ($serviceId) {
            case self::SERVICES_IDS['PROVIDE_ME_SAMPLES']:
                return 'Order writer’s samples';
                break;

            case self::SERVICES_IDS['EXPERT_PROOFREADING']:
                return 'Expert Proofreading';
                break;

            case self::SERVICES_IDS['VIP_SUPPORT']:
                return 'VIP Support';
                break;

            case self::SERVICES_IDS['PLAGIARISM_REPORT']:
                return 'Plagiarism Report';
                break;

            case self::SERVICES_IDS['DRAFT_OUTLINE']:
                return 'Draft/Outline';
                break;

            case self::SERVICES_IDS['PROGRESSIVE_DELIVERY']:
                return 'Progressive delivery';
                break;
                
            case self::SERVICES_IDS['WRITING_PAGES']:
                return $this->formatQuantitiveOperation($service, 'page');
                break;
                
            case self::SERVICES_IDS['WRITING_SLIDES']:
                return $this->formatQuantitiveOperation($service, 'PowerPoint slide');
                break;
                
            case self::SERVICES_IDS['WRITING_CHARTS']:
                return $this->formatQuantitiveOperation($service, 'chart');
                break;
                
            case self::SERVICES_IDS['CHOOSE_WRITER']:
                return 'Category of the writer';
                break;
                
            case self::SERVICES_IDS['COMPLEX_ASSIGNMENT']:
                return 'Complex assignment';
                break;
                
            case self::SERVICES_IDS['DISCOUNT']:
                return 'Discount';
                break;
                
            case self::SERVICES_IDS['USED_SOURCES']:
                return 'Copy of sources used';
                break;
                
            case self::ADD_SERVICES_IDS['ADD_BOOST_CATEGORY']:
                return 'Boost category';
                break;
                
            case self::ADD_SERVICES_IDS['ADD_SHORTEN_DEADLINE']:
                return 'Shortening of the deadline';
                break;
                
            default: 
                return $serviceId;
        }
    }

    /**
     * Returns title for given service & service id
     * @param {number|string} serviceId
     * @returns {number} Priority
     */
    public function getServicePriority($serviceId) {
        switch ($serviceId) {
            case self::SERVICES_IDS['WRITING_PAGES'] : 
                return 1;
                break;

            case self::SERVICES_IDS['WRITING_SLIDES'] : 
                return 2;
                break;

            case self::SERVICES_IDS['WRITING_CHARTS'] : 
                return 3;
                break;

            case self::SERVICES_IDS['PROGRESSIVE_DELIVERY'] : 
                return 4;
                break;

            case self::SERVICES_IDS['CHOOSE_WRITER'] : 
                return 5;
                break;

            case self::SERVICES_IDS['USED_SOURCES'] : 
                return 6;
                break;

            case self::SERVICES_IDS['COMPLEX_ASSIGNMENT'] : 
                return 7;
                break;

            case self::SERVICES_IDS['PROVIDE_ME_SAMPLES'] : 
                return 8;
                break;

            case self::SERVICES_IDS['ADD_BOOST_CATEGORY']: 
                return 10;
                break;

            case self::SERVICES_IDS['ADD_SHORTEN_DEADLINE']: 
                return 11;
                break;

            case self::SERVICES_IDS['EXPERT_PROOFREADING'] : 
                return 12;
                break;

            case self::SERVICES_IDS['VIP_SUPPORT'] : 
                return 13;
                break;

            case self::SERVICES_IDS['PLAGIARISM_REPORT'] : 
                return 14;
                break;

            case self::SERVICES_IDS['DRAFT_OUTLINE'] : 
                return 15;
                break;

            case self::ADD_SERVICES_IDS['ADD_WRITING_PAGES'] : 
                return 50;
                break;

            case self::ADD_SERVICES_IDS['ADD_WRITING_SLIDES'] : 
                return 51;
                break;

            case self::ADD_SERVICES_IDS['ADD_WRITING_CHARTS'] : 
                return 52;
                break;

            case self::ADD_SERVICES_IDS['ADD_USED_SOURCES'] : 
                return 53;
                break;

            case self::ADD_SERVICES_IDS['ADD_PROVIDE_ME_SAMPLES'] : 
                return 54;
                break;

            case self::ADD_SERVICES_IDS['ADD_PROGRESSIVE_DELIVERY'] : 
                return 55;
                break;

            case self::ADD_SERVICES_IDS['ADD_SHORTEN_DEADLINE'] : 
                return 56;
                break;

            case self::ADD_SERVICES_IDS['ADD_BOOST_CATEGORY'] : 
                return 57;
                break;

            case self::ADD_SERVICES_IDS['ADD_EXPERT_PROOFREADING'] : 
                return 59;
                break;

            case self::SERVICES_IDS['DISCOUNT'] : 
                return 100;
                break;

            default : 
                return 0;
                break;
        }
    }

    /**
     * @param {Object.<string, Service>} servicesById Base services
     * @returns {number} Base cost
     */
    public function getBaseCost( $servicesById ) {
        return $this->normalizePrice(
            $servicesById[ self::SERVICES_IDS['WRITING_PAGES'] ]['cost'] +
            $servicesById[ self::SERVICES_IDS['WRITING_SLIDES'] ]['cost'] +
            $servicesById[ self::SERVICES_IDS['WRITING_CHARTS'] ]['cost']
        );
    }

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

    /**
     * @param {Object.<string, Service>} servicesById Base+secondary services
     * @returns {number} Secondary cost
     */
    public function getSecondaryCost( $servicesById ) {
        return $this->normalizePrice(
            $servicesById[ self::SERVICES_IDS['USED_SOURCES'] ]['cost'] +
            $servicesById[ self::SERVICES_IDS['PROVIDE_ME_SAMPLES'] ]['cost'] +
            $servicesById[ self::SERVICES_IDS['PROGRESSIVE_DELIVERY'] ]['cost'] +
            $servicesById[ self::SERVICES_IDS['CHOOSE_WRITER'] ]['cost'] +
            $servicesById[ self::SERVICES_IDS['COMPLEX_ASSIGNMENT'] ]['cost'] +
            $servicesById[ self::SERVICES_IDS['EXPERT_PROOFREADING'] ]['cost'] + 
            $servicesById[ self::SERVICES_IDS['VIP_SUPPORT'] ]['cost'] + 
            $servicesById[ self::SERVICES_IDS['PLAGIARISM_REPORT'] ]['cost'] + 
            $servicesById[ self::SERVICES_IDS['DRAFT_OUTLINE'] ]['cost']
        );
    }

    /**
     * @param {Object.<string, Service>} servicesById any set of services
     * @returns {number} Total cost
     */
    public function getTotalCost( $servicesById ) {
        $totalCost = 0;

        foreach ( $servicesById as $serviceId => $service ) {
            $totalCost = $this->normalizePrice($totalCost + $service['cost']);
        }

        return $totalCost;
    }

    /**
     * @param {Object.<string, Service>} servicesById any set of services
     * @returns {number} Writer cost
     */
    public function getWriterTotalCost( $servicesById ) {
        $totalCost = 0;

        foreach ( $servicesById as $serviceId => $service ) {
            $totalCost = $this->normalizePrice($totalCost + $service['writerCost'] );
        }

        return $totalCost;
    }

    /**
     * @param {Object.<string, Service>} servicesById any set of services
     * @returns {number} Editor cost
     */
    public function getEditorTotalCost( $servicesById ) {
        $totalCost = 0;

        foreach ( $servicesById as $serviceId => $service ) {
            $totalCost = $this->normalizePrice($totalCost + $service['editorCost'] - $service['winbackReduction'] );
        }

        return $totalCost;
    }

    /**
     * @param {Object.<string, Service>} servicesById Base+secondary services
     * @returns {number} Winback coupons reduction
     */
    public function getCouponsReduction( $servicesById ) {
        return $this->normalizePrice(
            $this->getBaseCouponsReduction( $servicesById ) +
            $servicesById[ self::SERVICES_IDS['PROVIDE_ME_SAMPLES'] ]['winbackReduction'] +
            $servicesById[ self::SERVICES_IDS['PROGRESSIVE_DELIVERY'] ]['winbackReduction']  +
            $servicesById[ self::SERVICES_IDS['USED_SOURCES'] ]['winbackReduction']  +
            $servicesById[ self::SERVICES_IDS['CHOOSE_WRITER'] ]['winbackReduction'] +
            $servicesById[ self::SERVICES_IDS['COMPLEX_ASSIGNMENT'] ]['winbackReduction'] +
            $servicesById[ self::SERVICES_IDS['EXPERT_PROOFREADING'] ]['winbackReduction'] + 
            $servicesById[ self::SERVICES_IDS['VIP_SUPPORT'] ]['winbackReduction'] + 
            $servicesById[ self::SERVICES_IDS['PLAGIARISM_REPORT'] ]['winbackReduction'] + 
            $servicesById[ self::SERVICES_IDS['DRAFT_OUTLINE'] ]['winbackReduction']
        );
    }

    /**
     * @param {Object.<string, Service>} servicesById services
     * @returns {number} Virtual services cost cost
     */
    public function getAdditionalServicesRawCost( $servicesById ) {
        $result = 0;

        foreach ( self::ADD_SERVICES_IDS as $p ) {
            $result = $this->normalizePrice($result + $servicesById[self::ADD_SERVICES_IDS[$p]]['rawCost'] );
        }

        return $result;
    }

    /**
     * Returns complete set of services for order
     * @param {OrderInfo} order
     * @returns {Object.<string, Service>} servicesById
     */
    public function calculateServices( $order ) {
        $order = $this->orderWithDefaults( $order );
        $baseServices = $this->calculateBaseServices($order);
        $servicesById = $baseServices;

        $baseCost = $this->getBaseCost( $servicesById );
        $baseCouponsReduction = $this->getBaseCouponsReduction( $servicesById );
        
        $secondaryServices = $this->calculateSecondaryServices($order, $baseCost, $baseCouponsReduction);
        $servicesById = $secondaryServices + $servicesById;
        $secondaryCost = $this->getSecondaryCost($servicesById);
        $rawCost = $this->normalizePrice($baseCost + $secondaryCost);
        $couponsReduction = $this->getCouponsReduction($servicesById);

        $discountServices = $this->calculateDiscountServices($order, $rawCost, $couponsReduction);
        $servicesById = $servicesById + $discountServices;

        return $this->formatServicesById($servicesById);
    }

    /**
     * Formats calculator's output for the end user
     * @param {Object.<string, Service>} servicesById
     * @returns {Object.<string, Service>} Services map
     */
    public function formatServicesById( $servicesById ) {
        foreach( $servicesById as $serviceId => $service ) {
            $isFormatted = (bool) ArrayUtils::get($service, 'isFormatted');
            if ( $isFormatted ) {
                continue;
            } else {
                $service['isFormatted'] = true; 
            }

            $service['winbackReduction'] = $service['winbackReduction'] || 0;
            $service['title'] = $this->getServiceTitle( $service, $serviceId);
            $service['free'] = $service['cost'] == $service['winbackReduction'] && $service['cost'] > 0;
            $service['type_id'] = $serviceId;
            $service['priority'] = $this->getServicePriority( $serviceId );
            $service['active'] = $service['cost'] != 0 || $service['winbackReduction'] != 0;
        
            $servicesById[$serviceId] = $service;
        }

        return $servicesById;
    }

    /**
     * Basic orderform calculator
     * @param {OrderInfo} order
     * @returns {CostInfo}
     */
    public function calculate( $order ) {
        $order = $this->orderWithDefaults( $order );
        $servicesById = $this->calculateServices( $order );
        $output = array( 
            'servicesById' => $servicesById 
        );

        $output['baseCost'] = $this->getBaseCost( $servicesById );
        $output['secondaryCost'] = $this->getSecondaryCost( $servicesById );
        $output['rawCost'] = $this->normalizePrice($output['baseCost'] + $output['secondaryCost']);
        $output['couponsReduction'] = $this->getCouponsReduction( $servicesById);
        $output['totalCost'] = $this->getTotalCost( $servicesById );
        $output['writerCost'] = $this->getWriterTotalCost( $servicesById );
        $output['editorCost'] = $this->getEditorTotalCost( $servicesById );

        return $output;
    }

    public function getAddServiceOrderInfoPatch($addServicesInfo, $type_id) {
        switch($type_id) {
            case self::ADD_SERVICES_IDS['ADD_SHORTEN_DEADLINE']: 
                return [
                    'tariffHrs' => $addServicesInfo['shortenDeadlineHrs'],
                    'tariffPricePerPage' => $addServicesInfo['shortenDeadlinePricePerPage'],
                ];
                break;

            case self::ADD_SERVICES_IDS['ADD_WRITING_PAGES']: 
                return [
                    'pages' => $addServicesInfo['newPagesQuantity']
                ];
                break;

            case self::ADD_SERVICES_IDS['ADD_WRITING_SLIDES']: 
                return [
                    'slides' => $addServicesInfo['newSlidesQuantity']
                ];
                break;

            case self::ADD_SERVICES_IDS['ADD_WRITING_CHARTS']: 
                return [
                    'charts' => $addServicesInfo['newChartsQuantity']
                ];
                break;

            case self::ADD_SERVICES_IDS['ADD_USED_SOURCES']: 
                return [
                    'getUsedSourcesOn' => $addServicesInfo['getUsedSourcesOn']
                ];
                break;

            case self::ADD_SERVICES_IDS['ADD_PROVIDE_ME_SAMPLES']: 
                return [
                    'getSamplesOn' => $addServicesInfo['getSamplesOn'],
                ];
                break;

            case self::ADD_SERVICES_IDS['ADD_EXPERT_PROOFREADING']: 
                return [
                    'expertProofreading' => $addServicesInfo['expertProofreading'],
                ];
                break;

            case self::ADD_SERVICES_IDS['ADD_VIP_SUPPORT']: 
                return [
                    'vipSupport' => $addServicesInfo['vipSupport'],
                ];
                break;

            case self::ADD_SERVICES_IDS['ADD_DRAFT_OUTLINE']: 
                return [
                    'draftOutline' => $addServicesInfo['draftOutline'],
                ];
                break;

            case self::ADD_SERVICES_IDS['ADD_PLAGIARISM_REPORT']: 
                return [
                    'plagiarismReport' => $addServicesInfo['plagiarismReport'],
                ];
                break;

            case self::ADD_SERVICES_IDS['ADD_PROGRESSIVE_DELIVERY']: 
                return [
                    'getProgressiveDeliveryOn' => $addServicesInfo['getProgressiveDeliveryOn']
                ];
                break;

            case self::ADD_SERVICES_IDS['ADD_BOOST_CATEGORY']: 
                return [
                    'writerCategoryId' => $addServicesInfo['boostWriterCategoryId'],
                    'writerPercent' => $addServicesInfo['boostWriterPercent'],
                ];
                break;
        }
    }

    public function calculateAddService($addServicesInfo, $mutableOrder, $type_id) {
        $patch = $this->getAddServiceOrderInfoPatch($addServicesInfo, $type_id);
        $originalServices = $this->calculateServices( $mutableOrder );
        $mutableOrder = array_merge($mutableOrder, $patch);

        $nextServices = $this->calculateServices( $mutableOrder );
        $originalRawCost = $this->normalizePrice( $this->getBaseCost($originalServices) +  $this->getSecondaryCost($originalServices));
        $nextRawCost = $this->normalizePrice( $this->getBaseCost($nextServices) +  $this->getSecondaryCost($nextServices));
        $originalCost = $this->normalizePrice( $this->getTotalCost($originalServices));
        $nextCost = $this->normalizePrice( $this->getTotalCost($nextServices));
        $servicesDiff = $this->subtractServices($nextServices, $originalServices, $type_id);

        $result = array(
            'rawCost' => $this->normalizePrice($nextRawCost - $originalRawCost),
            'cost' => $this->normalizePrice($nextCost - $originalCost),
            'winbackQuantity' => 0,
            'winbackReduction' => 0,
            'underlyingServicesById' => $this->formatServicesById($servicesDiff)
        );

        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
     */
    public function subtractServices($nextServices, $prevServices, $causedByServiceId) {
        $finalServices = array();

        foreach( $nextServices as $serviceId => $nextService ) {
            $finalService = [];

            if ( ArrayUtils::has($prevServices, $serviceId) ) {
                $prevService = ArrayUtils::get($prevServices, $serviceId);

                $finalService['cost'] = $this->normalizePrice( $nextService['cost'] - $prevService['cost'] );
                $finalService['quantity'] = $nextService['quantity'] - $prevService['quantity'];
                $finalService['winbackQuantity'] = $nextService['winbackQuantity'] - $prevService['winbackQuantity'];
                $finalService['winbackReduction'] = $this->normalizePrice( $nextService['winbackReduction'] - $prevService['winbackReduction'] );
            
                // Useless metrics, just copy the later values (for consistency) (use $service['operations)
                // Todo: remove
                
                if (ArrayUtils::has($nextService, 'price')) { 
                    $finalService['price'] = $nextService['price']; 
                }
                if (ArrayUtils::has($nextService, 'itemPrice')) { 
                    $finalService['itemPrice'] = $nextService['itemPrice']; 
                }
                if (ArrayUtils::has($nextService, 'pricePercent')) { 
                    $finalService['pricePercent'] = $nextService['pricePercent']; 
                }
                if (ArrayUtils::has($nextService, 'disabledReason')) { 
                    $finalService['disabledReason'] = $nextService['disabledReason']; 
                }
                if (ArrayUtils::has($nextService, 'forcedReason')) { 
                    $finalService['forcedReason'] = $nextService['forcedReason']; 
                }
                
                if( !ArrayUtils::has($finalService, 'operations') ) {
                    $finalService['operations'] = array();
                }

                if (
                    ArrayUtils::get($nextService, 'quantity') != ArrayUtils::get($prevService, 'quantity') ||
                    ArrayUtils::get($nextService, 'price') != ArrayUtils::get($prevService, 'price') ||
                    ArrayUtils::get($nextService, 'itemPrice') != ArrayUtils::get($prevService, 'itemPrice') ||
                    ArrayUtils::get($nextService, 'pricePercent') != ArrayUtils::get($prevService, 'pricePercent')  
                ) {
                    // Quantitative information is differs, calculate operations
                    if (
                        ArrayUtils::get($nextService, 'price') == ArrayUtils::get($prevService, 'price') ||
                        ArrayUtils::get($nextService, 'itemPrice') == ArrayUtils::get($prevService, 'itemPrice') ||
                        ArrayUtils::get($nextService, 'pricePercent') == ArrayUtils::get($prevService, 'pricePercent')  
                    ) {
                        // Only quantity is different
                        $finalService['operations'][] = array(
                            'sign' => 1,
                            'quantity' => $nextService['quantity'] - $prevService['quantity'],
                            'price' => $nextService['price'],
                            'itemPrice' => $nextService['itemPrice'],
                            'pricePercent' => $nextService['pricePercent'],
                            'causedByServiceId' => $causedByServiceId,
                        );

                    } else {
                        $finalService['operations'][] = array(
                            [
                                '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;
    }

    public function mergeServices($aServices, $bServices) {
        $mergedServicesById = [];

        foreach($aServices as $serviceId) {
            $aService = ArrayUtils::get($aServices, $serviceId);
            $bService = ArrayUtils::get($bServices, $serviceId);
            $mergedService = $aService;

            if ( ArrayUtils::has($aService, 'operations') ) {
                $mergedService['operations'] = ArrayUtils::get($aService, 'operations');
            }

            if ($bService) {
                $mergedService['quantity'] = $mergedService['quantity'] + $bService['quantity'];
                $mergedService['cost'] = $this->normalizePrice($mergedService['cost'] + $bService['cost']);
                $mergedService['winbackQuantity'] = $mergedService['winbackQuantity'] + $bService['winbackQuantity'];
                $mergedService['winbackReduction'] = $this->normalizePrice($mergedService['winbackReduction'] + $bService['winbackReduction'] );
        
                // Useless metrics, just copy the later values (for consistency) (use $service['operations)
                if(ArrayUtils::has($bService, 'price')) $mergedService['price'] = $bService['price'];
                if(ArrayUtils::has($bService, 'itemPrice')) $mergedService['itemPrice'] = $bService['itemPrice'];
                if(ArrayUtils::has($bService, 'pricePercent')) $mergedService['pricePercent'] = $bService['pricePercent'];
                if(ArrayUtils::has($bService, 'disabledReason')) $mergedService['disabledReason'] = $bService['disabledReason'];
                if(ArrayUtils::has($bService, 'forcedReason')) $mergedService['forcedReason'] = $bService['forcedReason'];

                if( ArrayUtils::has($bService, 'operations') ) {
                    if ( !ArrayUtils::has($mergedService, 'operations') ) {
                        $mergedService['operations'] = array();
                    }
                }

                $mergedService['operations'] = ArrayUtils::get($bService, 'operations');
            }
            
            $mergedServicesById[$serviceId] = $mergedService;
        }

        return $mergedServicesById;
    }

    /**
     * @property {{
     *   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}
     */
    public function calculateAddServices( $addServicesInfo, $originalOrder ) {
        $originalOrder = $this->orderWithDefaults( $originalOrder );
        $addServicesInfo = $this->addServicesInfoWithDefaults($addServicesInfo, $originalOrder);
        
        $mutableOrder = $originalOrder;

        $addServicesById = array();
        for ($i = 0; $i < count(self::ADD_SERVICES_ORDER); $i += 1) {
            $type_id = self::ADD_SERVICES_ORDER[$i];
            $addService = $this->calculateAddService($addServicesInfo, $mutableOrder, $type_id);
            $addServicesById[$type_id] = $addService;
        }

        return $this->formatServicesById( $addServicesById );
    }

    public function extrudeServices($addServicesById, $order) {
        $order = $this->orderWithDefaults($order);

        // Now the craziness begins
        // We merge all the underlying services except those, which belong to UNMERGEABLE_ADD_SERVICES
        $mergedServicesById = $this->calculateServices($this->orderWithDefaults());
        $extrudedServicesById = [];

        for ($i = 0; $i < count(self::ADD_SERVICES_ORDER); $i += 1) {
            $type_id = self::ADD_SERVICES_ORDER[$i];
            if (self::UNMERGEABLE_ADD_SERVICES.strpos($type_id) == -1) {
                $addService = $addServicesById[$type_id];
                $mergedServicesById = $this->mergeServices($mergedServicesById, $addService['underlyingServicesById']);
            } else {
                $extrudedServicesById[$type_id] = $addServicesById[$type_id];
            }
        }

        $extrudedServicesById = array_merge($extrudedServicesById, $mergedServicesById);

        $rawCost = $this->getAdditionalServicesRawCost($addServicesById);

        $couponsReduction = 0;
        foreach ($mergedServicesById as $k) {
            $couponsReduction += $mergedServicesById[$k]['winbackReduction'] || 0;
        }
            
        $discountServices = $this->calculateDiscountServices($order, $rawCost, $couponsReduction);
        $extrudedServicesById = array_merge($extrudedServicesById, $discountServices);

        return $this->formatServicesById($extrudedServicesById);
    }

    public function getLastCalcServForAddServices( $addServicesById ) {
        $lastAddServiceId = self::ADD_SERVICES_ORDER[ count(self::ADD_SERVICES_ORDER) - 1];
        return $addServicesById[ $lastAddServiceId ]['underlyingServicesById'];
    }
}

