<?php declare(strict_types=1);

require_once __DIR__ . '/QrbProcessStatus.php';

// Helper to write the status file for the web interface.
// The methods of this class do not throw exceptions, but call "die"!
final class QrbProcessStatusTracker
{
    // status returned by trackStatus()
    public const GOOD      = 0;
    public const NOT_FOUND = 1;
    public const EOF       = 2;
    public const ERROR     = 3;
    
    // file names
    private string $statusFileName;
    private string $trackingFileName;
    
    // number of bytes already written to receiver
    private int $trackBytesWritten;
    // size of contents of status file
    private int $statusFileContentSize;
    // contents of status file
    private ?string $statusFileContent;
    // new status text
    private ?string $statusText;

    // Creates an instance.
    public function __construct()
    {
        $this->statusFileName   = QrbProcessStatus::getStatusFullFileName();
        $this->trackingFileName = QrbProcessStatus::getStatusFullFileName() . "Tracking";
    }
    
    // Tracks the status written by the current processing (by QrbProcessStatus).
    //
    // Returns:
    // GOOD:        new text found, 
    //              call getStatusText() to retrieve status text and call this method again
    // NOT_FOUND:   there is not yet any text (but there will be), 
    //              call again this method
    // EOF:         there is no more text,
    //              processing finished, do NOT call this method again
    // ERROR:       something unexpected happened,
    //              processing aborted, do NOT call this method again
    public function trackStatus() : int
    {
        $this->trackBytesWritten     = 0;
        $this->statusFileContentSize = 0;
        $this->statusFileContent     = NULL;
        $this->statusText            = NULL;
        
        // handle state of last call (updates trackBytesWritten)
//         error_log("+++++++++++++++++++ handleStateOfLastCall()");
        $result = $this->handleStateOfLastCall();
        if ($result != self::GOOD)
        {
            return $result;
        }

        // read text from status file (reads statusFileContent)
//         error_log("+++++++++++++++++++ readStatus()");
        $result = $this->readStatus();
        if ($result != self::GOOD)
        {
            return $result;
        }

        // update tracking file with new "track bytes written" (if EOF, removes EOF from statusFileContent)
//         error_log("+++++++++++++++++++ updateTrackingFile()");
        $result = $this->updateTrackingFile();
        if ($result != self::GOOD)
        {
            return $result;
        }

        // set up status text not already written
//         error_log("+++++++++++++++++++ Copy text");
        $this->statusText = substr($this->statusFileContent, $this->trackBytesWritten);
    
        // There's more to read.
        return self::GOOD;
    }

    // Returns status text added since last call to trackStatus().
    // Returns NULL if last call to trackStatus() didn't return GOOD.
    public function getStatusText() : ?string
    {
        return $this->statusText;
    }
    
    // Handles state of last call, does nothing if first call.
    //
    // Returns
    // GOOD     first call (nothing todo) or success (trackBytesWritten updated)
    // EOF      in case of EOF
    // ERROR    in case of an error
    private function handleStateOfLastCall() : int
    {
        // check if first call
        if (!file_exists($this->trackingFileName))  return self::GOOD;
        
        // read tracking file
        $trackingFile = fopen($this->trackingFileName, "r");
        if ($trackingFile === false)                return self::ERROR;
        
        clearstatcache();
        $trackingFileContents = fread($trackingFile, filesize($this->trackingFileName));
        if ($trackingFileContents === false)        return self::ERROR;
        
        fclose($trackingFile);
        
        // parse old "track bytes written"
        list($bytesWritten) = sscanf($trackingFileContents, "%8u");
        if ($bytesWritten)
        {
            $this->trackBytesWritten = $bytesWritten;
        }
        
        // check for EOF marker in tracking file
        $pos = stripos($trackingFileContents, QrbProcessStatus::STATUS_EOF_MARKER);
        if ($pos !== false)    // EOF marker found
        {
            // clean up tracking and status files
//             error_log("Delete tracking file '{$this->trackingFileName}'");
            unlink($this->trackingFileName);
//             error_log("Delete status file '{$this->statusFileName}'");
            unlink($this->statusFileName);
    
            return self::EOF;   // This is the normal end of the processing
        }
        
        return self::GOOD;
    }
    
    // Reads status.
    //
    // Returns
    // GOOD         success (statusFileContent updated)
    // NOT_FOUND    in case there is no status text (at least not yet)
    // ERROR        in case of an error
    private function readStatus() : int
    {
        // check if file exists
        if (!file_exists($this->statusFileName))
        {
//             error_log("File does not exist '{$this->statusFileName}'");
            return self::NOT_FOUND;
        }
        
        $statusFile = fopen($this->statusFileName, "r");
        if ($statusFile === false)
        {
//             error_log("Cannot open file '{$this->statusFileName}'");
            return self::NOT_FOUND;
        }
        
        // check if file exists, but nothing was written into it (may really happen!)
        clearstatcache();
        $this->statusFileContentSize = filesize($this->statusFileName);
        
        if ($this->statusFileContentSize == 0)
        {
//             error_log("File size of file '{$this->statusFileName}' is 0");
            return self::NOT_FOUND;
        }
        
        $this->statusFileContent = fread($statusFile, $this->statusFileContentSize);
        if ($this->statusFileContent === false)     return self::ERROR;
        
        return self::GOOD;
    }
    
    // Updates the tracking file.
    //
    // Returns
    // GOOD     success (if EOF, statusFileContent updated)
    // ERROR    in case of an error
    private function updateTrackingFile() : int
    {
        $trackingFile = fopen($this->trackingFileName, "w");
        if ($trackingFile === false)    return self::ERROR;
        
        // write new "track bytes written" into tracking file
        $statusFileContentizeString = sprintf("%8u", $this->statusFileContentSize);
        fwrite($trackingFile, $statusFileContentizeString . "\n");
        fflush($trackingFile);
        
        // check for EOF marker in status file
        $pos = stripos($this->statusFileContent, QrbProcessStatus::STATUS_EOF_MARKER);
        if ($pos !== false)    // EOF marker found
        {
            // remove EOF marker from status file contents
            $this->statusFileContent = substr($this->statusFileContent, 0, $pos);
            
            // write EOF marker into tracking file for next check by handleStateOfLastCall()
            if (($trackingFile = fopen($this->trackingFileName, "a")) !== false)
            {    
                fwrite($trackingFile, QrbProcessStatus::STATUS_EOF_MARKER . "\n");
                fflush($trackingFile);
                fclose($trackingFile);
            }
        }
        
        return self::GOOD;
    }
}

?>
