<?php declare(strict_types=1);

require_once __DIR__ . '/../QrbStorage.php';

require_once __DIR__ . '/QrbFilePaths.php';
require_once __DIR__ . '/QrbLogFile.php';
require_once __DIR__ . '/QrbProtocolFile.php';

require_once __DIR__ . '/../../common/QrbDataType.php';
require_once __DIR__ . '/../../common/QrbDateTime.php';
require_once __DIR__ . '/../../common/QrbEnvironment.php';
require_once __DIR__ . '/../../common/QrbException.php';
require_once __DIR__ . '/../../common/QrbLockedException.php';
require_once __DIR__ . '/../../common/QrbFiles.php';

require_once __DIR__ . '/../../data/QrbProfileConfiguration.php';

class QrbFileStorage implements QrbStorage
{
    private $currentProfileLockFileHandle;
    
    private ?QrbLogFile      $qrbLogFile      = NULL;

    // environment read by the constructor
    private QrbEnvironment   $qrbEnv;
    private ?string          $runName = NULL;
    private bool             $testRun;
    private bool             $dryRun;
    
    // Creates a QrbStorage instance and reads the environment.
    public function __construct()
    {
        $this->qrbEnv = $this->readEnvironment();   // cache environment
    }


    //////////////////////////////////////////////////////////////////////////////////////
    // Environment

    // Reads the environment.
    private function readEnvironment() : QrbEnvironment
    {
        $envJson = QrbFiles::readJsonFile(QrbFilePaths::getEnvironmentFullFileName());
        return QrbEnvironment::fromJson($envJson);;
    }

    // Stores the given environment.
    //
    // Throws QrbException if store fails.
    private function storeEnvironment(QrbEnvironment $qrbEnv) : void
    {
        $this->qrbEnv = $qrbEnv;    // update cache
        $envJson = QrbEnvironment::toJson($qrbEnv);
        QrbFiles::writeJsonFile(QrbFilePaths::getEnvironmentFullFileName(), $envJson);
    }
    
    // Sets maximum number of runs to keep.
    //
    // maxRunsToKeep: a positive number
    //
    // Throws QrbException if store fails.
    public function setMaxRunsToKeep(int $maxRunsToKeep) : void
    {
        if ($maxRunsToKeep < 1)
            throw new QrbException(static::class . " - Invalid value (< 1), value is: {$maxRunsToKeep}",
                                   "exception.invalidArgument",
                                   "{$maxRunsToKeep}");
            
        $this->qrbEnv->setMaxRunsToKeep($maxRunsToKeep);
        $this->storeEnvironment($this->qrbEnv);
    }

    // Returns maximum number of runs to keep.
    //
    // Returns a positive number.
    public function getMaxRunsToKeep() : int
    {
        return $this->qrbEnv->getMaxRunsToKeep();
    }


    //////////////////////////////////////////////////////////////////////////////////////
    // Log
    
    // Writes the given message and optionally the additional lines to the log file.
    public function writeLog(string $message, ?array $additionaLines = NULL) : int|false
    {
        if (!$this->qrbLogFile)
        {
            $this->qrbLogFile = new QrbLogFile(QrbFilePaths::getDataPath());
        }
        
        return $this->qrbLogFile->write($message, $additionaLines);
    }


    //////////////////////////////////////////////////////////////////////////////////////
    // Lock handling for current profile
    
    // Tries to get a lock for the current profile.
    //
    // Throws a QrbLockException if the lock can't be acquired.
    public function lockCurrentProfile()
    {
        $this->lockProfile($this->qrbEnv->getCurrentProfile());
    }
    
    // Tries to get a lock for the given profile.
    //
    // $profile: profile to lock
    //
    // Throws a QrbLockException if the lock can't be acquired.
    private function lockProfile(String $profile)
    {
        $this->writeLog("Acquiring lock for profile '{$profile}'");
        
        $profilePath = QrbFilePaths::getProfilePath($profile);
        $profileLockFileName = $profilePath . "/.lock";
        $this->currentProfileLockFileHandle = fopen($profileLockFileName, "c");
        
        if ($this->currentProfileLockFileHandle === false)
            throw new QrbLockedException(static::class . " - Unable to create lock file",
                                         "exception.lock.unableToAcquireLock",
                                         $profile);
        
        if (!flock($this->currentProfileLockFileHandle, LOCK_EX | LOCK_NB))
            throw new QrbLockedException(static::class . " - Unable to acquire lock",
                                         "exception.lock.unableToAcquireLock",
                                         $profile);
        
        if (!ftruncate($this->currentProfileLockFileHandle, 0))
            throw new QrbLockedException(static::class . " - Unable to truncate lock file",
                                         "exception.lock.unableToAcquireLock",
                                         $profile);
        
        if (fwrite($this->currentProfileLockFileHandle, "Lock acquired\n") === false)
            throw new QrbLockedException(static::class . " - Unable to write to lock file",
                                         "exception.lock.unableToAcquireLock",
                                         $profile);
        
        if (!fflush($this->currentProfileLockFileHandle))
            throw new QrbLockedException(static::class . " - Unable to flush lock file",
                                         "exception.lock.unableToAcquireLock",
                                         $profile);
    }
    
    // Unlocks the current profile.
    public function unlockCurrentProfile()
    {
        $this->unlockProfile($this->qrbEnv->getCurrentProfile());
    }
    
    // Unlocks the currently locked profile (that should be the given profile).
    //
    // $profile: profile to unlock
    private function unlockProfile(string $profile)
    {
        if (fwrite($this->currentProfileLockFileHandle, "Lock released\n") !== false)
        {
            fflush($this->currentProfileLockFileHandle);
        }

        if (!flock($this->currentProfileLockFileHandle, LOCK_UN))
            error_log("QrbFileStorage: Unable to unlock lock file for profile '{$profile}'");
        
        if (!fclose($this->currentProfileLockFileHandle))
            error_log("QrbFileStorage: Unable to close lock file for profile '{$profile}'");
        
        $this->writeLog("Lock released for profile '{$profile}'");
    }
    
    
    //////////////////////////////////////////////////////////////////////////////////////
    // Profiles
    
    // Returns all profiles stored in the data path.
    public function getProfiles() : array // of string
    {
        $profiles = array();
        
        // scan directory
        $dataPath = QrbFilePaths::getDataPath();
        $entries = scandir($dataPath);
        
        if ($entries === false)
        {
            error_log("QrbFileStorage: Unable to scan dir '{$dataPath}'");
            return $profiles;
        }
        
        foreach ($entries as $entry)
        {
            // suppress dot entries ('.' and '..') and ensure entry is a (sub) directory
            if (   ($entry != ".") 
                && ($entry != "..")
                && is_dir($dataPath . "/" . $entry)
               ) 
            {
                array_push($profiles, $entry);
            }
        }
        
        return $profiles;
    }

    // Returns the current profile.
    public function getCurrentProfile() : string
    {
        return $this->qrbEnv->getCurrentProfile(); 
    }
    
    // Checks whether the given profile exists.    
    public function doesProfileExist(string $profile) : bool
    {
        $profileExists = false;
        $existingProfiles = $this->getProfiles();
        
        foreach ($existingProfiles as $existingProfile)
        {
            if ($existingProfile === $profile)
            {
                $profileExists = true;
                break;
            }
        }
        
        return $profileExists;        
    }
    
    // Makes the given profile the current one.
    //
    // profile: profile to select (one of the profiles returned by getCurrentProfile())
    //
    // Returns false if the given profile does not exist.
    //
    // Throws QrbException if storing selected profile fails.
    // Throws a QrbLockException if the lock can't be acquired.
    public function selectCurrentProfile(string $profile) : bool
    {
        if (!$this->doesProfileExist($profile))
        {
            $this->writeLog("Selecting profile '{$profile}' not possible as it doesn't exist.");
            return false;
        }
        
        $this->lockProfile($profile);
        
        try
        {
            $this->writeLog("Setting profile '{$profile}' into environment");
            $this->qrbEnv->setCurrentProfile($profile);
            $this->storeEnvironment($this->qrbEnv);
            $this->writeLog("Environment after write: {$this->qrbEnv}");
            return true;
        }
        finally
        {
            $this->unlockProfile($profile);
        }
    }
    
    // Creates a new profile and makes it the current one.
    //
    // profile: new profile to create
    //
    // Returns false if the given profile does already exist.
    //
    // Throws QrbException if storing new profile fails.
    public function createProfile(string $profile) : bool
    {
        if ($this->doesProfileExist($profile))
        {
            $this->writeLog("Creating profile '{$profile}' not possible as it already exists.");
            return false;
        }
        
        $this->writeLog("Setting profile '{$profile}' into environment");
        $this->qrbEnv->setCurrentProfile($profile);
        $this->storeEnvironment($this->qrbEnv);
        $this->writeLog("Environment after write: {$this->qrbEnv}");
        return true;
    }
    
    // Deletes the profile with the given name and makes the default profile the current one.
    //
    // Returns false if the given profile is the default profile or is an empty string.
    //
    // Throws QrbException if storing default profile or deleting old profile fails.
    public function deleteProfile(string $profile) : bool
    {
        if ($profile === QrbEnvironment::DEFAULT_PROFILE)
        {
            $this->writeLog("Deleting profile '{$profile}' not possible as it is the default profile.");
            return false;
        }
        
        if ($profile === "")
        {
            $this->writeLog("Deleting profile '' not possible as it is an empty string.");
            return false;
        }
        
        $this->writeLog("Setting profile 'Default' into environment");
        $this->qrbEnv->setCurrentProfile(QrbEnvironment::DEFAULT_PROFILE);
        $this->storeEnvironment($this->qrbEnv);
        $this->writeLog("Environment after write: {$this->qrbEnv}");
    
        $profilePath = QrbFilePaths::getDataPath() . "/" . $profile;

        if (!file_exists($profilePath))
        {
            return true;
        }

        if (! QrbFiles::deletePath($profilePath, true) )
            throw new QrbException(static::class . " - Could not delete path '{$profilePath}'.",
                                   "exception.files.unableToDeletePath",
                                   $profilePath);
        return true;
    }


    //////////////////////////////////////////////////////////////////////////////////////
    // Configuration
    
    // Reads the configuration of the current profile.
    // If the profile configuration is not found, creates a new instance and persists it.
    //
    // Throws QrbException if storing the new instance fails.
    public function getProfileConfiguration() : QrbProfileConfiguration
    {
        // Try to read JSON file; this will fail if the user has chosen to create a new profile.
        try
        {
            $cfgFullFileName = QrbFilePaths::getConfigurationFullFileName($this->qrbEnv->getCurrentProfile());
            $cfgJson = QrbFiles::readJsonFile($cfgFullFileName);
            $qrbCfg = QrbProfileConfiguration::fromJson($cfgJson);
        }
        catch (\Throwable $th)
        {
            // create and store new profile configuration
            $qrbCfg = new QrbProfileConfiguration();
            $this->storeProfileConfiguration($qrbCfg);
        }
        
        return $qrbCfg;
    }
    
    // Stores the given configuration as the configuration of the current profile.    
    //
    // Throws QrbException if store fails.
    public function storeProfileConfiguration(QrbProfileConfiguration $qrbCfg) : void
    {
        $cfgFullFileName = QrbFilePaths::getConfigurationFullFileName($this->qrbEnv->getCurrentProfile());
        QrbFiles::writeJsonFile($cfgFullFileName, QrbProfileConfiguration::toJson($qrbCfg));
    }
    
    
    //////////////////////////////////////////////////////////////////////////////////////
    // input data
    
    // Deletes input data of the current profile, but not the profile itself. 
    //
    // Returns false if delete fails.
    public function deleteInputData() : bool
    {
        return QrbFiles::deletePath(QrbFilePaths::getInputPath($this->qrbEnv->getCurrentProfile()), false);
    }

    // Checks if the given input data exists and if so returns the relative path to the file.
    //
    // Returns NULL if file does not exist.
    //
    // public for unit test
    public function checkInputFileExists(string $inputDataName) : ?string
    {
        if (file_exists(QrbFilePaths::getInputPath($this->qrbEnv->getCurrentProfile()) . "/" . $inputDataName))
        {
            return QrbFilePaths::getRelativeInputPath($this->qrbEnv->getCurrentProfile()) . "/" . $inputDataName;
        }
        
        return NULL;
    }
    
    // Checks if the given input data exists and if so returns the relative path to the file.
    //
    // $qrbDataType: QrbDataType
    // language:     one of "de", "fr", "it", "en" or "" (no or default language)
    //
    // Returns NULL if file does not exist.
    public function checkInputDataExists(QrbDataType $qrbDataType, string $language) : ?string
    {
        return $this->checkInputFileExists($this->getFileName($qrbDataType, $language));
    }
    
    // Stores the given input data.
    //
    // tmpFileName:  name as stored by PHP upload form
    // $qrbDataType: QrbDataType
    // language:     one of "de", "fr", "it", "en" or "" (no or default language)
    //
    // Throws QrbException if storing fails.
    public function storeInputData(string $tmpFileName, QrbDataType $qrbDataType, string $language) : void
    {
        $targetFileFullName = $this->getPath($qrbDataType) . "/". $this->getFileName($qrbDataType, $language);
        
        if (!move_uploaded_file($tmpFileName, $targetFileFullName))
        {
            // probably never called, as move_uploaded_file() throws anyway?
            throw new QrbException(static::class . " - There was an error moving the uploaded file to '{$targetFileFullName}'.",
                                   "exception.upload.errorMovingFile",
                                   $targetFileFullName);
        }
    }

    // Reads PDF data and template from storage.
    //
    // Returns an instance of QrbPdfData.
    //
    // Throws QrbException if read fails.
    public function readPdfData(array $additionalLanguages) : QrbPdfData
    {
        // Read PDF template file(s)
        $pdfTemplateFileName = $this->getFullFileName(QrbDataType::PDF_TEMPLATE);
        $pdfTemplate = 
            QrbFiles::readMultiLanguageText($pdfTemplateFileName, 
                                            QrbEnvironment::getSupportedLanguages(),
                                            $additionalLanguages);
        // Read JSON file
        $pdfDataFileName = $this->getFullFileName(QrbDataType::PDF_DATA);
        $pdfDataJson = QrbFiles::readJsonFile($pdfDataFileName);
        // Create PDF data
        return new QrbPdfData($pdfTemplate, 
                              $pdfDataJson, 
                              QrbEnvironment::getSupportedLanguages(),
                              $additionalLanguages);
    }
    
    // Reads mail data and body from storage.
    //
    // Returns an instance of QrbMailData.
    //
    // Throws QrbException if read fails.
    public function readMailData(array $additionalLanguages, bool $hasAttachment) : QrbMailData
    {
        // Read mail body file(s)
        $mailBodyFileName = $this->getFullFileName(QrbDataType::MAIL_BODY);
        $mailBody = 
            QrbFiles::readMultiLanguageText($mailBodyFileName, 
                                            QrbEnvironment::getSupportedLanguages(),
                                            $additionalLanguages);
        // Read JSON file
        $mailDataFileName = $this->getFullFileName(QrbDataType::MAIL_DATA);
        $mailDataJson = QrbFiles::readJsonFile($mailDataFileName);
        // Create mail data
        return new QrbMailData($mailDataJson, 
                               $mailBody, 
                               $hasAttachment,
                               QrbEnvironment::getSupportedLanguages(),
                               $additionalLanguages);
    }

    // Reads creditor information from storaage.    
    //
    // Returns an instance of QrbCreditorInformation.
    //
    // Throws QrbException if read fails.
    public function readCredInfo() : QrbCreditorInformation
    {
        // Read JSON file
        $credInfoFileName = $this->getFullFileName(QrbDataType::CRED_INFO);
        $credInfoJson = QrbFiles::readJsonFile($credInfoFileName);
        // Create creditor information
        return new QrbCreditorInformation($credInfoJson);
    }
    
    // Reads (optional) additional information.
    //
    // Returns an instance of QrbAdditionalInformation or NULL if there is no additional information.
    //
    // Throws QrbException if read fails.
    public function readAddInfo(array $additionalLanguages) : ?QrbAdditionalInformation
    {
        $addInfoFileName = $this->getFullFileName(QrbDataType::ADD_INFO);
        
        if (!file_exists($addInfoFileName))
        {
            $this->writeLog("Optional AdditionalInformation is not present.");
            return NULL;
        }

        // Read JSON file
        $addInfoJson = QrbFiles::readJsonFile($addInfoFileName);
        // Create additional information
        return new QrbAdditionalInformation($addInfoJson, 
                                            QrbEnvironment::getSupportedLanguages(),
                                            $additionalLanguages);
    }

    // Read recipients.
    // For more details on the checks, see QrbRecipient.
    //
    // qrbDataType:        either QrbDataType::RECIPIENTS or QrbDataType::TEST_RCPNTS
    // amountMandatory:    if true checks that all recipients have an amount
    // createPdf:          if true checks that all recipients have the fields required to create a PDF
    // referenceMandatory: if true checks that all recipients have a reference
    // sendMail:           if true checks that all recipients have an e-mail address
    //
    // Returns a list of QrbRecipient.
    //
    // Throws QrbException if read fails.
    public function readRecipients(QrbDataType $qrbDataType, 
                                   bool        $amountMandatory, 
                                   bool        $createPdf, 
                                   bool        $referenceMandatory, 
                                   bool        $sendMail) : array // of QrbRecipient
    {
        // Read CSV with recipients data
        $recipientsFileName = $this->getFullFileName($qrbDataType);
        $recipientsData     = QrbFiles::readCsvFile($recipientsFileName);
        
        $supportedLanguages = QrbEnvironment::getSupportedLanguages();
        
        $recipients         = array();
        $recipientIndex     = 0;
        
        foreach ($recipientsData as $recipientLine)
        {
            if ($recipientIndex == 0)
            {
                // read header names
                $headerNames = 
                    QrbRecipient::parseHeader($recipientLine, 
                                                 $amountMandatory, $createPdf, 
                                                 $referenceMandatory, $sendMail);
            }
            else
            {
                // create recipient and append it to output
                $recipient = 
                    new QrbRecipient($recipientIndex, $recipientLine, $headerNames,
                                        $amountMandatory, $createPdf, 
                                        $referenceMandatory, $sendMail, 
                                        $supportedLanguages);
                array_push($recipients, $recipient);
            }
            
            $recipientIndex++;
        }

        return $recipients;
    }

    // Stores the given PDF with the given name.
    //
    // Throws QrbException if store fails.
    public function storeQrBillPdf(string $pdfOutputName, string $pdfOutput) : void
    {
        $pdfOutputFullFileName = "{$this->getPath(QrbDataType::PDF_OUTPUT)}/{$pdfOutputName}.pdf";
        QrbFiles::writeTextFile($pdfOutputFullFileName, $pdfOutput);
    }


    //////////////////////////////////////////////////////////////////////////////////////
    // processing
    
    // Initializes a new run depending on the profile and the given name of the run.
    //
    // runName: name of the run
    // testRun: if true then the output is marked as beeing created by a test run 
    //          (only the last test run is preserved, see deleteOutputOfOldRuns())
    // dryRun:  if true then no output directory is created
    //
    // Throws QrbException if initialization fails somehow.
    public function initializeNewRun(string $runName, bool $testRun, bool $dryRun) : void
    {
        $this->runName = $runName;
        $this->testRun = $testRun;
        $this->dryRun  = $dryRun;
        $this->writeLog("Current run = '{$this->runName}', test run = {$this->testRun}, dry run = {$this->dryRun}.");
        $this->writeLog("Environment: {$this->qrbEnv}");

        if ($this->dryRun) return;  // do not create output directory if dryRun
                
        $directoryName = $this->getPath(QrbDataType::PDF_OUTPUT);

        if (!file_exists($directoryName) && (mkdir($directoryName, 0777, true) === false))
            throw new QrbException(static::class . " - Failed to create output directory {$directoryName}",
                                   "exception.storage.createDirectoryFailed",
                                   "{$directoryName}");

        if ($this->testRun)
            // write empty marker file
            QrbFiles::writeTextFile($this->getFullFileName(QrbDataType::MARKER_TEST),
                                    "");
    }
    
    // Writes the given protocol entries to disk.
    //
    // protocol: array of QrbProtocolEntry
    //
    // Returns a string containing the relative path to the current protocol
    // or NULL in case the protocol has not been stored (empty or dryRun).
    //
    // Throws QrbException if store fails.
    public function writeProtocol(array $protocol) : ?string
    {
        if ($this->dryRun) return NULL; // do not store protocol if dryRun
            
        $qrbProtocolFile = new QrbProtocolFile();
        
        if (!$qrbProtocolFile->write($this->getFullFileName(QrbDataType::PROTOCOL), $protocol))
            return NULL;
            
        return QrbFilePaths::getRelativeOutputPathOfRun($this->qrbEnv->getCurrentProfile(), 
                                                        $this->runName) 
               . "/" . $this->getFileName(QrbDataType::PROTOCOL);
    }
    
    // Zips the result of the current run and returns a link to the created ZIP file.
    //
    // Returns a string containing the relative path to the created ZIP archive
    //         or NULL if there were no files to zip.
    //
    // Throws QrbException if creating the ZIP archive fails.
    public function zipCurrentRun() : ?string
    {
        if ($this->dryRun) return NULL; // do not create ZIP archive if dryRun
                
        $outputDirectory = $this->getPath(QrbDataType::PDF_OUTPUT);
        
        if (!file_exists($outputDirectory))
        {
            $this->writeLog("Output directory '{$outputDirectory}' does not exist (dryRun?).");
            return NULL;
        }
        
        $zipFileName     = "{$this->runName}.zip";
        $fullZipFileName = "{$outputDirectory}/{$zipFileName}"; 
        $timestamp       = QrbDateTime::createHumanReadableTimeStamp(new DateTimeImmutable());
        
        $zip = new ZipArchive;
        $res = $zip->open($fullZipFileName, ZipArchive::CREATE | ZipArchive::OVERWRITE);
        if (!$res)
        {
            throw new QrbException(static::class . " - Open of ZIP archive failed (error = {$res})",
                                   "exception.storage.openZipFailed",
                                   "{$fullZipFileName}");
        }
        
        if (!$zip->setArchiveComment("zipped on " . $timestamp))
        {
            $this->writeLog("WARNING: Adding a comment to ZIP archive failed.");
            // ignore and go on
        }
        
        $protocolFileName = $this->getFileName(QrbDataType::PROTOCOL);
        
        foreach (scandir($outputDirectory) as $fileName)
        { 
            // zip PDFs and protocol
            if (  (pathinfo($fileName, PATHINFO_EXTENSION) == "pdf") 
               || ($fileName == $protocolFileName) )
            {
                if (!$zip->addFile("{$outputDirectory}/{$fileName}", $fileName))
                {
                    throw new QrbException(static::class . " - Adding a file to the ZIP archive failed",
                                           "exception.storage.addingFileToZipFailed",
                                           "{$fileName}");
                }
            }
            else
            {
                $this->writeLog("Ignoring file '{$fileName}', not adding to the ZIP archive.");
            }
        }

        $noOfFiles = $zip->numFiles;
        
        if (!$zip->close())
        {
            throw new QrbException(static::class . " - Closing of ZIP archive failed",
                                   "exception.storage.closeZipFailed",
                                   "{$fullZipFileName}");
        }
        
        if ($noOfFiles > 0)
        {
            $this->writeLog("ZIP file '{$zipFileName}' contains {$noOfFiles} entries");
            return QrbFilePaths::getRelativeOutputPathOfRun($this->qrbEnv->getCurrentProfile(), 
                                                            $this->runName)
                   . "/{$zipFileName}";
        }
        else
        {
            $this->writeLog("WARNING: Deleting ZIP file '{$zipFileName}' as it contains 0 entries");
            if (file_exists($fullZipFileName) && !unlink($fullZipFileName))
                $this->writeLog("ERROR: Deleting empty ZIP archive failed.");
            return NULL;
        }
    }

    // Deletes output files of old runs.
    public function deleteOutputOfOldRuns() : void
    {
        $currentRunOutputPath = $this->getPath(QrbDataType::PDF_OUTPUT);
        $profiles = $this->getProfiles();
        
        foreach ($profiles as $profile)
        {
            $profileOutputPath = QrbFilePaths::getOutputPath($profile);
            $testRunOfCurrentProfile = false;

            // delete old test runs
            $profileOutputSubPaths = $this->getSubDirs($profileOutputPath);
            
            foreach ($profileOutputSubPaths as $profileOutputSubPath)
            {
                $testRunOfCurrentProfile = 
                    $this->testRun && ($profileOutputSubPath == $currentRunOutputPath);

                if  (  file_exists($profileOutputSubPath . "/" . $this->getFileName(QrbDataType::MARKER_TEST)) 
                    && !$testRunOfCurrentProfile ) 
                {
                    // delete old test run directory
                    $this->writeLog("Deleting old test run: {$profileOutputSubPath}");
                    QrbFiles::deletePath($profileOutputSubPath, true);
                }
            }
            
            // delete other old runs
            $profileOutputSubPaths = $this->getSubDirs($profileOutputPath);
            sort($profileOutputSubPaths);
            
            $noOfOutputDirs = count($profileOutputSubPaths);
            $maxFilesToKeep = $this->qrbEnv->getMaxRunsToKeep() + ($testRunOfCurrentProfile ? 1 : 0);

            while ( $noOfOutputDirs-- > $maxFilesToKeep )
            {
                $profileOutputSubPath = array_shift($profileOutputSubPaths);
                $this->writeLog("Deleting old run: {$profileOutputSubPath}");
                QrbFiles::deletePath($profileOutputSubPath, true);
            }
        }
    }


    //////////////////////////////////////////////////////////////////////////////////////
    // helpers

    // Deletes all the data (for clean up in unit tests).
    public function deleteData() : void
    {
        QrbFiles::deletePath(QrbFilePaths::getDataPath(), true);
    }

    // Returns an array of full file names (including path) od the sub directories of the given directory.
    private function getSubDirs(string $directory) : array
    {
        $result = array();
        
        $fileNames = scandir($directory);
        
        foreach($fileNames as $fileName)
        {
            // skip "." and ".."
            if ( ($fileName == ".") || ($fileName == "..") )    continue;
                
            $fullFileName = QrbFiles::fixPath("{$directory}/{$fileName}");
            
            if (is_dir($fullFileName))
            {
                array_push($result, $fullFileName);
            }
        }
        
        return $result;
    }


    //////////////////////////////////////////////////////////////////////////////////////
    // helpers for file handling regarding QrbDataType

    // public for unit test
    public function getPath(QrbDataType $qrbDataType) : string
    {
        switch ($qrbDataType)
        {
            case QrbDataType::PDF_TEMPLATE: // no JSON file
            case QrbDataType::PDF_DATA: 
            case QrbDataType::MAIL_BODY:    // no JSON file 
            case QrbDataType::MAIL_DATA: 
            case QrbDataType::CRED_INFO: 
            case QrbDataType::ADD_INFO: 
            case QrbDataType::RECIPIENTS:   // no JSON file, CSV
                return QrbFilePaths::getInputPath($this->qrbEnv->getCurrentProfile());
            case QrbDataType::TEST_RCPNTS:  // no JSON file, CSV
                return QrbFilePaths::getCfgPath();
            case QrbDataType::MARKER_TEST:  // no JSON file
            case QrbDataType::PROTOCOL:     // no JSON file, CSV
            case QrbDataType::PDF_OUTPUT:   // no JSON file, PDF
                return QrbFilePaths::getOutputPathOfRun($this->qrbEnv->getCurrentProfile(), 
                                                        $this->runName);
        }
    }
    
    // public for unit test
    public function getExtension(QrbDataType $qrbDataType) : string
    {
        switch ($qrbDataType)
        {
            case QrbDataType::PDF_TEMPLATE: // no JSON file
                return ".pdf";
            case QrbDataType::PDF_DATA: 
                return ".json";
            case QrbDataType::MAIL_BODY:    // no JSON file 
                return ".html";
            case QrbDataType::MAIL_DATA: 
            case QrbDataType::CRED_INFO: 
            case QrbDataType::ADD_INFO: 
                return ".json";
            case QrbDataType::RECIPIENTS:   // no JSON file, CSV
            case QrbDataType::TEST_RCPNTS:  // no JSON file, CSV
                return ".csv";
            case QrbDataType::MARKER_TEST:  // no JSON file
                return ".marker";
            case QrbDataType::PROTOCOL:     // no JSON file, CSV
                return ".csv";
            case QrbDataType::PDF_OUTPUT:   // no JSON file, PDF
                return ".pdf";
        }
    }
    
    // public for unit test
    // language: one of "de", "fr", "it", "en" or "" (no or default language)
    public function getFileName(QrbDataType $qrbDataType, string $language = "") : string
    {
        if ($language != "")
        {
            return $qrbDataType->value . "_" . $language . $this->getExtension($qrbDataType);
        }
        
        return $qrbDataType->value . $this->getExtension($qrbDataType);
    }
    
    // Returns full file name (path + filename).
    // language: one of "de", "fr", "it", "en" or "" (no or default language)
    private function getFullFileName(QrbDataType $qrbDataType, string $language = "") : string
    {
        return $this->getPath($qrbDataType) . "/" . $this->getFileName($qrbDataType, $language);
    }
}

?>
