<?php

// SPDX-FileCopyrightText: 2018 Icinga GmbH <https://icinga.com>
// SPDX-License-Identifier: GPL-3.0-or-later

namespace Icinga\Util;

use SplFileObject;
use ErrorException;
use RuntimeException;
use Icinga\Exception\NotWritableError;

/**
 * File
 *
 * A class to ease opening files and reading/writing to them.
 */
class File extends SplFileObject
{
    /**
     * The mode used to open the file
     *
     * @var string
     */
    protected $openMode;

    /**
     * The access mode to use when creating directories
     *
     * @var int
     */
    public static $dirMode = 1528; // 2770

    /**
     * @see SplFileObject::__construct()
     */
    public function __construct($filename, $openMode = 'r', $useIncludePath = false, $context = null)
    {
        $this->openMode = $openMode;
        if ($context === null) {
            parent::__construct($filename, $openMode, $useIncludePath);
        } else {
            parent::__construct($filename, $openMode, $useIncludePath, $context);
        }
    }

    /**
     * Create a file using the given access mode and return a instance of File open for writing
     *
     * @param   string  $path           The path to the file
     * @param   int     $accessMode     The access mode to set
     * @param   bool    $recursive      Whether missing nested directories of the given path should be created
     *
     * @return  File
     *
     * @throws  RuntimeException        In case the file cannot be created or the access mode cannot be set
     * @throws  NotWritableError        In case the path's (existing) parent is not writable
     */
    public static function create($path, $accessMode, $recursive = true)
    {
        $dirPath = dirname($path);
        if ($recursive && !is_dir($dirPath)) {
            static::createDirectories($dirPath);
        } elseif (! is_writable($dirPath)) {
            throw new NotWritableError(sprintf('Path "%s" is not writable', $dirPath));
        }

        $file = new static($path, 'x+');

        if (! @chmod($path, $accessMode)) {
            $error = error_get_last();
            throw new RuntimeException(sprintf(
                'Cannot set access mode "%s" on file "%s" (%s)',
                decoct($accessMode),
                $path,
                $error['message']
            ));
        }

        return $file;
    }

    /**
     * Create missing directories
     *
     * @param   string  $path
     *
     * @throws  RuntimeException        In case a directory cannot be created or the access mode cannot be set
     */
    protected static function createDirectories($path)
    {
        $part = strpos($path, DIRECTORY_SEPARATOR) === 0 ? DIRECTORY_SEPARATOR : '';
        foreach (explode(DIRECTORY_SEPARATOR, ltrim($path, DIRECTORY_SEPARATOR)) as $dir) {
            $part .= $dir . DIRECTORY_SEPARATOR;

            if (! is_dir($part)) {
                if (! @mkdir($part, static::$dirMode)) {
                    $error = error_get_last();
                    throw new RuntimeException(sprintf(
                        'Failed to create missing directory "%s" (%s)',
                        $part,
                        $error['message']
                    ));
                }

                if (! @chmod($part, static::$dirMode)) {
                    $error = error_get_last();
                    throw new RuntimeException(sprintf(
                        'Failed to set access mode "%s" for directory "%s" (%s)',
                        decoct(static::$dirMode),
                        $part,
                        $error['message']
                    ));
                }
            }
        }
    }

    #[\ReturnTypeWillChange]
    public function fwrite($str, $length = null)
    {
        $this->assertOpenForWriting();
        $this->setupErrorHandler();
        $retVal = $length === null ? parent::fwrite($str) : parent::fwrite($str, $length);
        restore_error_handler();
        return $retVal;
    }

    public function ftruncate($size): bool
    {
        $this->assertOpenForWriting();
        $this->setupErrorHandler();
        $retVal = parent::ftruncate($size);
        restore_error_handler();
        return $retVal;
    }

    #[\ReturnTypeWillChange]
    public function ftell()
    {
        $this->setupErrorHandler();
        $retVal = parent::ftell();
        restore_error_handler();
        return $retVal;
    }

    public function flock($operation, &$wouldblock = null): bool
    {
        $this->setupErrorHandler();
        $retVal = parent::flock($operation, $wouldblock);
        restore_error_handler();
        return $retVal;
    }

    #[\ReturnTypeWillChange]
    public function fgetc()
    {
        $this->setupErrorHandler();
        $retVal = parent::fgetc();
        restore_error_handler();
        return $retVal;
    }

    public function fflush(): bool
    {
        $this->setupErrorHandler();
        $retVal = parent::fflush();
        restore_error_handler();
        return $retVal;
    }

    /**
     * Setup an error handler that throws a RuntimeException for every emitted E_WARNING
     */
    protected function setupErrorHandler()
    {
        set_error_handler(
            function ($errno, $errstr, $errfile, $errline) {
                restore_error_handler();
                throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
            },
            E_WARNING
        );
    }

    /**
     * Assert that the file was opened for writing and throw an exception otherwise
     *
     * @throws  NotWritableError    In case the file was not opened for writing
     */
    protected function assertOpenForWriting()
    {
        if (!preg_match('@w|a|\+@', $this->openMode)) {
            throw new NotWritableError('File not open for writing');
        }
    }
}
