<?php
/**
 * Piwik - Open source web analytics
 *
 * @link http://piwik.org
 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
 *
 * @category Piwik_Plugins
 * @package Actions
 */
namespace Piwik\Plugins\Actions;

use Exception;
use Piwik\Archive;

use Piwik\Common;
use Piwik\DataTable;

use Piwik\Date;
use Piwik\Metrics;
use Piwik\Piwik;
use Piwik\Plugins\CustomVariables\API as APICustomVariables;
use Piwik\Tracker\Action;
use Piwik\Tracker\ActionSiteSearch;
use Piwik\Tracker\PageUrl;

/**
 * The Actions API lets you request reports for all your Visitor Actions: Page URLs, Page titles (Piwik Events),
 * File Downloads and Clicks on external websites.
 *
 * For example, "getPageTitles" will return all your page titles along with standard <a href='http://piwik.org/docs/analytics-api/reference/#toc-metric-definitions' target='_blank'>Actions metrics</a> for each row.
 *
 * It is also possible to request data for a specific Page Title with "getPageTitle"
 * and setting the parameter pageName to the page title you wish to request.
 * Similarly, you can request metrics for a given Page URL via "getPageUrl", a Download file via "getDownload"
 * and an outlink via "getOutlink".
 *
 * Note: pageName, pageUrl, outlinkUrl, downloadUrl parameters must be URL encoded before you call the API.
 * @package Actions
 * @method static \Piwik\Plugins\Actions\API getInstance()
 */
class API extends \Piwik\Plugin\API
{
    /**
     * Returns the list of metrics (pages, downloads, outlinks)
     *
     * @param int $idSite
     * @param string $period
     * @param string $date
     * @param bool|string $segment
     * @param bool|array $columns
     * @return DataTable
     */
    public function get($idSite, $period, $date, $segment = false, $columns = false)
    {
        Piwik::checkUserHasViewAccess($idSite);
        $archive = Archive::build($idSite, $period, $date, $segment);

        $metrics = Archiver::$actionsAggregateMetrics;
        $metrics['Actions_avg_time_generation'] = 'avg_time_generation';

        // get requested columns
        $columns = Piwik::getArrayFromApiParameter($columns);
        if (!empty($columns)) {
            // get the columns that are available and requested
            $columns = array_intersect($columns, array_values($metrics));
            $columns = array_values($columns); // make sure indexes are right
            $nameReplace = array();
            foreach ($columns as $i => $column) {
                $fullColumn = array_search($column, $metrics);
                $columns[$i] = $fullColumn;
                $nameReplace[$fullColumn] = $column;
            }

            if (false !== ($avgGenerationTimeRequested = array_search('Actions_avg_time_generation', $columns))) {
                unset($columns[$avgGenerationTimeRequested]);
                $avgGenerationTimeRequested = true;
            }
        } else {
            // get all columns
            unset($metrics['Actions_avg_time_generation']);
            $columns = array_keys($metrics);
            $nameReplace = & $metrics;
            $avgGenerationTimeRequested = true;
        }

        if ($avgGenerationTimeRequested) {
            $tempColumns[] = Archiver::METRIC_SUM_TIME_RECORD_NAME;
            $tempColumns[] = Archiver::METRIC_HITS_TIMED_RECORD_NAME;
            $columns = array_merge($columns, $tempColumns);
            $columns = array_unique($columns);

            $nameReplace[Archiver::METRIC_SUM_TIME_RECORD_NAME] = 'sum_time_generation';
            $nameReplace[Archiver::METRIC_HITS_TIMED_RECORD_NAME] = 'nb_hits_with_time_generation';
        }

        $table = $archive->getDataTableFromNumeric($columns);

        // replace labels (remove Actions_)
        $table->filter('ReplaceColumnNames', array($nameReplace));

        // compute avg generation time
        if ($avgGenerationTimeRequested) {
            $table->filter('ColumnCallbackAddColumnQuotient', array('avg_time_generation', 'sum_time_generation', 'nb_hits_with_time_generation', 3));
            $table->deleteColumns(array('sum_time_generation', 'nb_hits_with_time_generation'));
        }

        return $table;
    }

    /**
     * @param int $idSite
     * @param string $period
     * @param Date $date
     * @param bool $segment
     * @param bool $expanded
     * @param bool|int $idSubtable
     * @param bool|int $depth
     *
     * @return DataTable|DataTable\Map
     */
    public function getPageUrls($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false,
                                $depth = false)
    {
        $dataTable = Archive::getDataTableFromArchive(
            'Actions_actions_url', $idSite, $period, $date, $segment, $expanded, $idSubtable, $depth);
        $this->filterPageDatatable($dataTable);
        $this->filterActionsDataTable($dataTable, $expanded);
        return $dataTable;
    }

    /**
     * @param int $idSite
     * @param string $period
     * @param Date $date
     * @param bool $segment
     * @param bool $expanded
     * @param bool $idSubtable
     *
     * @return DataTable|DataTable\Map
     */
    public function getPageUrlsFollowingSiteSearch($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false)
    {
        $dataTable = $this->getPageUrls($idSite, $period, $date, $segment, $expanded, $idSubtable);
        $this->keepPagesFollowingSearch($dataTable);
        return $dataTable;
    }

    /**
     * @param int $idSite
     * @param string $period
     * @param Date $date
     * @param bool $segment
     * @param bool $expanded
     * @param bool $idSubtable
     *
     * @return DataTable|DataTable\Map
     */
    public function getPageTitlesFollowingSiteSearch($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false)
    {
        $dataTable = $this->getPageTitles($idSite, $period, $date, $segment, $expanded, $idSubtable);
        $this->keepPagesFollowingSearch($dataTable);
        return $dataTable;
    }

    /**
     * @param DataTable $dataTable
     */
    protected function keepPagesFollowingSearch($dataTable)
    {
        // Keep only pages which are following site search
        $dataTable->filter('ColumnCallbackDeleteRow', array(
                                                           'nb_hits_following_search',
                                                           function ($value) {
                                                               return $value <= 0;
                                                           }
                                                      ));
    }

    /**
     * Returns a DataTable with analytics information for every unique entry page URL, for
     * the specified site, period & segment.
     */
    public function getEntryPageUrls($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false)
    {
        $dataTable = $this->getPageUrls($idSite, $period, $date, $segment, $expanded, $idSubtable);
        $this->filterNonEntryActions($dataTable);
        return $dataTable;
    }

    /**
     * Returns a DataTable with analytics information for every unique exit page URL, for
     * the specified site, period & segment.
     */
    public function getExitPageUrls($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false)
    {
        $dataTable = $this->getPageUrls($idSite, $period, $date, $segment, $expanded, $idSubtable);
        $this->filterNonExitActions($dataTable);
        return $dataTable;
    }

    public function getPageUrl($pageUrl, $idSite, $period, $date, $segment = false)
    {
        $callBackParameters = array('Actions_actions_url', $idSite, $period, $date, $segment, $expanded = false, $idSubtable = false);
        $dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $pageUrl, Action::TYPE_PAGE_URL);
        $this->filterPageDatatable($dataTable);
        $this->filterActionsDataTable($dataTable);
        return $dataTable;
    }

    public function getPageTitles($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false)
    {
        $dataTable = Archive::getDataTableFromArchive('Actions_actions', $idSite, $period, $date, $segment, $expanded, $idSubtable);
        $this->filterPageDatatable($dataTable);
        $this->filterActionsDataTable($dataTable, $expanded);
        return $dataTable;
    }

    /**
     * Returns a DataTable with analytics information for every unique entry page title
     * for the given site, time period & segment.
     */
    public function getEntryPageTitles($idSite, $period, $date, $segment = false, $expanded = false,
                                       $idSubtable = false)
    {
        $dataTable = $this->getPageTitles($idSite, $period, $date, $segment, $expanded, $idSubtable);
        $this->filterNonEntryActions($dataTable);
        return $dataTable;
    }

    /**
     * Returns a DataTable with analytics information for every unique exit page title
     * for the given site, time period & segment.
     */
    public function getExitPageTitles($idSite, $period, $date, $segment = false, $expanded = false,
                                      $idSubtable = false)
    {
        $dataTable = $this->getPageTitles($idSite, $period, $date, $segment, $expanded, $idSubtable);
        $this->filterNonExitActions($dataTable);
        return $dataTable;
    }

    public function getPageTitle($pageName, $idSite, $period, $date, $segment = false)
    {
        $callBackParameters = array('Actions_actions', $idSite, $period, $date, $segment, $expanded = false, $idSubtable = false);
        $dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $pageName, Action::TYPE_PAGE_TITLE);
        $this->filterPageDatatable($dataTable);
        $this->filterActionsDataTable($dataTable);
        return $dataTable;
    }

    public function getDownloads($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false)
    {
        $dataTable = Archive::getDataTableFromArchive('Actions_downloads', $idSite, $period, $date, $segment, $expanded, $idSubtable);
        $this->filterActionsDataTable($dataTable, $expanded);
        return $dataTable;
    }

    public function getDownload($downloadUrl, $idSite, $period, $date, $segment = false)
    {
        $callBackParameters = array('Actions_downloads', $idSite, $period, $date, $segment, $expanded = false, $idSubtable = false);
        $dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $downloadUrl, Action::TYPE_DOWNLOAD);
        $this->filterActionsDataTable($dataTable);
        return $dataTable;
    }

    public function getOutlinks($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false)
    {
        $dataTable = Archive::getDataTableFromArchive('Actions_outlink', $idSite, $period, $date, $segment, $expanded, $idSubtable);
        $this->filterActionsDataTable($dataTable, $expanded);
        return $dataTable;
    }

    public function getOutlink($outlinkUrl, $idSite, $period, $date, $segment = false)
    {
        $callBackParameters = array('Actions_outlink', $idSite, $period, $date, $segment, $expanded = false, $idSubtable = false);
        $dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $outlinkUrl, Action::TYPE_OUTLINK);
        $this->filterActionsDataTable($dataTable);
        return $dataTable;
    }

    public function getSiteSearchKeywords($idSite, $period, $date, $segment = false)
    {
        $dataTable = $this->getSiteSearchKeywordsRaw($idSite, $period, $date, $segment);
        $dataTable->deleteColumn(Metrics::INDEX_SITE_SEARCH_HAS_NO_RESULT);
        $this->filterPageDatatable($dataTable);
        $this->filterActionsDataTable($dataTable);
        $this->addPagesPerSearchColumn($dataTable);
        return $dataTable;
    }

    /**
     * Visitors can search, and then click "next" to view more results. This is the average number of search results pages viewed for this keyword.
     *
     * @param DataTable|DataTable\Simple|DataTable\Map $dataTable
     * @param string $columnToRead
     */
    protected function addPagesPerSearchColumn($dataTable, $columnToRead = 'nb_hits')
    {
        $dataTable->filter('ColumnCallbackAddColumnQuotient', array('nb_pages_per_search', $columnToRead, 'nb_visits', $precision = 1));
    }

    protected function getSiteSearchKeywordsRaw($idSite, $period, $date, $segment)
    {
        $dataTable = Archive::getDataTableFromArchive('Actions_sitesearch', $idSite, $period, $date, $segment, $expanded = false);
        return $dataTable;
    }

    public function getSiteSearchNoResultKeywords($idSite, $period, $date, $segment = false)
    {
        $dataTable = $this->getSiteSearchKeywordsRaw($idSite, $period, $date, $segment);
        // Delete all rows that have some results
        $dataTable->filter('ColumnCallbackDeleteRow',
            array(
                 Metrics::INDEX_SITE_SEARCH_HAS_NO_RESULT,
                 function ($value) {
                     return $value < 1;
                 }
            ));
        $dataTable->deleteRow(DataTable::ID_SUMMARY_ROW);
        $dataTable->deleteColumn(Metrics::INDEX_SITE_SEARCH_HAS_NO_RESULT);
        $this->filterPageDatatable($dataTable);
        $this->filterActionsDataTable($dataTable);
        $this->addPagesPerSearchColumn($dataTable);
        return $dataTable;
    }

    /**
     * @param int $idSite
     * @param string $period
     * @param Date $date
     * @param bool $segment
     *
     * @return DataTable|DataTable\Map
     */
    public function getSiteSearchCategories($idSite, $period, $date, $segment = false)
    {
        Actions::checkCustomVariablesPluginEnabled();
        $customVariables = APICustomVariables::getInstance()->getCustomVariables($idSite, $period, $date, $segment, $expanded = false, $_leavePiwikCoreVariables = true);

        $customVarNameToLookFor = ActionSiteSearch::CVAR_KEY_SEARCH_CATEGORY;

        $dataTable = new DataTable();
        // Handle case where date=last30&period=day
        // FIXMEA: this logic should really be refactored somewhere, this is ugly!
        if ($customVariables instanceof DataTable\Map) {
            $dataTable = $customVariables->getEmptyClone();

            $customVariableDatatables = $customVariables->getDataTables();
            $dataTables = $dataTable->getDataTables();
            foreach ($customVariableDatatables as $key => $customVariableTableForDate) {
                // we do not enter the IF, in the case idSite=1,3 AND period=day&date=datefrom,dateto,
                if ($customVariableTableForDate instanceof DataTable
                    && $customVariableTableForDate->getMetadata(Archive\DataTableFactory::TABLE_METADATA_PERIOD_INDEX)
                ) {
                    $row = $customVariableTableForDate->getRowFromLabel($customVarNameToLookFor);
                    if ($row) {
                        $dateRewrite = $customVariableTableForDate->getMetadata(Archive\DataTableFactory::TABLE_METADATA_PERIOD_INDEX)->getDateStart()->toString();
                        $idSubtable = $row->getIdSubDataTable();
                        $categories = APICustomVariables::getInstance()->getCustomVariablesValuesFromNameId($idSite, $period, $dateRewrite, $idSubtable, $segment);
                        $dataTable->addTable($categories, $key);
                    }
                }
            }
        } elseif ($customVariables instanceof DataTable) {
            $row = $customVariables->getRowFromLabel($customVarNameToLookFor);
            if ($row) {
                $idSubtable = $row->getIdSubDataTable();
                $dataTable = APICustomVariables::getInstance()->getCustomVariablesValuesFromNameId($idSite, $period, $date, $idSubtable, $segment);
            }
        }
        $this->filterActionsDataTable($dataTable);
        $this->addPagesPerSearchColumn($dataTable, $columnToRead = 'nb_actions');
        return $dataTable;
    }

    /**
     * Will search in the DataTable for a Label matching the searched string
     * and return only the matching row, or an empty datatable
     */
    protected function getFilterPageDatatableSearch($callBackParameters, $search, $actionType, $table = false,
                                                    $searchTree = false)
    {
        if ($searchTree === false) {
            // build the query parts that are searched inside the tree
            if ($actionType == Action::TYPE_PAGE_TITLE) {
                $searchedString = Common::unsanitizeInputValue($search);
            } else {
                $idSite = $callBackParameters[1];
                try {
                    $searchedString = PageUrl::excludeQueryParametersFromUrl($search, $idSite);
                } catch (Exception $e) {
                    $searchedString = $search;
                }
            }
            ArchivingHelper::reloadConfig();
            $searchTree = ArchivingHelper::getActionExplodedNames($searchedString, $actionType);
        }

        if ($table === false) {
            // fetch the data table
            $table = call_user_func_array(array('Piwik\Archive', 'getDataTableFromArchive'), $callBackParameters);

            if ($table instanceof DataTable\Map) {
                // search an array of tables, e.g. when using date=last30
                // note that if the root is an array, we filter all children
                // if an array occurs inside the nested table, we only look for the first match (see below)
                $dataTableMap = $table->getEmptyClone();

                foreach ($table->getDataTables() as $label => $subTable) {
                    $newSubTable = $this->doFilterPageDatatableSearch($callBackParameters, $subTable, $searchTree);

                    $dataTableMap->addTable($newSubTable, $label);
                }

                return $dataTableMap;
            }
        }

        return $this->doFilterPageDatatableSearch($callBackParameters, $table, $searchTree);
    }

    /**
     * This looks very similar to LabelFilter.php should it be refactored somehow? FIXME
     */
    protected function doFilterPageDatatableSearch($callBackParameters, $table, $searchTree)
    {
        // filter a data table array
        if ($table instanceof DataTable\Map) {
            foreach ($table->getDataTables() as $subTable) {
                $filteredSubTable = $this->doFilterPageDatatableSearch($callBackParameters, $subTable, $searchTree);

                if ($filteredSubTable->getRowsCount() > 0) {
                    // match found in a sub table, return and stop searching the others
                    return $filteredSubTable;
                }
            }

            // nothing found in all sub tables
            return new DataTable;
        }

        // filter regular data table
        if ($table instanceof DataTable) {
            // search for the first part of the tree search
            $search = array_shift($searchTree);
            $row = $table->getRowFromLabel($search);
            if ($row === false) {
                // not found
                $result = new DataTable;
                $result->setAllTableMetadata($table->getAllTableMetadata());
                return $result;
            }

            // end of tree search reached
            if (count($searchTree) == 0) {
                $result = new DataTable();
                $result->addRow($row);
                $result->setAllTableMetadata($table->getAllTableMetadata());
                return $result;
            }

            // match found on this level and more levels remaining: go deeper
            $idSubTable = $row->getIdSubDataTable();
            $callBackParameters[6] = $idSubTable;
            $table = call_user_func_array(array('Piwik\Archive', 'getDataTableFromArchive'), $callBackParameters);
            return $this->doFilterPageDatatableSearch($callBackParameters, $table, $searchTree);
        }

        throw new Exception("For this API function, DataTable " . get_class($table) . " is not supported");
    }

    /**
     * Common filters for Page URLs and Page Titles
     *
     * @param DataTable|DataTable\Simple|DataTable\Map $dataTable
     */
    protected function filterPageDatatable($dataTable)
    {
        $columnsToRemove = array('bounce_rate');
        $dataTable->queueFilter('ColumnDelete', array($columnsToRemove));

        // Average time on page = total time on page / number visits on that page
        $dataTable->queueFilter('ColumnCallbackAddColumnQuotient', array('avg_time_on_page', 'sum_time_spent', 'nb_visits', 0));

        // Bounce rate = single page visits on this page / visits started on this page
        $dataTable->queueFilter('ColumnCallbackAddColumnPercentage', array('bounce_rate', 'entry_bounce_count', 'entry_nb_visits', 0));

        // % Exit = Number of visits that finished on this page / visits on this page
        $dataTable->queueFilter('ColumnCallbackAddColumnPercentage', array('exit_rate', 'exit_nb_visits', 'nb_visits', 0));

        // Handle performance analytics
        $hasTimeGeneration = (array_sum($dataTable->getColumn(Metrics::INDEX_PAGE_SUM_TIME_GENERATION)) > 0);
        if ($hasTimeGeneration) {
            // Average generation time = total generation time / number of pageviews
            $precisionAvgTimeGeneration = 3;
            $dataTable->queueFilter('ColumnCallbackAddColumnQuotient', array('avg_time_generation', 'sum_time_generation', 'nb_hits_with_time_generation', $precisionAvgTimeGeneration));
            $dataTable->queueFilter('ColumnDelete', array(array('sum_time_generation')));
        } else {
            // No generation time: remove it from the API output and add it to empty_columns metadata, so that
            // the columns can also be removed from the view
            $dataTable->filter('ColumnDelete', array(array(
                                                         Metrics::INDEX_PAGE_SUM_TIME_GENERATION,
                                                         Metrics::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION,
                                                         Metrics::INDEX_PAGE_MIN_TIME_GENERATION,
                                                         Metrics::INDEX_PAGE_MAX_TIME_GENERATION
                                                     )));

            if ($dataTable instanceof DataTable) {
                $emptyColumns = $dataTable->getMetadata(DataTable::EMPTY_COLUMNS_METADATA_NAME);
                if (!is_array($emptyColumns)) {
                    $emptyColumns = array();
                }
                $emptyColumns[] = 'sum_time_generation';
                $emptyColumns[] = 'avg_time_generation';
                $emptyColumns[] = 'min_time_generation';
                $emptyColumns[] = 'max_time_generation';
                $dataTable->setMetadata(DataTable::EMPTY_COLUMNS_METADATA_NAME, $emptyColumns);
            }
        }
    }

    /**
     * Common filters for all Actions API
     *
     * @param DataTable|DataTable\Simple|DataTable\Map $dataTable
     * @param bool $expanded
     */
    protected function filterActionsDataTable($dataTable, $expanded = false)
    {
        // Must be applied before Sort in this case, since the DataTable can contain both int and strings indexes
        // (in the transition period between pre 1.2 and post 1.2 datatable structure)
        $dataTable->filter('ReplaceColumnNames');
        $dataTable->filter('Sort', array('nb_visits', 'desc', $naturalSort = false, $expanded));

        $dataTable->queueFilter('ReplaceSummaryRowLabel');
    }

    /**
     * Removes DataTable rows referencing actions that were never the first action of a visit.
     *
     * @param DataTable $dataTable
     */
    private function filterNonEntryActions($dataTable)
    {
        $dataTable->filter('ColumnCallbackDeleteRow', array('entry_nb_visits', function ($visits) { return !strlen($visits); }));
    }

    /**
     * Removes DataTable rows referencing actions that were never the last action of a visit.
     *
     * @param DataTable $dataTable
     */
    private function filterNonExitActions($dataTable)
    {
        $dataTable->filter('ColumnCallbackDeleteRow', array('exit_nb_visits', function ($visits) { return !strlen($visits); }));
    }
}
