<?php
/**
 * Piwik - free/libre analytics platform
 *
 * @link https://matomo.org
 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
 *
 */

namespace Piwik\Plugins\Goals;

use Piwik\DataAccess\LogAggregator;
use Piwik\DataArray;
use Piwik\DataTable;
use Piwik\Metrics;
use Piwik\Tracker\GoalManager;
use Piwik\Plugins\VisitFrequency\API as VisitFrequencyAPI;

class Archiver extends \Piwik\Plugin\Archiver
{
    const VISITS_UNTIL_RECORD_NAME = 'visits_until_conv';
    const DAYS_UNTIL_CONV_RECORD_NAME = 'days_until_conv';
    const ITEMS_SKU_RECORD_NAME = 'Goals_ItemsSku';
    const ITEMS_NAME_RECORD_NAME = 'Goals_ItemsName';
    const ITEMS_CATEGORY_RECORD_NAME = 'Goals_ItemsCategory';
    const SKU_FIELD = 'idaction_sku';
    const NAME_FIELD = 'idaction_name';
    const CATEGORY_FIELD = 'idaction_category';
    const CATEGORY2_FIELD = 'idaction_category2';
    const CATEGORY3_FIELD = 'idaction_category3';
    const CATEGORY4_FIELD = 'idaction_category4';
    const CATEGORY5_FIELD = 'idaction_category5';
    const NO_LABEL = ':';
    const LOG_CONVERSION_TABLE = 'log_conversion';
    const VISITS_COUNT_FIELD = 'visitor_count_visits';
    const DAYS_SINCE_FIRST_VISIT_FIELD = 'visitor_days_since_first';

    const NEW_VISIT_SEGMENT = 'visitorType%3D%3Dnew'; // visitorType==new

    /**
     * This array stores the ranges to use when displaying the 'visits to conversion' report
     */
    public static $visitCountRanges = array(
        array(1, 1),
        array(2, 2),
        array(3, 3),
        array(4, 4),
        array(5, 5),
        array(6, 6),
        array(7, 7),
        array(8, 8),
        array(9, 14),
        array(15, 25),
        array(26, 50),
        array(51, 100),
        array(100)
    );
    /**
     * This array stores the ranges to use when displaying the 'days to conversion' report
     */
    public static $daysToConvRanges = array(
        array(0, 0),
        array(1, 1),
        array(2, 2),
        array(3, 3),
        array(4, 4),
        array(5, 5),
        array(6, 6),
        array(7, 7),
        array(8, 14),
        array(15, 30),
        array(31, 60),
        array(61, 120),
        array(121, 364),
        array(364)
    );
    protected $dimensionRecord = array(
        self::SKU_FIELD      => self::ITEMS_SKU_RECORD_NAME,
        self::NAME_FIELD     => self::ITEMS_NAME_RECORD_NAME,
        self::CATEGORY_FIELD => self::ITEMS_CATEGORY_RECORD_NAME
    );

    /**
     * Array containing one DataArray for each Ecommerce items dimension (name/sku/category abandoned carts and orders)
     * @var array
     */
    protected $itemReports = array();

    public function aggregateDayReport()
    {
        $this->aggregateGeneralGoalMetrics();
        $this->aggregateEcommerceItems();

        $this->getProcessor()->processDependentArchive('Goals', API::NEW_VISIT_SEGMENT);
        $this->getProcessor()->processDependentArchive('Goals', VisitFrequencyAPI::RETURNING_VISITOR_SEGMENT);
    }

    protected function aggregateGeneralGoalMetrics()
    {
        $prefixes = array(
            self::VISITS_UNTIL_RECORD_NAME    => 'vcv',
            self::DAYS_UNTIL_CONV_RECORD_NAME => 'vdsf',
        );

        $selects = array();
        $selects = array_merge($selects, LogAggregator::getSelectsFromRangedColumn(
            self::VISITS_COUNT_FIELD, self::$visitCountRanges, self::LOG_CONVERSION_TABLE, $prefixes[self::VISITS_UNTIL_RECORD_NAME]
        ));
        $selects = array_merge($selects, LogAggregator::getSelectsFromRangedColumn(
            self::DAYS_SINCE_FIRST_VISIT_FIELD, self::$daysToConvRanges, self::LOG_CONVERSION_TABLE, $prefixes[self::DAYS_UNTIL_CONV_RECORD_NAME]
        ));

        $query = $this->getLogAggregator()->queryConversionsByDimension(array(), false, $selects);
        if ($query === false) {
            return;
        }

        $totalConversions = $totalRevenue = 0;
        $goals = new DataArray();
        $visitsToConversions = $daysToConversions = array();

        $conversionMetrics = $this->getLogAggregator()->getConversionsMetricFields();
        while ($row = $query->fetch()) {
            $idGoal = $row['idgoal'];
            unset($row['idgoal']);
            unset($row['label']);

            $values = array();
            foreach ($conversionMetrics as $field => $statement) {
                $values[$field] = $row[$field];
            }
            $goals->sumMetrics($idGoal, $values);

            if (empty($visitsToConversions[$idGoal])) {
                $visitsToConversions[$idGoal] = new DataTable();
            }
            $array = LogAggregator::makeArrayOneColumn($row, Metrics::INDEX_NB_CONVERSIONS, $prefixes[self::VISITS_UNTIL_RECORD_NAME]);
            $visitsToConversions[$idGoal]->addDataTable(DataTable::makeFromIndexedArray($array));

            if (empty($daysToConversions[$idGoal])) {
                $daysToConversions[$idGoal] = new DataTable();
            }
            $array = LogAggregator::makeArrayOneColumn($row, Metrics::INDEX_NB_CONVERSIONS, $prefixes[self::DAYS_UNTIL_CONV_RECORD_NAME]);
            $daysToConversions[$idGoal]->addDataTable(DataTable::makeFromIndexedArray($array));

            // We don't want to sum Abandoned cart metrics in the overall revenue/conversions/converted visits
            // since it is a "negative conversion"
            if ($idGoal != GoalManager::IDGOAL_CART) {
                $totalConversions += $row[Metrics::INDEX_GOAL_NB_CONVERSIONS];
                $totalRevenue += $row[Metrics::INDEX_GOAL_REVENUE];
            }
        }

        // Stats by goal, for all visitors
        $numericRecords = $this->getConversionsNumericMetrics($goals);
        $this->getProcessor()->insertNumericRecords($numericRecords);

        $this->insertReports(self::VISITS_UNTIL_RECORD_NAME, $visitsToConversions);
        $this->insertReports(self::DAYS_UNTIL_CONV_RECORD_NAME, $daysToConversions);

        // Stats for all goals
        $nbConvertedVisits = $this->getProcessor()->getNumberOfVisitsConverted();
        $metrics = array(
            self::getRecordName('nb_conversions')      => $totalConversions,
            self::getRecordName('nb_visits_converted') => $nbConvertedVisits,
            self::getRecordName('revenue')             => $totalRevenue,
        );
        $this->getProcessor()->insertNumericRecords($metrics);
    }

    protected function getConversionsNumericMetrics(DataArray $goals)
    {
        $numericRecords = array();
        $goals = $goals->getDataArray();
        foreach ($goals as $idGoal => $array) {
            foreach ($array as $metricId => $value) {
                $metricName = Metrics::$mappingFromIdToNameGoal[$metricId];
                $recordName = self::getRecordName($metricName, $idGoal);
                $numericRecords[$recordName] = $value;
            }
        }
        return $numericRecords;
    }

    /**
     * @param string $recordName 'nb_conversions'
     * @param int|bool $idGoal idGoal to return the metrics for, or false to return overall
     * @return string Archive record name
     */
    public static function getRecordName($recordName, $idGoal = false)
    {
        $idGoalStr = '';
        if ($idGoal !== false) {
            $idGoalStr = $idGoal . "_";
        }
        return 'Goal_' . $idGoalStr . $recordName;
    }

    protected function insertReports($recordName, $visitsToConversions)
    {
        foreach ($visitsToConversions as $idGoal => $table) {
            $record = self::getRecordName($recordName, $idGoal);
            $this->getProcessor()->insertBlobRecord($record, $table->getSerialized());
        }
        $overviewTable = $this->getOverviewFromGoalTables($visitsToConversions);
        $this->getProcessor()->insertBlobRecord(self::getRecordName($recordName), $overviewTable->getSerialized());
    }

    protected function getOverviewFromGoalTables($tableByGoal)
    {
        $overview = new DataTable();
        foreach ($tableByGoal as $idGoal => $table) {
            if ($this->isStandardGoal($idGoal)) {
                $overview->addDataTable($table);
            }
        }
        return $overview;
    }

    protected function isStandardGoal($idGoal)
    {
        return !in_array($idGoal, $this->getEcommerceIdGoals());
    }

    protected function aggregateEcommerceItems()
    {
        $this->initItemReports();
        foreach ($this->getItemsDimensions() as $dimension) {
            $query = $this->getLogAggregator()->queryEcommerceItems($dimension);
            if ($query == false) {
                continue;
            }
            $this->aggregateFromEcommerceItems($query, $dimension);
        }
        $this->insertItemReports();
        return true;
    }

    protected function initItemReports()
    {
        foreach ($this->getEcommerceIdGoals() as $ecommerceType) {
            foreach ($this->dimensionRecord as $dimension => $record) {
                $this->itemReports[$dimension][$ecommerceType] = new DataArray();
            }
        }
    }

    protected function insertItemReports()
    {
        /** @var DataArray $array */
        foreach ($this->itemReports as $dimension => $itemAggregatesByType) {
            foreach ($itemAggregatesByType as $ecommerceType => $itemAggregate) {
                $recordName = $this->dimensionRecord[$dimension];
                if ($ecommerceType == GoalManager::IDGOAL_CART) {
                    $recordName = self::getItemRecordNameAbandonedCart($recordName);
                }
                $table = $itemAggregate->asDataTable();
                $this->getProcessor()->insertBlobRecord($recordName, $table->getSerialized());
            }
        }
    }

    protected function getItemsDimensions()
    {
        $dimensions = array_keys($this->dimensionRecord);
        foreach ($this->getItemExtraCategories() as $category) {
            $dimensions[] = $category;
        }
        return $dimensions;
    }

    protected function getItemExtraCategories()
    {
        return array(self::CATEGORY2_FIELD, self::CATEGORY3_FIELD, self::CATEGORY4_FIELD, self::CATEGORY5_FIELD);
    }

    protected function isItemExtraCategory($field)
    {
        return in_array($field, $this->getItemExtraCategories());
    }

    protected function aggregateFromEcommerceItems($query, $dimension)
    {
        while ($row = $query->fetch()) {
            $ecommerceType = $row['ecommerceType'];

            $label = $this->cleanupRowGetLabel($row, $dimension);
            if ($label === false) {
                continue;
            }

            // Aggregate extra categories in the Item categories array
            if ($this->isItemExtraCategory($dimension)) {
                $array = $this->itemReports[self::CATEGORY_FIELD][$ecommerceType];
            } else {
                $array = $this->itemReports[$dimension][$ecommerceType];
            }

            $this->roundColumnValues($row);
            $array->sumMetrics($label, $row);
        }
    }

    protected function cleanupRowGetLabel(&$row, $currentField)
    {
        $label = $row['label'];
        if (empty($label)) {
            // An empty additional category -> skip this iteration
            if ($this->isItemExtraCategory($currentField)) {
                return false;
            }
            $label = "Value not defined";
            // Product Name/Category not defined"
            if (\Piwik\Plugin\Manager::getInstance()->isPluginActivated('CustomVariables')) {
                $label = \Piwik\Plugins\CustomVariables\Archiver::LABEL_CUSTOM_VALUE_NOT_DEFINED;
            }
        }

        if ($row['ecommerceType'] == GoalManager::IDGOAL_CART) {
            // abandoned carts are the numner of visits with an abandoned cart
            $row[Metrics::INDEX_ECOMMERCE_ORDERS] = $row[Metrics::INDEX_NB_VISITS];
        }

        unset($row[Metrics::INDEX_NB_VISITS]);
        unset($row['label']);
        unset($row['labelIdAction']);
        unset($row['ecommerceType']);

        return $label;
    }

    protected function roundColumnValues(&$row)
    {
        $columnsToRound = array(
            Metrics::INDEX_ECOMMERCE_ITEM_REVENUE,
            Metrics::INDEX_ECOMMERCE_ITEM_QUANTITY,
            Metrics::INDEX_ECOMMERCE_ITEM_PRICE,
            Metrics::INDEX_ECOMMERCE_ITEM_PRICE_VIEWED,
        );
        foreach ($columnsToRound as $column) {
            if (isset($row[$column])
                && $row[$column] == round($row[$column])
            ) {
                $row[$column] = round($row[$column]);
            }
        }
    }

    protected function getEcommerceIdGoals()
    {
        return array(GoalManager::IDGOAL_CART, GoalManager::IDGOAL_ORDER);
    }

    public static function getItemRecordNameAbandonedCart($recordName)
    {
        return $recordName . '_Cart';
    }

    /**
     * @internal param $this->getProcessor()
     */
    public function aggregateMultipleReports()
    {
        /*
         * Archive Ecommerce Items
         */
        $dataTableToSum = $this->dimensionRecord;
        foreach ($this->dimensionRecord as $recordName) {
            $dataTableToSum[] = self::getItemRecordNameAbandonedCart($recordName);
        }
        $columnsAggregationOperation = null;

        $this->getProcessor()->aggregateDataTableRecords($dataTableToSum,
            $maximumRowsInDataTableLevelZero = null,
            $maximumRowsInSubDataTable = null,
            $columnToSortByBeforeTruncation = null,
            $columnsAggregationOperation,
            $columnsToRenameAfterAggregation = null,
            $countRowsRecursive = array());

        /*
         *  Archive General Goal metrics
         */
        $goalIdsToSum = GoalManager::getGoalIds($this->getProcessor()->getParams()->getSite()->getId());

        //Ecommerce
        $goalIdsToSum[] = GoalManager::IDGOAL_ORDER;
        $goalIdsToSum[] = GoalManager::IDGOAL_CART; //bug here if idgoal=1
        // Overall goal metrics
        $goalIdsToSum[] = false;

        $fieldsToSum = array();
        foreach ($goalIdsToSum as $goalId) {
            $metricsToSum = Goals::getGoalColumns($goalId);
            foreach ($metricsToSum as $metricName) {
                $fieldsToSum[] = self::getRecordName($metricName, $goalId);
            }
        }
        $this->getProcessor()->aggregateNumericMetrics($fieldsToSum);

        $columnsAggregationOperation = null;

        foreach ($goalIdsToSum as $goalId) {
            // sum up the visits to conversion data table & the days to conversion data table
            $this->getProcessor()->aggregateDataTableRecords(
                array(self::getRecordName(self::VISITS_UNTIL_RECORD_NAME, $goalId),
                      self::getRecordName(self::DAYS_UNTIL_CONV_RECORD_NAME, $goalId)),
                $maximumRowsInDataTableLevelZero = null,
                $maximumRowsInSubDataTable = null,
                $columnToSortByBeforeTruncation = null,
                $columnsAggregationOperation,
                $columnsToRenameAfterAggregation = null,
                $countRowsRecursive = array());
        }

        $columnsAggregationOperation = null;
        // sum up goal overview reports
        $this->getProcessor()->aggregateDataTableRecords(
                array(self::getRecordName(self::VISITS_UNTIL_RECORD_NAME),
                      self::getRecordName(self::DAYS_UNTIL_CONV_RECORD_NAME)),
                $maximumRowsInDataTableLevelZero = null,
                $maximumRowsInSubDataTable = null,
                $columnToSortByBeforeTruncation = null,
                $columnsAggregationOperation,
                $columnsToRenameAfterAggregation = null,
                $countRowsRecursive = array());

        $this->getProcessor()->processDependentArchive('Goals', API::NEW_VISIT_SEGMENT);
        $this->getProcessor()->processDependentArchive('Goals', VisitFrequencyAPI::RETURNING_VISITOR_SEGMENT);
    }
}
