<?php

namespace Xentral\Modules\Report;

use DateTime;
use Exception;
use Xentral\Components\Database\Database;
use Xentral\Components\Util\StringUtil;
use Xentral\Modules\Report\Data\ReportColumn;
use Xentral\Modules\Report\Data\ReportColumnCollection;
use Xentral\Modules\Report\Data\ReportData;
use Xentral\Modules\Report\Data\ReportParameter;
use Xentral\Modules\Report\Data\ReportParameterCollection;
use Xentral\Modules\Report\Exception\ColumnFormatException;
use Xentral\Modules\Report\Exception\DatabaseTransactionException;
use Xentral\Modules\Report\Exception\InvalidArgumentException;
use Xentral\Modules\Report\Exception\ParameterNameException;
use Xentral\Modules\Report\Exception\ReportSqlQueryException;
use Xentral\Modules\Report\Exception\UnresolvedParameterException;

final class ReportService
{
    /** @var Database $db */
    private $db;

    /** @var ReportGateway $gateway */
    private $gateway;

    /** @var ReportResolveParameterService $resolver */
    private $resolver;

    /**
     * @param Database                      $db
     * @param ReportGateway                 $gateway
     * @param ReportResolveParameterService $resolver
     */
    public function __construct(Database $db, ReportGateway $gateway, ReportResolveParameterService $resolver)
    {
        $this->db = $db;
        $this->gateway = $gateway;
        $this->resolver = $resolver;
    }

    /**
     * @param $sqlStatement
     *
     * @return bool
     */
    public function isSqlStatementAllowed($sqlStatement)
    {
        $filterKeyWords = [
            'INTO',
            'INSERT',
            'UPDATE',
            'DELETE',
            'ALTER',
            'SHOW',
            'USE',
            'TRUNCATE',
            'LOAD',
            'CREATE',
            'DROP',
            'RENAME',
        ];

        foreach ($filterKeyWords as $keyWord) {
            $allOccurances = [];
            $escapedOccurances = [];
            $allPattern = sprintf('/%s/i', $keyWord);
            $escapedPattern = sprintf('/\W[`\'][^`\']*%s[^`\']*[`\']\W?/i', $keyWord);
            if (preg_match_all($allPattern, $sqlStatement, $allOccurances)) {
                preg_match_all($escapedPattern, $sqlStatement, $escapedOccurances);
                if (count($allOccurances[0]) !== count($escapedOccurances[0])) {
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * @param ReportData $report
     *
     * @return int
     */
    public function saveReport(ReportData $report)
    {
        $id = $report->getId();
        if ($id !== null && $id > 0 && $this->gateway->reportExists($id)) {
            $newId = $this->updateReport($report);
        } else {
            $newId = $this->insertReport($report);
        }

        return $newId;
    }

    /**
     * @param int $id
     *
     * @throws InvalidArgumentException
     * @throws DatabaseTransactionException
     *
     * @return int
     */
    public function copyReport($id = 0)
    {
        $report = $this->gateway->getReportById($id);
        if ($report === null) {
            throw new InvalidArgumentException(sprintf('Report with id %s not found.', $id));
        }

        $newParameters = null;
        $parameters = $report->getParameters();
        if ($parameters !== null) {
            $newParameters = [];
            foreach ($parameters as $param) {
                $data = $param->toArray();
                $data['id'] = 0;
                $newParameters[] = ReportParameter::fromDbState($data);
            }
        }

        $newColumns = null;
        $columns = $report->getColumns();
        if ($columns !== null) {
            $newColumns = [];
            foreach ($columns as $column) {
                $data = $column->toArray();
                $data['id'] = 0;
                $temp = ReportColumn::fromDbState($data);
                $newColumns[] = $temp;
            }
        }

        $newName = $this->generateIncrementedReportName($report->getName());
        $newReport = new ReportData(
            $newName,
            $report->getDescription(),
            $report->getProjectId(),
            $report->getSqlQuery(),
            new ReportColumnCollection($newColumns),
            new ReportParameterCollection($newParameters),
            0,
            $report->getRemark(),
            $report->getCategory(),
            false
        );

        return $this->insertReport($newReport);
    }

    /**
     * @param ReportColumnCollection $columns
     * @param int                    $reportId
     */
    public function saveColumnCollection(ReportColumnCollection $columns, $reportId)
    {
        foreach ($columns as $column) {
            $this->saveColumn($column, $reportId);
        }
    }

    /**
     * @param ReportColumn       $column
     * @param                    $reportId
     *
     * @return int
     */
    public function saveColumn(ReportColumn $column, $reportId)
    {
        $id = $column->getId();
        if ($id !== null && $id > 0 && $this->gateway->columnExists($id)) {
            $newId = $this->updateColumn($column, $reportId);
        } else {
            $newId = $this->insertColumn($column, $reportId);
        }

        return $newId;
    }

    /**
     * @param ReportParameterCollection $parameters
     * @param int                       $reportId
     */
    public function saveParameterCollection(ReportParameterCollection $parameters, $reportId)
    {
        foreach ($parameters as $param) {
            $this->saveParameter($param, $reportId);
        }
    }

    /**
     * @param ReportParameter $parameter
     * @param int             $reportId
     *
     * @return int
     */
    public function saveParameter(ReportParameter $parameter, $reportId)
    {
        $id = $parameter->getId();
        if ($id !== null && $id > 0 && $this->gateway->parameterExists($id)) {
            $newId = $this->updateParameter($parameter, $reportId);
        } else {
            $newId = $this->insertParameter($parameter, $reportId);
        }

        return $newId;
    }

    /**
     * @param array $transferArray
     *
     * @return int
     */
    public function saveTransferArray($transferArray)
    {
        $id = (int)$transferArray['id'];
        $reportId = (int)$transferArray['report_id'];
        if (!$this->gateway->reportExists($reportId)) {
            return 0;
        }

        if ($id !== null && $id > 0) {
            $newId = $this->updateTransferArray($transferArray);
        } else {
            $newId = $this->insertTransferArray($transferArray);
        }

        return $newId;
    }

    /**
     * @param array $shareArray
     *
     * @return int
     */
    public function saveShareArray($shareArray)
    {
        $id = (int)$shareArray['id'];
        $reportId = (int)$shareArray['report_id'];
        if (!$this->gateway->reportExists($reportId)) {
            return 0;
        }

        if ($id !== null && $id > 0) {
            $newId = $this->updateShareArray($shareArray);
        } else {
            $newId = $this->insertShareArray($shareArray);
        }

        return $newId;
    }

    /**
     * @param array $reportUserArray
     *
     * @return int
     */
    public function saveReportUserArray($reportUserArray)
    {
        $id = (int)$reportUserArray['id'];
        $reportId = (int)$reportUserArray['report_id'];
        if (!$this->gateway->reportExists($reportId)) {
            return 0;
        }
        if ($id !== null && $id > 0) {
            $newId = $this->updateReportUserArray($reportUserArray);
        } else {
            $newId = $this->insertReportUserArray($reportUserArray);
        }

        return $newId;
    }

    /**
     * @param int $id
     *
     * @throws InvalidArgumentException
     *
     * @return void
     */
    public function deleteColumnById($id)
    {
        if ($id < 1) {
            throw new InvalidArgumentException('Cannot delete Column without valid ID.');
        }
        $sql = 'DELETE FROM `report_column` WHERE id=:idvalue';
        $values = ['idvalue' => $id];

        $this->db->perform($sql, $values);
    }

    /**
     * @param $reportId
     *
     * @return int amount of deleted columns
     */
    public function deleteAllColumnsByReportId($reportId)
    {
        $sql = 'DELETE FROM `report_column` WHERE report_id = :reportId';

        return $this->db->fetchAffected($sql, ['reportId' => $reportId]);
    }

    /**
     * @param int $id
     *
     * @throws DatabaseTransactionException
     *
     * @return void
     */
    public function deleteReportById($id)
    {
        if ($id < 1) {
            throw new InvalidArgumentException('Cannot delete Report without valid ID.');
        }
        $sqlDeleteReport = 'DELETE FROM `report` WHERE id=:idValue';
        $sqlDeleteParam = 'DELETE FROM `report_parameter` WHERE report_id=:idValue';
        $sqlDeleteColumn = 'DELETE FROM `report_column` WHERE report_id=:idValue';
        $sqlDeleteShare = 'DELETE FROM `report_share` WHERE report_id=:idValue';
        $sqlDeleteTransfer = 'DELETE FROM `report_transfer` WHERE report_id=:idValue';
        $sqlDeleteSharedUser = 'DELETE FROM `report_user` WHERE report_id=:idValue';
        $sqlDeleteFavorites = 'DELETE FROM `report_favorite` WHERE report_id=:idValue';
        $values = ['idValue' => $id];
        $this->db->beginTransaction();
        try {
            $this->db->perform($sqlDeleteParam, $values);
            $this->db->perform($sqlDeleteColumn, $values);
            $this->db->perform($sqlDeleteReport, $values);
            $this->db->perform($sqlDeleteShare, $values);
            $this->db->perform($sqlDeleteTransfer, $values);
            $this->db->perform($sqlDeleteSharedUser, $values);
            $this->db->perform($sqlDeleteFavorites, $values);
        } catch (Exception $e) {
            $this->db->rollBack();
            throw new DatabaseTransactionException('Failed to delete report', $e->getCode(), $e);
        }
        $this->db->commit();
    }

    /**
     * @param int $id
     *
     * @return bool true = success
     */
    public function tryDeleteUserShare($id)
    {
        if ($id < 1) {
            return false;
        }
        $sql = 'DELETE FROM `report_user` WHERE id = :idValue';
        $countAffected = $this->db->fetchAffected($sql, ['idValue' => $id]);

        return $countAffected > 0;
    }

    /**
     * @param int      $reportId
     * @param DateTime $dateTime
     *
     * @return bool true = success
     */
    public function setLastTransferFtp($reportId, DateTime $dateTime)
    {
        $sql = 'UPDATE `report_transfer` SET ftp_last_transfer = :dateValue WHERE report_id = :idValue';
        $values = ['dateValue' => $dateTime->format('Y-m-d H:i:s'), 'idValue' => (int)$reportId];
        $affectedRows = $this->db->fetchAffected($sql, $values);

        return !($affectedRows < 1);
    }

    /**
     * @param int      $reportId
     * @param DateTime $dateTime
     *
     * @return bool true = success
     */
    public function setLastTransferEmail($reportId, DateTime $dateTime)
    {
        $sql = 'UPDATE `report_transfer` SET email_last_transfer = :dateValue WHERE report_id = :idValue';
        $values = ['dateValue' => $dateTime->format('Y-m-d H:i:s'), 'idValue' => (int)$reportId];
        $affectedRows = $this->db->fetchAffected($sql, $values);

        return !($affectedRows < 1);
    }

    /**
     * @param $id
     */
    public function deleteParamById($id)
    {
        if ($id < 1) {
            throw new InvalidArgumentException('Cannot delete Parameter without valid ID.');
        }
        $sql = 'DELETE FROM report_parameter WHERE id=:idvalue';
        $values = ['idvalue' => $id];

        $this->db->perform($sql, $values);
    }

    /**
     * @param ReportData $report
     *
     * @param array      $parameterValues
     *
     * @return string sql statement with inserted variable values
     */
    public function resolveParameters(ReportData $report, $parameterValues = [])
    {
        return $this->resolver->resolveParameters($report, $parameterValues);
    }

    /**
     * @param string $statement
     * @param array  $parameters
     *
     * @return array
     */
    public function testSqlStatement($statement, $parameters = [])
    {
        //1. error: statement empty
        if ($statement === '') {
            $testResult['messagetype'] = 'info';
            $testResult['message'] = 'Cannot execute empty statement';

            return $testResult;
        }

        //2. error: invalid parameter names
        $testReport = new ReportData('', '', '', $statement);
        $params = [];
        foreach ($parameters as $name => $value) {
            try {
                $params[] = new ReportParameter($name, $value);
            } catch (ParameterNameException $e) {
                $testResult['messagetype'] = 'error';
                $testResult['message'] = sprintf(
                    'Invalid Parameter name "%s".
                    Parameter names can only contain letters, numbers and "_".
                    Parameter names must start with a letter.',
                    $name
                );

                return $testResult;
            }
        }
        $testReport->setParameters(new ReportParameterCollection($params));

        //3. error: wrong variable names in query
        $errorVarnames = $this->resolver->findInvalidVariableNames($statement);
        if (count($errorVarnames) > 0) {
            $testResult['messagetype'] = 'error';
            $testResult['message'] = sprintf(
                'Invalid Variable Names: %s. Variable names can only contain letters A-Z, numbers and "_".',
                implode(', ', $errorVarnames)
            );

            return $testResult;
        }

        //4. error: unresolved Variables
        try {
            $compiled = $this->resolveParameters($testReport);
        } catch (UnresolvedParameterException $e) {
            $testResult['messagetype'] = 'error';
            $testResult['message'] = $e->getMessage();

            return $testResult;
        }

        //5. error: Syntax error
        if (!$this->isSqlStatementAllowed($compiled)) {
            $testResult['messagetype'] = 'error';
            $testResult['message'] = 'Reports can only use SELECT statements!';

            return $testResult;
        }

        //6. error: Query Semantic error
        if(!preg_match('/LIMIT\s*\d/', $compiled)){
            $compiled .= " LIMIT 101";
        }
        try {
            $rows = $this->db->fetchAll($compiled);
            $columnNames = [];
            if (is_array($rows) && count($rows) > 0) {
                $columnNames = array_keys($rows[0]);
            }
        } catch (Exception $e) {
            $testResult['messagetype'] = 'error';
            $testResult['message'] = sprintf("QUERY FAILED:\n%s", $e->getMessage());

            $testResult['message'] .= "\n\r".$compiled;

            return $testResult;
        }

        //everything fine
        $message = "Query successful: More than 100 datasets found";
        if(count($rows) < 101){
            $message = sprintf('Query successful: %s datasets found', count($rows));
        }

        $message .= "\n\r".print_r($rows[0],true);

        $testResult = [
            'messagetype' => 'success',
            'message'     => $message,
            'columnnames' => $columnNames,
        ];

        return $testResult;
    }

    /**
     * Finds possible copied reports of the name and
     * generates a report name with increment if needed.
     *
     * @example  generateIncrementedReportName('Existing Report') -> 'Existing Report (1)'
     * @example  generateIncrementedReportName('Existing Report (1)') -> 'Existing Report (2)'
     * @example  generateIncrementedReportName('New Report') -> 'New Report'
     *
     * @param string $reportName
     *
     * @param int    $ignoreId
     *
     * @return string
     */
    public function generateIncrementedReportName($reportName, $ignoreId = 0)
    {
        $newName = $this->getBaseName($reportName);
        $copies = $this->findReportCopyNames($newName, $ignoreId);
        if (count($copies) > 0) {
            $maxIncrement = 0;
            foreach ($copies as $name) {
                preg_match('/^.+\s*\((\d+)\)$/', $name, $matches);
                if (isset($matches[1]) && (int)$matches[1] > $maxIncrement) {
                    $maxIncrement = (int)$matches[1];
                }
            }
            $newName = sprintf('%s (%s)', $newName, $maxIncrement + 1);
        }

        return $newName;
    }

    /*
     *
     */
    public function autoCreateColumns(ReportData $report)
    {
        try {
            $compiledStatement = $this->resolveParameters($report);
            $sampleRow = $this->db->fetchRow($compiledStatement);
        } catch (Exception $e) {
            throw new ReportSqlQueryException('Query delivered no result.');
        }
        $resultKeys = array_keys($sampleRow);
        $existingKeys = [];
        $columns = $report->getColumns();
        $columnsArray = [];
        $totalWidth = 0;
        $maxSequence = 0;
        if ($columns !== null) {
            foreach ($columns as $column) {
                $columnsArray[] = $column;
                $totalWidth += (int)$column->getWidth();
                $existingKeys[] = $column->getKey();
                if ($column->getSequence() > $maxSequence) {
                    $maxSequence = $column->getSequence();
                }
            }
        }

        $addKeys = [];
        foreach ($resultKeys as $key) {
            if (!in_array($key, $existingKeys, true)) {
                $addKeys[] = $key;
            }
        }

        if (count($addKeys) === 0) {
            return $report;
        }
        $width = floor((190 - $totalWidth) / count($addKeys));
        $sequence = $maxSequence + 1;
        foreach ($addKeys as $addKey) {
            $newCol = new ReportColumn(
                $addKey,
                StringUtil::toTitleCase($addKey),
                $width,
                ReportColumn::ALIGN_LEFT,
                false,
                0,
                $sequence
            );
            $columnsArray[] = $newCol;
            $sequence++;
        }
        $columnCollection = new ReportColumnCollection($columnsArray);
        $report->setColumns($columnCollection);

        return $report;
    }

    /**
     * @param int $reportId
     * @param int $userId
     *
     * @return bool success
     */
    public function addReportFavorite($reportId, $userId)
    {
        if ($reportId < 1 || $userId < 1) {
            return false;
        }
        if ($this->gateway->isFavoriteReportOfUser($reportId, $userId)) {
            return true;
        }
        $sql = 'INSERT INTO `report_favorite` (report_id, user_id) VALUES (:reportId, :userId)';
        $affected = $this->db->fetchAffected($sql, ['reportId' => $reportId, 'userId' => $userId]);

        return $affected > 0;
    }

    /**
     * @param int $reportId
     * @param int $userId
     *
     * @return bool success
     */
    public function removeReportFavorite($reportId, $userId)
    {
        if ($reportId < 1 || $userId < 1) {
            return false;
        }
        if (!$this->gateway->isFavoriteReportOfUser($reportId, $userId)) {
            return true;
        }
        $sql = 'DELETE FROM `report_favorite` WHERE report_id = :reportId AND user_id = :userId';
        $affected = $this->db->fetchAffected($sql, ['reportId' => $reportId, 'userId' => $userId]);

        return $affected > 0;
    }

    /**
     * @param string $format
     *
     * @throws ColumnFormatException
     *
     * @return void
     */
    public  function validateCustomColumnFormat(string $format): void
    {
        if (!$this->isSqlStatementAllowed($format)) {
            throw new ColumnFormatException('invalid column format statement');
        }
        if (preg_match('/{VALUE}/i', $format) !== 1) {
            throw new ColumnFormatException('Format statement must contain "{VALUE}" variable.');
        }
    }

    /**
     * @param ReportData $report
     *
     * @throws DatabaseTransactionException
     *
     * @return int
     */
    private function insertReport(ReportData $report)
    {
        $sql = 'INSERT INTO report (name, description, project, sql_query, remark, category, readonly,
                                    csv_delimiter, csv_enclosure)
                VALUES (:name, :description, :project, :sql_query, :remark, :category, :readonly,
                                    :csv_delimiter, :csv_enclosure)';
        $values = [
            'name'          => $report->getName(),
            'description'   => $report->getDescription(),
            'project'       => $report->getProjectId(),
            'sql_query'     => $report->getSqlQuery(),
            'remark'        => $report->getRemark(),
            'category'      => $report->getCategory(),
            'readonly'      => 0,
            'csv_delimiter' => $report->getCsvDelimiter(),
            'csv_enclosure' => $report->getCsvEnclosure(),
        ];
        if ($report->isReadonly()) {
            $values['readonly'] = 1;
        }

        $this->db->beginTransaction();
        try {
            $this->db->perform($sql, $values);
            $insertId = $this->db->lastInsertId();
            $columns = $report->getColumns();
            if ($columns !== null && $columns !== []) {
                $this->saveColumnCollection($columns, $insertId);
            }
            $params = $report->getParameters();
            if ($params !== null && $params !== []) {
                $this->saveParameterCollection($params, $insertId);
            }
        } catch (Exception $e) {
            $this->db->rollBack();
            throw new DatabaseTransactionException($e->getMessage(), $e->getCode(), $e);
        }
        $this->db->commit();

        return $insertId;
    }

    /**
     * @param ReportData $report
     *
     * @return int
     */
    private function updateReport(ReportData $report)
    {
        $id = $report->getId();
        $sql = 'UPDATE report 
                SET name=:name, description=:description, project=:project, sql_query=:sql_query, remark=:remark, 
                    category=:category, csv_delimiter=:csv_delimiter, csv_enclosure=:csv_enclosure, readonly=:readonly
                WHERE id=:idvalue';
        $values = [
            'name'          => $report->getName(),
            'description'   => $report->getDescription(),
            'project'       => $report->getProjectId(),
            'sql_query'     => $report->getSqlQuery(),
            'idvalue'       => $id,
            'remark'        => $report->getRemark(),
            'category'      => $report->getCategory(),
            'csv_delimiter' => $report->getCsvDelimiter(),
            'csv_enclosure' => $report->getCsvEnclosure(),
            'readonly'      => (int)$report->isReadonly(),
        ];

        $this->db->beginTransaction();
        try {
            $this->db->perform($sql, $values);
            $columns = $report->getColumns();
            if ($columns !== null && $columns !== []) {
                $this->saveColumnCollection($columns, $id);
            }
            $params = $report->getParameters();
            if ($params !== null && $params !== []) {
                $this->saveParameterCollection($params, $id);
            }
        } catch (Exception $e) {
            $this->db->rollBack();
            throw new DatabaseTransactionException($e->getMessage(), $e->getCode(), $e);
        }
        $this->db->commit();

        return $id;
    }

    /**
     * @param ReportColumn $column
     * @param int          $reportId
     *
     * @return int
     */
    private function insertColumn(ReportColumn $column, $reportId)
    {
        $sql = 'INSERT INTO report_column 
                (report_id, key_name, title, width, alignment, sorting, sum, sequence, format_type, format_statement)
                VALUES (:reportId, :keyName, :title, :width, :alignment, :sorting, :sum, :sequence,
                        :format_type, :format_statement)';
        $sum = 0;
        if ($column->isSumColumn()) {
            $sum = 1;
        }
        $values = [
            'reportId'  => $reportId,
            'keyName'   => $column->getKey(),
            'title'     => $column->getTitle(),
            'width'     => $column->getWidth(),
            'alignment' => $column->getAlignment(),
            'sorting'   => $column->getSorting(),
            'sum'       => $sum,
            'sequence'  => $column->getSequence(),
            'format_type' => $column->getFormatType(),
            'format_statement' => $column->getFormatStatement(),
        ];
        $this->db->perform($sql, $values);

        return $this->db->lastInsertId();
    }

    /**
     * @param ReportColumn $column
     * @param int          $reportId
     *
     * @return int
     */
    private function updateColumn(ReportColumn $column, $reportId)
    {
        $id = $column->getId();
        $sql = 'UPDATE report_column 
                SET report_id = :reportId, key_name = :keyName, title = :title, width = :width, alignment = :alignment,
                    sorting = :sorting, sum = :sum, sequence = :sequence, format_type = :format_type,
                    format_statement = :format_statement
                WHERE id = :idvalue';
        $sum = 0;
        if ($column->isSumColumn()) {
            $sum = 1;
        }
        $values = [
            'reportId'  => $reportId,
            'keyName'   => $column->getKey(),
            'title'     => $column->getTitle(),
            'width'     => $column->getWidth(),
            'alignment' => $column->getAlignment(),
            'sorting'   => $column->getSorting(),
            'sum'       => $sum,
            'sequence'  => $column->getSequence(),
            'format_type' => $column->getFormatType(),
            'format_statement' => $column->getFormatStatement(),
            'idvalue' => $id,
        ];
        $this->db->perform($sql, $values);

        return $id;
    }

    /**
     * @param ReportParameter $parameter
     * @param int             $reportId
     *
     * @return int
     */
    private function insertParameter(ReportParameter $parameter, $reportId)
    {
        $sql = 'INSERT INTO report_parameter
                (report_id, varname, displayname, default_value, options, description, editable, control_type)
                VALUES 
                (:reportId, :varName, :displayName, :defaultValue, :options, :description, :editable, :controlType)';
        $editable = 0;
        if ($parameter->isEditable()) {
            $editable = 1;
        }
        $values = [
            'reportId'     => $reportId,
            'varName'      => $parameter->getVarname(),
            'displayName'  => $parameter->getDisplayname(),
            'defaultValue' => $parameter->getDefaultValue(),
            'options'      => $parameter->getOptionsAsString(),
            'description'  => $parameter->getDescription(),
            'editable'     => $editable,
            'controlType'  => $parameter->getControlType(),
        ];
        $this->db->perform($sql, $values);

        return $this->db->lastInsertId();
    }

    /**
     * @param ReportParameter $parameter
     * @param int             $reportId
     *
     * @return int
     */
    private function updateParameter(ReportParameter $parameter, $reportId)
    {
        $id = $parameter->getId();
        $sql = 'UPDATE report_parameter
                SET report_id=:reportId, varname=:varName, displayname=:displayName, default_value=:defaultValue,
                    options=:options, description=:description, editable=:editable, control_type=:controlType
                WHERE id=:idvalue';
        $editable = 0;
        if ($parameter->isEditable()) {
            $editable = 1;
        }
        $values = [
            'reportId'     => $reportId,
            'varName'      => $parameter->getVarname(),
            'displayName'  => $parameter->getDisplayname(),
            'defaultValue' => $parameter->getDefaultValue(),
            'options'      => $parameter->getOptionsAsString(),
            'description'  => $parameter->getDescription(),
            'editable'     => $editable,
            'idvalue'      => $id,
            'controlType'  => $parameter->getControlType(),
        ];
        $this->db->perform($sql, $values);

        return $id;
    }

    /**
     * @param $transferArray
     *
     * @return array
     */
    private function sanitizeTransferDateTimes($transferArray)
    {
        $dateTimeKeys = [
            'ftp_daytime',
            'ftp_last_transfer',
            'email_daytime',
            'email_last_transfer',
            'url_begin',
            'url_end',
        ];
        foreach ($dateTimeKeys as $key) {
            if (isset($transferArray[$key]) && empty($transferArray[$key])) {
                $transferArray[$key] = null;
            }
        }
        if (isset($transferArray['email_daytime']) && $transferArray['email_daytime'] !== null) {
            $transferArray['email_daytime'] = sprintf('%s:00', $transferArray['email_daytime']);
        }
        if (isset($transferArray['ftp_daytime']) && $transferArray['ftp_daytime'] !== null) {
            $transferArray['ftp_daytime'] = sprintf('%s:00', $transferArray['ftp_daytime']);
        }

        return $transferArray;
    }

    /**
     * @param array $transferArray
     *
     * @return int
     */
    private function insertTransferArray($transferArray)
    {
        $sql = "INSERT INTO `report_transfer` (report_id, ftp_active, ftp_passive, ftp_type, ftp_host, ftp_port, ftp_user,
                               ftp_password, ftp_interval_mode, ftp_interval_value, ftp_daytime, ftp_format,
                               ftp_filename, email_active, email_recipient, email_subject, email_interval_mode,
                               email_interval_value, email_daytime, email_format, email_filename, url_format, url_begin, url_end,
                               url_address, api_active, api_account_id, api_format, url_token) 
                VALUES (:report_id, :ftp_active, :ftp_passive, :ftp_type, :ftp_host, :ftp_port, :ftp_user, :ftp_password,
                        :ftp_interval_mode, :ftp_interval_value, :ftp_daytime, :ftp_format, :ftp_filename,
                        :email_active, :email_recipient, :email_subject, :email_interval_mode, :email_interval_value,
                        :email_daytime, :email_format, :email_filename, :url_format, :url_begin, :url_end, :url_address, :api_active,
                        :api_account_id, :api_format, '')";
        $values = $this->sanitizeTransferDateTimes($transferArray);
        $this->db->fetchAffected($sql, $values);

        return $this->db->lastInsertId();
    }

    /**
     * @param array $transferArray
     *
     * @return int
     */
    private function updateTransferArray($transferArray)
    {
        $sql = 'UPDATE `report_transfer` SET 
                    report_id = :report_id,
                    ftp_active = :ftp_active,
                    ftp_passive = :ftp_passive,
                    ftp_type = :ftp_type,
                    ftp_host = :ftp_host,
                    ftp_port = :ftp_port,
                    ftp_user = :ftp_user,
                    ftp_password = :ftp_password,
                    ftp_interval_mode = :ftp_interval_mode,
                    ftp_interval_value = :ftp_interval_value, 
                    ftp_daytime = :ftp_daytime, 
                    ftp_format = :ftp_format,
                    ftp_filename = :ftp_filename,
                    email_active = :email_active,
                    email_recipient = :email_recipient,
                    email_subject = :email_subject,
                    email_interval_mode = :email_interval_mode,
                    email_interval_value = :email_interval_value,
                    email_daytime = :email_daytime,
                    email_format = :email_format,
                    email_filename = :email_filename,
                    url_format = :url_format,
                    url_begin = :url_begin, 
                    url_end = :url_end,
                    url_address = :url_address,
                    api_active = :api_active,
                    api_account_id = :api_account_id, 
                    api_format = :api_format
                    WHERE id=:id';
        $values = $this->sanitizeTransferDateTimes($transferArray);
        $affectedRows = $this->db->fetchAffected($sql, $values);
        if ($affectedRows > 0) {
            return (int)$values['id'];
        }

        return 0;
    }

    /**
     * @param array $reportUserArray
     *
     * @return int
     */
    private function insertReportUserArray($reportUserArray)
    {
        $sql = 'INSERT INTO `report_user` (report_id, user_id, name, chart_enabled, file_enabled,
                                            menu_enabled, tab_enabled)
                                VALUES (:report_id, :user_id, :name, :chart_enabled, :file_enabled,
                                            :menu_enabled, :tab_enabled)';
        $this->db->fetchAffected($sql, $reportUserArray);

        return $this->db->lastInsertId();
    }

    /**
     * @param array $reportUserArray
     *
     * @return int
     */
    private function updateReportUserArray($reportUserArray)
    {
        $username = $reportUserArray['name'];
        unset($reportUserArray['name']);
        $reportUserArray['userName'] = $username;
        $sql = 'UPDATE `report_user` SET
                    report_id = :report_id,
                    user_id = :user_id,
                         name = :userName,
                         chart_enabled = :chart_enabled,
                         file_enabled = :file_enabled,
                         menu_enabled = :menu_enabled,
                         tab_enabled = :tab_enabled
                WHERE id = :id';
        $affectedRows = $this->db->fetchAffected($sql, $reportUserArray);
        if ($affectedRows > 0) {
            return (int)$reportUserArray['id'];
        }

        return 0;
    }

    /**
     * @param array $shareArray
     *
     * @return int
     */
    private function insertShareArray($shareArray)
    {
        $sql = 'INSERT INTO `report_share` (report_id, chart_public, chart_axislabel, chart_dateformat, chart_type,
                            chart_interval_value, chart_interval_mode, file_public, file_pdf_enabled, file_csv_enabled,
                            file_xls_enabled, menu_public, menu_doctype, menu_label, menu_format, tab_public,
                            tab_module, tab_label, tab_position, tab_action, 
                            chart_x_column, data_columns,chart_group_column) 
                VALUES (:report_id, :chart_public, :chart_axislabel, :chart_dateformat, :chart_type,
                            :chart_interval_value, :chart_interval_mode, :file_public, :file_pdf_enabled, :file_csv_enabled,
                            :file_xls_enabled, :menu_public, :menu_doctype, :menu_label, :menu_format, :tab_public,
                            :tab_module, :tab_label, :tab_position, :tab_action,
                            :chart_x_column, :data_columns,:chart_group_column)';
        $values = $this->sanitizeTransferDateTimes($shareArray);
        $this->db->fetchAffected($sql, $values);

        return $this->db->lastInsertId();
    }

    /**
     * @param array $shareArray
     *
     * @return int
     */
    private function updateShareArray($shareArray)
    {
        $sql = 'UPDATE `report_share` SET 
                    report_id = :report_id,     
                    chart_public = :chart_public,
                    chart_axislabel = :chart_axislabel,
                    chart_dateformat = :chart_dateformat,
                    chart_type = :chart_type,
                    chart_x_column = :chart_x_column,
                    data_columns = :data_columns,
                    chart_group_column = :chart_group_column,
                    chart_interval_value = :chart_interval_value,
                    chart_interval_mode = :chart_interval_mode,
                    file_public = :file_public,
                    file_pdf_enabled = :file_pdf_enabled,
                    file_csv_enabled = :file_csv_enabled,
                    file_xls_enabled = :file_xls_enabled,
                    menu_public = :menu_public,
                    menu_doctype = :menu_doctype,
                    menu_label = :menu_label,
                    menu_format = :menu_format,
                    tab_public = :tab_public,
                    tab_module = :tab_module,
                    tab_action = :tab_action,
                    tab_label = :tab_label,
                    tab_position = :tab_position
                    WHERE id=:id';
        $values = $shareArray;
        $affectedRows = $this->db->fetchAffected($sql, $values);
        if ($affectedRows > 0) {
            return (int)$values['id'];
        }

        return 0;
    }

    /**
     * Finds copies of the specified name.
     *
     * @param string $name
     *
     * @param int    $ignoreId
     *
     * @return array
     */
    private function findReportCopyNames($name, $ignoreId = 0)
    {
        $sql = 'SELECT r.id, r.name FROM `report` AS `r` 
                WHERE (r.name LIKE :origName 
                   OR r.name LIKE :copyFormat1
                   OR r.name LIKE :copyFormat2)
                   AND r.id <> :ignoreId';
        $values = [
            'origName'    => $name,
            'copyFormat1' => sprintf('%s(%%)', $name),
            'copyFormat2' => sprintf('%s (%%)', $name),
            'ignoreId'    => $ignoreId,
        ];
        $resultArray = $this->db->fetchPairs($sql, $values);
        if (empty($resultArray)) {
            return [];
        }

        return $resultArray;
    }

    /**
     * @param $reportName
     *
     * @return mixed
     */
    private function getBaseName($reportName)
    {
        $baseName = $reportName;
        $isCopyName = preg_match('/^(.+\S)\s*\((\d+)\)$/', $reportName, $copyNameParts);
        if ($isCopyName === 1 && count($copyNameParts) > 1) {
            $baseName = $copyNameParts[1];
        }

        return $baseName;
    }
}