<?php

namespace classes;

use Exception;
use RuntimeException;

class Printer
{
    private string $printerAddress;
    private $socket;
    private int $numOfCopies;
    private int $delayBetweenCopies;

    /**
     * __construct method
     *
     * Initialises a new instance of the Printer class.
     *
     * @param string $printerAddress The address of the printer.
     * @param int $printerPort The port number of the printer. Defaults to 9100.
     *
     * @throws Exception If unable to connect to the printer.
     */
    public function __construct(string $printerAddress, int $printerPort = 9100, int $delayBetweenCopies = 750)
    {
        $this->printerAddress = $printerAddress;
        $this->delayBetweenCopies = $delayBetweenCopies;

        // Check connection with the printer
        $this->pingPrinter();

        // Creates a TCP socket
        if (!$this->socket = stream_socket_client("tcp://{$this->printerAddress}:{$printerPort}", $errorNumber, $errorString, 30))
        {
            throw new RuntimeException($errorString, $errorNumber);
        }
    }

    /**
     * pingPrinter method
     *
     * Pings the printer to check if it is reachable.
     *
     * @throws Exception If the printer is not reachable.
     */
    public function pingPrinter(): void
    {
        $pingCommand = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN')
            ? "ping -n 1 $this->printerAddress"
            : "ping -c 1 $this->printerAddress";

        // Execute the ping command
        $result = shell_exec($pingCommand);

        // Checks the output for a successful response from the ping command
        if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN')
        {
            if (!strpos($result, 'Reply from'))
            {
                throw new RuntimeException("Printer $this->printerAddress is not reachable");
            }
        }
        elseif (!strpos($result, '1 packets transmitted, 1 received'))
        {
            throw new RuntimeException("Printer $this->printerAddress is not reachable");
        }
    }

    /**
     * printFile method
     *
     * Prints the contents of a file to the printer.
     *
     * @param string $filePath The path to the file to be printed.
     *
     * @throws Exception If the file is not found or if unable to load the file contents.
     */
    public function printFile(string $filePath, int $numOfCopies = 1): void
    {
        $this->numOfCopies = $numOfCopies;

        // Checks if the file exists
        if (!file_exists($filePath))
        {
            throw new RuntimeException("File not found");
        }

        // Reads the contents of the file
        if (!$fileContents = file_get_contents($filePath))
        {
            throw new Exception("Unable to load file contents");
        }
        try
        {
            $this->printData($fileContents);
        }
        catch (Exception $error)
        {
            echo json_encode(['error' => $error->getMessage()]);
            exit();
        }
    }

    /**
     * printData method
     *
     * Sends the given data to the printer.
     *
     * @param string $data The data to be printed.
     *
     * @return void
     * @throws Exception If unable to send data to the printer.
     *
     */
    public function printData(string $data): void
    {
        for ($i = 0; $i < $this->numOfCopies; ++$i)
        {
            // Sends data to the printer
            if (!fwrite($this->socket, $data))
            {
                throw new RuntimeException('Failed to send data to printer');
            }
            usleep($this->delayBetweenCopies * 1000);
        }
    }

    /**
     * Sends formatted data to the printer.
     *
     * @param string $data The formatted data to send to the printer.
     * @return void
     * @throws Exception If failed to send data to the printer.
     */
    public function printFormattedData(string $data): void
    {
        if (!fprintf($this->socket, $data))
        {
            throw new RuntimeException("Failed to send data to printer");
        }
    }

    /**
     * Closes the socket connection when the object is destroyed.
     *
     * @return void
     */
    public function __destruct()
    {
        $this->closeSocket();
    }

    /**
     * Closes the socket connection.
     *
     * @return void
     */
    public function closeSocket(): void
    {
        fclose($this->socket);
    }
}