<?php

/**
 * Matomo - free/libre analytics platform
 *
 * @link    https://matomo.org
 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
 */
namespace Piwik\DataTable\Renderer;

use Exception;
use Piwik\DataTable\Map;
use Piwik\DataTable\Renderer;
use Piwik\DataTable;
use Piwik\DataTable\Simple;
use Piwik\Piwik;
/**
 * XML export of a given DataTable.
 * See the tests cases for more information about the XML format (/tests/core/DataTable/Renderer.test.php)
 * Or have a look at the API calls examples.
 *
 * Works with recursive DataTable (when a row can be associated with a subDataTable).
 *
 */
class Xml extends Renderer
{
    /**
     * Computes the dataTable output and returns the string/binary
     */
    public function render() : string
    {
        return '<?xml version="1.0" encoding="utf-8" ?>' . "\n" . $this->renderTable($this->table);
    }
    /**
     * Computes the output for the given data table
     *
     * @param DataTable|DataTable\Map $table
     * @throws Exception
     */
    protected function renderTable($table, bool $returnOnlyDataTableXml = \false, string $prefixLines = '') : string
    {
        $array = $this->convertDataTableToArray($table);
        if ($table instanceof Map) {
            $out = $this->renderDataTableMap($table, $array, $prefixLines);
            if ($returnOnlyDataTableXml) {
                return $out;
            }
            return "<results>\n{$out}</results>";
        }
        // integer value of ZERO is a value we want to display
        if ($array != 0 && empty($array)) {
            if ($returnOnlyDataTableXml) {
                throw new Exception("Illegal state, what xml shall we return?");
            }
            return "<result />";
        }
        if ($table instanceof Simple) {
            if (is_array($array)) {
                $out = $this->renderDataTableSimple($array);
            } else {
                $out = $array;
            }
            if ($returnOnlyDataTableXml) {
                return $out;
            }
            if (is_array($array)) {
                $out = "<result>\n" . $out . "</result>";
            } else {
                $value = self::formatValueXml($out);
                if ($value === '') {
                    $out = "<result />";
                } else {
                    $out = "<result>" . $value . "</result>";
                }
            }
            return $out;
        }
        if ($table instanceof DataTable) {
            $out = $this->renderDataTable($array);
            if ($returnOnlyDataTableXml) {
                return $out;
            }
            return "<result>\n{$out}</result>";
        }
        if (is_array($array)) {
            $out = $this->renderArray($array, $prefixLines . "\t");
            if ($returnOnlyDataTableXml) {
                return $out;
            }
            return "<result>\n{$out}</result>";
        }
        return '';
    }
    /**
     * Renders an array as XML.
     *
     * @param array $array The array to render.
     * @param string $prefixLines The string to prefix each line in the output.
     */
    private function renderArray(array $array, string $prefixLines) : string
    {
        $isAssociativeArray = Piwik::isAssociativeArray($array);
        // check if array contains arrays, and if not wrap the result in an extra <row> element
        // (only check if this is the root renderArray call)
        // NOTE: this is for backwards compatibility. before, array's were added to a new DataTable.
        // if the array had arrays, they were added as multiple rows, otherwise it was treated as
        // one row. removing will change API output.
        $wrapInRow = $prefixLines === "\t" && self::shouldWrapArrayBeforeRendering($array, $wrapSingleValues = \false, $isAssociativeArray);
        // render the array
        $result = "";
        if ($wrapInRow) {
            $result .= "{$prefixLines}<row>\n";
            $prefixLines .= "\t";
        }
        foreach ($array as $key => $value) {
            // based on the type of array & the key, determine how this node will look
            if ($isAssociativeArray) {
                if (strpos($key, '=') !== \false) {
                    [$keyAttributeName, $key] = explode('=', $key, 2);
                    $prefix = "<row {$keyAttributeName}=\"{$key}\">";
                    $suffix = "</row>";
                    $emptyNode = "<row {$keyAttributeName}=\"{$key}\">";
                } elseif (!self::isValidXmlTagName($key)) {
                    $prefix = "<row key=\"{$key}\">";
                    $suffix = "</row>";
                    $emptyNode = "<row key=\"{$key}\"/>";
                } else {
                    $prefix = "<{$key}>";
                    $suffix = "</{$key}>";
                    $emptyNode = "<{$key} />";
                }
            } else {
                $prefix = "<row>";
                $suffix = "</row>";
                $emptyNode = "<row/>";
            }
            // render the array item
            if (is_array($value) || $value instanceof \stdClass) {
                $result .= $prefixLines . $prefix . "\n";
                $result .= $this->renderArray((array) $value, $prefixLines . "\t");
                $result .= $prefixLines . $suffix . "\n";
            } elseif ($value instanceof DataTable || $value instanceof Map) {
                if ($value->getRowsCount() == 0) {
                    $result .= $prefixLines . $emptyNode . "\n";
                } else {
                    $result .= $prefixLines . $prefix . "\n";
                    if ($value instanceof Map) {
                        $result .= $this->renderDataTableMap($value, $this->convertDataTableToArray($value), $prefixLines);
                    } elseif ($value instanceof Simple) {
                        $result .= $this->renderDataTableSimple($this->convertDataTableToArray($value), $prefixLines);
                    } else {
                        $result .= $this->renderDataTable($this->convertDataTableToArray($value), $prefixLines);
                    }
                    $result .= $prefixLines . $suffix . "\n";
                }
            } else {
                $xmlValue = self::formatValueXml($value);
                if (strlen(strval($xmlValue)) !== 0) {
                    $result .= $prefixLines . $prefix . $xmlValue . $suffix . "\n";
                } else {
                    $result .= $prefixLines . $emptyNode . "\n";
                }
            }
        }
        if ($wrapInRow) {
            $result .= substr($prefixLines, 0, strlen($prefixLines) - 1) . "</row>\n";
        }
        return $result;
    }
    /**
     * Computes the output for the given data table array
     *
     * @param array $array
     */
    protected function renderDataTableMap(Map $table, array $array, string $prefixLines = '') : string
    {
        // CASE 1
        //array
        //  'day1' => string '14' (length=2)
        //  'day2' => string '6' (length=1)
        $firstTable = current($array);
        if (!is_array($firstTable)) {
            $xml = '';
            $nameDescriptionAttribute = $table->getKeyName();
            foreach ($array as $valueAttribute => $value) {
                if (empty($value)) {
                    $xml .= $prefixLines . "\t<result {$nameDescriptionAttribute}=\"{$valueAttribute}\" />\n";
                } elseif ($value instanceof DataTable\DataTableInterface) {
                    //TODO somehow this code is not tested, cover this case
                    $out = $this->renderTable($value, \true);
                    $xml .= "\t<result {$nameDescriptionAttribute}=\"{$valueAttribute}\">\n{$out}</result>\n";
                } elseif (is_array($value)) {
                    if (!is_array(reset($value))) {
                        $out = $this->renderDataTableSimple($value);
                    } else {
                        $out = $this->renderDataTable($value);
                    }
                    $xml .= "\t<result {$nameDescriptionAttribute}=\"{$valueAttribute}\">\n{$out}</result>\n";
                } else {
                    $xml .= $prefixLines . "\t<result {$nameDescriptionAttribute}=\"{$valueAttribute}\">" . self::formatValueXml($value) . "</result>\n";
                }
            }
            return $xml;
        }
        $subTables = $table->getDataTables();
        $firstTable = current($subTables);
        // CASE 2
        //array
        //  'day1' =>
        //    array
        //      'nb_uniq_visitors' => string '18'
        //      'nb_visits' => string '101'
        //  'day2' =>
        //    array
        //      'nb_uniq_visitors' => string '28'
        //      'nb_visits' => string '11'
        if ($firstTable instanceof Simple) {
            $xml = '';
            $nameDescriptionAttribute = $table->getKeyName();
            foreach ($array as $valueAttribute => $dataTableSimple) {
                if (count($dataTableSimple) == 0) {
                    $xml .= $prefixLines . "\t<result {$nameDescriptionAttribute}=\"{$valueAttribute}\" />\n";
                } else {
                    if (is_array($dataTableSimple)) {
                        if (!is_array(reset($dataTableSimple))) {
                            $dataTableSimple = "\n" . $this->renderDataTableSimple($dataTableSimple, $prefixLines . "\t") . $prefixLines . "\t";
                        } else {
                            $dataTableSimple = "\n" . $this->renderDataTable($dataTableSimple, $prefixLines . "\t") . $prefixLines . "\t";
                        }
                    }
                    $xml .= $prefixLines . "\t<result {$nameDescriptionAttribute}=\"{$valueAttribute}\">" . $dataTableSimple . "</result>\n";
                }
            }
            return $xml;
        }
        // CASE 3
        //array
        //  'day1' =>
        //    array
        //      0 =>
        //        array
        //          'label' => string 'phpmyvisites'
        //          'nb_uniq_visitors' => int 11
        //          'nb_visits' => int 13
        //      1 =>
        //        array
        //          'label' => string 'phpmyvisits'
        //          'nb_uniq_visitors' => int 2
        //          'nb_visits' => int 2
        //  'day2' =>
        //    array
        //      0 =>
        //        array
        //          'label' => string 'piwik'
        //          'nb_uniq_visitors' => int 121
        //          'nb_visits' => int 130
        //      1 =>
        //        array
        //          'label' => string 'piwik bis'
        //          'nb_uniq_visitors' => int 20
        //          'nb_visits' => int 120
        if ($firstTable instanceof DataTable) {
            $xml = '';
            $nameDescriptionAttribute = $table->getKeyName();
            foreach ($array as $keyName => $arrayForSingleDate) {
                $dataTableOut = $this->renderDataTable($arrayForSingleDate, $prefixLines . "\t");
                if (empty($dataTableOut)) {
                    $xml .= $prefixLines . "\t<result {$nameDescriptionAttribute}=\"{$keyName}\" />\n";
                } else {
                    $xml .= $prefixLines . "\t<result {$nameDescriptionAttribute}=\"{$keyName}\">\n";
                    $xml .= $dataTableOut;
                    $xml .= $prefixLines . "\t</result>\n";
                }
            }
            return $xml;
        }
        if ($firstTable instanceof Map) {
            $xml = '';
            $tables = $table->getDataTables();
            $nameDescriptionAttribute = $table->getKeyName();
            foreach ($tables as $valueAttribute => $tableInArray) {
                $out = $this->renderTable($tableInArray, \true, $prefixLines . "\t");
                $xml .= $prefixLines . "\t<result {$nameDescriptionAttribute}=\"{$valueAttribute}\">\n" . $out . $prefixLines . "\t</result>\n";
            }
            return $xml;
        }
        return '';
    }
    /**
     * Computes the output for the given data array
     *
     * @param array $array
     */
    protected function renderDataTable($array, string $prefixLine = '') : string
    {
        $columnsHaveInvalidChars = $this->areTableLabelsInvalidXmlTagNames(reset($array));
        $out = '';
        foreach ($array as $rowId => $row) {
            if (!is_array($row)) {
                $value = self::formatValueXml($row);
                if (strlen($value) == 0) {
                    $out .= $prefixLine . "\t\t<{$rowId} />\n";
                } else {
                    $out .= $prefixLine . "\t\t<{$rowId}>" . $value . "</{$rowId}>\n";
                }
                continue;
            }
            // Handing case idgoal=7, creating a new array for that one
            $rowAttribute = '';
            if (strstr($rowId, '=') !== \false) {
                $rowAttribute = explode('=', $rowId);
                $rowAttribute = " " . $rowAttribute[0] . "='" . $rowAttribute[1] . "'";
            }
            $out .= $prefixLine . "\t<row{$rowAttribute}>";
            if (count($row) === 1 && key($row) === 0) {
                $value = self::formatValueXml(current($row));
                $out .= $prefixLine . $value;
            } else {
                $out .= "\n";
                foreach ($row as $name => $value) {
                    // handle the recursive dataTable case by XML outputting the recursive table
                    if ($value instanceof DataTable) {
                        $value = $this->convertDataTableToArray($value);
                        if ($value instanceof Simple) {
                            $value = "\n" . $this->renderDataTableSimple($value, $prefixLine . "\t\t");
                        } else {
                            $value = "\n" . $this->renderDataTable($value, $prefixLine . "\t\t");
                        }
                        $value .= $prefixLine . "\t\t";
                    } elseif (is_array($value)) {
                        if (is_array(reset($value))) {
                            $value = "\n" . $this->renderDataTable($value, $prefixLine . "\t\t");
                        } else {
                            $value = "\n" . $this->renderArray($value, $prefixLine . "\t\t");
                        }
                        $value .= $prefixLine . "\t\t";
                    } else {
                        $value = self::formatValueXml($value);
                    }
                    [$tagStart, $tagEnd] = $this->getTagStartAndEndFor($name, $columnsHaveInvalidChars);
                    if (strlen((string) $value) == 0) {
                        $out .= $prefixLine . "\t\t<{$tagStart} />\n";
                    } else {
                        $out .= $prefixLine . "\t\t<{$tagStart}>" . $value . "</{$tagEnd}>\n";
                    }
                }
                $out .= "\t";
            }
            $out .= $prefixLine . "</row>\n";
        }
        return $out;
    }
    /**
     * Computes the output for the given data array (representing a simple data table)
     *
     * @param $array
     */
    protected function renderDataTableSimple($array, string $prefixLine = '') : string
    {
        if (!is_array($array)) {
            $array = array('value' => $array);
        }
        $columnsHaveInvalidChars = $this->areTableLabelsInvalidXmlTagNames($array);
        $out = '';
        foreach ($array as $keyName => $value) {
            $xmlValue = self::formatValueXml($value);
            [$tagStart, $tagEnd] = $this->getTagStartAndEndFor($keyName, $columnsHaveInvalidChars);
            if (is_string($xmlValue) && strlen($xmlValue) == 0) {
                $out .= $prefixLine . "\t<{$tagStart} />\n";
            } elseif ($value instanceof DataTable || is_array($value)) {
                $arrayValue = $this->convertDataTableToArray($value);
                if (!is_array(reset($arrayValue))) {
                    $xmlTable = $this->renderDataTableSimple($arrayValue, $prefixLine . "\t");
                } else {
                    $xmlTable = $this->renderDataTable($arrayValue, $prefixLine . "\t");
                }
                $out .= $prefixLine . "\t<{$tagStart}>\n" . $xmlTable . $prefixLine . "\t</{$tagEnd}>\n";
            } else {
                $out .= $prefixLine . "\t<{$tagStart}>" . $xmlValue . "</{$tagEnd}>\n";
            }
        }
        return $out;
    }
    /**
     * Returns true if a string is a valid XML tag name, false if otherwise.
     */
    private static function isValidXmlTagName(string $str) : bool
    {
        static $validTagRegex = null;
        if ($validTagRegex === null) {
            $invalidTagChars = "!\"#\$%&'()*+,\\/;<=>?@[\\]\\\\^`{|}~";
            $invalidTagStartChars = $invalidTagChars . "\\-.0123456789";
            $validTagRegex = "/^[^" . $invalidTagStartChars . "][^" . $invalidTagChars . "]*\$/";
        }
        $result = preg_match($validTagRegex, $str);
        return !empty($result);
    }
    private function areTableLabelsInvalidXmlTagNames($rowArray) : bool
    {
        if (!empty($rowArray)) {
            foreach ($rowArray as $name => $value) {
                if (!self::isValidXmlTagName($name)) {
                    return \true;
                }
            }
        }
        return \false;
    }
    private function getTagStartAndEndFor($keyName, $columnsHaveInvalidChars) : array
    {
        if ($columnsHaveInvalidChars) {
            $tagStart = "col name=\"" . self::formatValueXml($keyName) . "\"";
            $tagEnd = "col";
        } else {
            $tagStart = $tagEnd = $keyName;
        }
        return [$tagStart, $tagEnd];
    }
}
