<?php

namespace Beganovich\Snappdf;

use Beganovich\Snappdf\Command\DownloadChromiumCommand;
use Beganovich\Snappdf\Exception\BinaryNotExecutable;
use Beganovich\Snappdf\Exception\BinaryNotFound;
use Beganovich\Snappdf\Exception\MissingContent;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Process\Process;

class Snappdf
{
    private $chromiumPath;

    private $chromiumArguments = [];

    private $url;

    private $html;

    private $waitBeforePrinting;

    private $keepTemporaryFiles = false;

    public function __construct()
    {
        $this->chromiumArguments = [
            '--headless',
            '--disable-gpu',
            '--disable-translate',
            '--disable-extensions',
            '--disable-sync',
            '--disable-background-networking',
            '--disable-software-rasterizer',
            '--disable-default-apps',
            '--disable-dev-shm-usage',
            '--safebrowsing-disable-auto-update',
            '--run-all-compositor-stages-before-draw',
            '--no-first-run',
            '--no-margins',
            '--no-sandbox',
            '--print-to-pdf-no-header',
            '--no-pdf-header-footer',
            '--hide-scrollbars',
            '--ignore-certificate-errors',
        ];
    }

    public function getUrl(): ?string
    {
        return $this->url;
    }

    public function setUrl(string $url): self
    {
        $this->url = $url;

        return $this;
    }

    public function getChromiumPath(): string
    {
        if ($this->chromiumPath) {
            return $this->chromiumPath;
        }

        if (getenv('SNAPPDF_EXECUTABLE_PATH')) {
            return getenv('SNAPPDF_EXECUTABLE_PATH');
        }

        $latestRevisionFile = dirname(__FILE__, 2) . '/versions/revision.txt';

        // FreeBSD Support.  Chromium is typically installed via packages or ports.
        if (PHP_OS == 'FreeBSD' && file_exists(PHP_BINDIR . '/chrome')) {
            return PHP_BINDIR . '/chrome';
        }

        if (file_exists($latestRevisionFile)) {
            $chromuimBinary = (new DownloadChromiumCommand())->generatePlatformExecutable(
                file_get_contents($latestRevisionFile)
            );

            if (!is_executable($chromuimBinary)) {
                throw new BinaryNotExecutable('Downloaded Chromium binary is not executable. Make sure to set correct permissions (0755)');
            }

            return $chromuimBinary;
        }

        throw new BinaryNotFound('Browser binary not found. Make sure you download it or set using setChromiumPath().');
    }

    public function setChromiumPath(string $chromiumPath): self
    {
        $this->chromiumPath = $chromiumPath;

        return $this;
    }

    public function getChromiumArguments(): array
    {
        if (getenv('SNAPPDF_EXECUTABLE_ARGUMENTS')) {
            $arguments = explode(" ", getenv('SNAPPDF_EXECUTABLE_ARGUMENTS'));
        } else {
            $arguments = $this->chromiumArguments;
        }

        if ($this->waitBeforePrinting) {
            $matches  = preg_grep('/--virtual-time-budget=(.*)/', $arguments);
            if (count($matches) > 0) {
                $arguments = preg_replace(
                    '/--virtual-time-budget=(.*)/',
                    '--virtual-time-budget=' . (int)$this->waitBeforePrinting,
                    $arguments
                );
            } else {
                array_splice($arguments, 2, 0, ['--virtual-time-budget=' . (int)$this->waitBeforePrinting]);
            }
        }

        return $arguments;
    }

    public function addChromiumArguments(string $chromiumArgument): self
    {
        $arguments = explode(" ", $chromiumArgument);

        foreach ($arguments as $argument) {
            $arg = explode('=', $argument);
            $matches  = preg_grep('/' . $arg[0] . '(.*)/', $this->chromiumArguments);

            if (count($matches) > 0) {
                $this->chromiumArguments = preg_replace(
                    '/' . $arg[0] . '(.*)/',
                    $argument,
                    $this->chromiumArguments
                );
            } else {
                $this->chromiumArguments = array_merge($this->chromiumArguments, [$argument]);
            }
        }

        return $this;
    }

    public function clearChromiumArguments(): self
    {
        $this->chromiumArguments = [];

        return $this;
    }

    public function clearChromiumArgument(string $argument): self
    {
        $this->chromiumArguments = \array_diff($this->chromiumArguments, [$argument]);

        return $this;
    }

    public function getHtml(): ?string
    {
        return $this->html;
    }

    public function setHtml(string $html): self
    {
        $this->html = $html;

        return $this;
    }

    public function waitBeforePrinting(int $waitBeforePrinting): self
    {
        $this->waitBeforePrinting = $waitBeforePrinting;

        return $this;
    }

    public function getWaitTime(): ?int
    {
        return $this->waitBeforePrinting;
    }

    public function setKeepTemporaryFiles(bool $keep): self
    {
        $this->keepTemporaryFiles = $keep;

        return $this;
    }

    public function getKeepTemporaryFiles(): bool
    {
        return (bool) $this->keepTemporaryFiles;
    }

    private function cleanup(string $tempFile, array $content): void
    {
        if ($this->keepTemporaryFiles) {
            return;
        }

        unlink($tempFile);

        if ($content['type'] === 'html') {
            unlink($content['content']);
        }
    }

    public function generate(): ?string
    {
        $content = [
            'type' => null,
            'content' => null,
        ];

        if ($this->getUrl()) {
            $content['type'] = 'url';
            $content['content'] = $this->getUrl();
        }

        if ($this->getHtml()) {
            $temporaryFile = tempnam(sys_get_temp_dir(), 'html_');
            rename($temporaryFile, $temporaryFile .= '.html');
            file_put_contents($temporaryFile, $this->getHtml());

            $content['type'] = 'html';
            $content['content'] = $temporaryFile;
        }

        if (!$content['content']) {
            throw new MissingContent('No content provided. Make sure you call setHtml() or setUrl() before generate().');
        }

        $pdf = tempnam(sys_get_temp_dir(), 'pdf_');
        rename($pdf, $pdf .= '.pdf');

        $commandInput = [$this->getChromiumPath()];

        foreach ($this->getChromiumArguments() as $argument) {
            array_push($commandInput, $argument);
        }

        array_push(
            $commandInput,
            '--print-to-pdf=' . $pdf,
            $content['content'],
        );

        $platform = (new DownloadChromiumCommand())->generatePlatformCode();

        if ($platform == 'Win' || $platform == 'Win_x64') {
            return $this->executeOnWindows($commandInput, $pdf, $content);
        }

        $process = new Process($commandInput);

        $process->run();

        if (!$process->isSuccessful()) {
            throw new \Symfony\Component\Process\Exception\ProcessFailedException($process);
        }

        $pdfContent = file_get_contents($pdf);

        $this->cleanup($pdf, $content);

        return $pdfContent;
    }

    public function save(string $path): void
    {
        $pdf = $this->generate();

        $filesystem = new Filesystem();

        $filesystem->appendToFile($path, $pdf);
    }

    private function executeOnWindows(array $commands, $pdf, array $content): ?string
    {
        $command = implode(' ', $commands) . ' 2>&1'; // must add 2>&1 to redirect stderr to stdout // see https://stackoverflow.com/a/16665146/7511165

        exec($command, $output, $statusCode);

        if ($statusCode && !empty($output)) {
            // $output is an array of lines of the command output
            $message = implode("\n", $output);
            // ProcessFailedException accepts only a string as $message
            throw new \Beganovich\Snappdf\Exception\ProcessFailedException($message);
        }

        $pdfContent = file_get_contents($pdf);

        $this->cleanup($pdf, $content);

        return $pdfContent;
    }
}
