<?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\CustomVariables;

use Piwik\Config;
use Piwik\DataAccess\LogAggregator;
use Piwik\DataArray;
use Piwik\DataTable;
use Piwik\Metrics;
use Piwik\Tracker\GoalManager;
use Piwik\Tracker;

require_once PIWIK_INCLUDE_PATH . '/libs/PiwikTracker/PiwikTracker.php';

class Archiver extends \Piwik\Plugin\Archiver
{
    const LABEL_CUSTOM_VALUE_NOT_DEFINED = "Value not defined";
    const CUSTOM_VARIABLE_RECORD_NAME = 'CustomVariables_valueByName';

    // Ecommerce reports use custom variables.
    // We specifically set the limits high to get accurate Ecommerce reports
    const MAX_ROWS_WHEN_ECOMMERCE = 50000;

    /**
     * @var DataArray
     */
    protected $dataArray;
    protected $maximumRowsInDataTableLevelZero;
    protected $maximumRowsInSubDataTable;
    protected $newEmptyRow;

    private $metadata = array();
    private $metadataFlat = array();

    function __construct($processor)
    {
        parent::__construct($processor);

        if ($processor->getParams()->getSite()->isEcommerceEnabled()) {
            $this->maximumRowsInDataTableLevelZero = self::MAX_ROWS_WHEN_ECOMMERCE;
            $this->maximumRowsInSubDataTable = self::MAX_ROWS_WHEN_ECOMMERCE;
        } else {
            $this->maximumRowsInDataTableLevelZero = Config::getInstance()->General['datatable_archiving_maximum_rows_custom_variables'];
            $this->maximumRowsInSubDataTable = Config::getInstance()->General['datatable_archiving_maximum_rows_subtable_custom_variables'];
        }
    }

    public function aggregateMultipleReports()
    {
        $columnsAggregationOperation = array('slots' => 'uniquearraymerge');

        $this->getProcessor()->aggregateDataTableRecords(
            self::CUSTOM_VARIABLE_RECORD_NAME,
            $this->maximumRowsInDataTableLevelZero,
            $this->maximumRowsInSubDataTable,
            $columnToSort = Metrics::INDEX_NB_VISITS,
            $columnsAggregationOperation,
            $columnsToRenameAfterAggregation = null,
            $countRowsRecursive = array());
    }

    public function aggregateDayReport()
    {
        $this->dataArray = new DataArray();

        $maxCustomVariables = CustomVariables::getNumUsableCustomVariables();
        for ($i = 1; $i <= $maxCustomVariables; $i++) {
            $this->aggregateCustomVariable($i);
        }

        $this->removeVisitsMetricsFromActionsAggregate();
        $this->dataArray->enrichMetricsWithConversions();
        $table = $this->dataArray->asDataTable();

        foreach ($table->getRows() as $row) {
            $label = $row->getColumn('label');
            if (!empty($this->metadata[$label])) {
                foreach ($this->metadata[$label] as $name => $value) {
                    $row->addMetadata($name, $value);
                }
            }
        }

        $blob = $table->getSerialized(
            $this->maximumRowsInDataTableLevelZero, $this->maximumRowsInSubDataTable,
            $columnToSort = Metrics::INDEX_NB_VISITS
        );

        $this->getProcessor()->insertBlobRecord(self::CUSTOM_VARIABLE_RECORD_NAME, $blob);
    }

    protected function aggregateCustomVariable($slot)
    {
        $keyField = "custom_var_k" . $slot;
        $valueField = "custom_var_v" . $slot;
        $where = "%s.$keyField != ''";
        $dimensions = array($keyField, $valueField);

        $query = $this->getLogAggregator()->queryVisitsByDimension($dimensions, $where);
        $this->aggregateFromVisits($query, $keyField, $valueField);

        // IF we query Custom Variables scope "page" either: Product SKU, Product Name,
        // then we also query the "Product page view" price which was possibly recorded.
        $additionalSelects = false;

        if (in_array($slot, array(\PiwikTracker::CVAR_INDEX_ECOMMERCE_ITEM_SKU, \PiwikTracker::CVAR_INDEX_ECOMMERCE_ITEM_NAME, \PiwikTracker::CVAR_INDEX_ECOMMERCE_ITEM_CATEGORY))) {
            $additionalSelects = array($this->getSelectAveragePrice());
        }
        $query = $this->getLogAggregator()->queryActionsByDimension($dimensions, $where, $additionalSelects);
        $this->aggregateFromActions($query, $keyField, $valueField);

        $query = $this->getLogAggregator()->queryConversionsByDimension($dimensions, $where);
        $this->aggregateFromConversions($query, $keyField, $valueField);
    }

    protected function getSelectAveragePrice()
    {
        $field = "custom_var_v" . \PiwikTracker::CVAR_INDEX_ECOMMERCE_ITEM_PRICE;
        return LogAggregator::getSqlRevenue("AVG(log_link_visit_action." . $field . ")") . " as `" . Metrics::INDEX_ECOMMERCE_ITEM_PRICE_VIEWED . "`";
    }

    protected function aggregateFromVisits($query, $keyField, $valueField)
    {
        while ($row = $query->fetch()) {
            $key = $row[$keyField];
            $value = $this->cleanCustomVarValue($row[$valueField]);

            $this->addMetadata($keyField, $key, Model::SCOPE_VISIT);

            $this->dataArray->sumMetricsVisits($key, $row);
            $this->dataArray->sumMetricsVisitsPivot($key, $value, $row);
        }
    }

    protected function cleanCustomVarValue($value)
    {
        if (strlen($value)) {
            return $value;
        }
        return self::LABEL_CUSTOM_VALUE_NOT_DEFINED;
    }

    protected function aggregateFromActions($query, $keyField, $valueField)
    {
        while ($row = $query->fetch()) {
            $key = $row[$keyField];
            $value = $this->cleanCustomVarValue($row[$valueField]);

            $this->addMetadata($keyField, $key, Model::SCOPE_PAGE);

            $alreadyAggregated = $this->aggregateEcommerceCategories($key, $value, $row);
            if (!$alreadyAggregated) {
                $this->aggregateActionByKeyAndValue($key, $value, $row);
                $this->dataArray->sumMetricsActions($key, $row);
            }
        }
    }

    private function addMetadata($keyField, $label, $scope)
    {
        $index = (int) str_replace('custom_var_k', '', $keyField);

        if (!array_key_exists($label, $this->metadata)) {
            $this->metadata[$label] = array('slots' => array());
        }

        $uniqueId = $label . 'scope' . $scope . 'index' . $index;

        if (!isset($this->metadataFlat[$uniqueId])) {
            $this->metadata[$label]['slots'][] = array('scope' => $scope, 'index' => $index);
            $this->metadataFlat[$uniqueId] = true;
        }
    }

    /**
     * @param string $key
     * @param string $value
     * @param $row
     * @return bool True if the $row metrics were already added to the ->metrics
     */
    protected function aggregateEcommerceCategories($key, $value, $row)
    {
        $ecommerceCategoriesAggregated = false;
        if ($key == '_pkc'
            && $value[0] == '[' && $value[1] == '"'
        ) {
            // In case categories were truncated, try closing the array
            if (substr($value, -2) != '"]') {
                $value .= '"]';
            }
            $decoded = json_decode($value);
            if (is_array($decoded)) {
                $count = 0;
                foreach ($decoded as $category) {
                    if (empty($category)
                        || $count >= GoalManager::MAXIMUM_PRODUCT_CATEGORIES
                    ) {
                        continue;
                    }
                    $this->aggregateActionByKeyAndValue($key, $category, $row);
                    $ecommerceCategoriesAggregated = true;
                    $count++;
                }
            }
        }
        return $ecommerceCategoriesAggregated;
    }

    protected function aggregateActionByKeyAndValue($key, $value, $row)
    {
        $this->dataArray->sumMetricsActionsPivot($key, $value, $row);

        if ($this->isReservedKey($key)) {
            // Price tracking on Ecommerce product/category pages:
            // the average is returned from the SQL query so the price is not "summed" like other metrics
            $index = Metrics::INDEX_ECOMMERCE_ITEM_PRICE_VIEWED;
            if (!empty($row[$index])) {
                $this->dataArray->setRowColumnPivot($key, $value, $index, (float)$row[$index]);
            }
        }
    }

    protected static function isReservedKey($key)
    {
        return in_array($key, API::getReservedCustomVariableKeys());
    }

    protected function aggregateFromConversions($query, $keyField, $valueField)
    {
        if ($query === false) {
            return;
        }
        while ($row = $query->fetch()) {
            $key = $row[$keyField];

            $value = $this->cleanCustomVarValue($row[$valueField]);
            $this->dataArray->sumMetricsGoals($key, $row);
            $this->dataArray->sumMetricsGoalsPivot($key, $value, $row);
        }
    }

    /**
     * Delete Visit, Unique Visitor and Users metric from 'page' scope custom variables.
     *
     * - Custom variables of 'visit' scope: it is expected that these ones have the "visit" column set.
     * - Custom variables of 'page' scope: we cannot process "Visits" count for these.
     *   Why?
     *     "Actions" column is processed with a SELECT count(*).
     *     A same visit can set the same custom variable of 'page' scope multiple times.
     *     We cannot sum the values of count(*) as it would be incorrect.
     *     The way we could process "Visits" Metric for 'page' scope variable is to issue a count(Distinct *) or so,
     *     but it is no implemented yet (this would likely be very slow for high traffic sites).
     *
     */
    protected function removeVisitsMetricsFromActionsAggregate()
    {
        $dataArray = & $this->dataArray->getDataArray();
        foreach ($dataArray as $key => &$row) {
            if (!self::isReservedKey($key)
                && DataArray::isRowActions($row)
            ) {
                unset($row[Metrics::INDEX_NB_UNIQ_VISITORS]);
                unset($row[Metrics::INDEX_NB_VISITS]);
                unset($row[Metrics::INDEX_NB_USERS]);
            }
        }
    }

}
