<?php

/**
 *
 * @package   Duplicator
 * @copyright (c) 2021, Snapcreek LLC
 */

namespace Duplicator\Libs\DupArchive;

use Duplicator\Libs\DupArchive\Headers\DupArchiveReaderDirectoryHeader;
use Duplicator\Libs\DupArchive\Headers\DupArchiveReaderFileHeader;
use Duplicator\Libs\DupArchive\Headers\DupArchiveReaderGlobHeader;
use Duplicator\Libs\DupArchive\Headers\DupArchiveReaderHeader;
use Duplicator\Libs\DupArchive\Info\DupArchiveExpanderInfo;
use Exception;

class DupArchiveExpandBasicEngine extends DupArchive
{
    protected static $logCallback   = null;
    protected static $chmodCallback = null;
    protected static $mkdirCallback = null;

    /**
     * Set callabcks function
     *
     * @param null|callable $log   function callback
     * @param null|callable $chmod function callback
     * @param null|callable $mkdir function callback
     *
     * @return void
     */
    public static function setCallbacks($log, $chmod, $mkdir)
    {
        self::$logCallback   = (is_callable($log) ? $log : null);
        self::$chmodCallback = (is_callable($chmod) ? $chmod : null);
        self::$mkdirCallback = (is_callable($mkdir) ? $mkdir : null);
    }

    /**
     * Write log
     *
     * @param string $s     string
     * @param bool   $flush if true flush file
     *
     * @return void
     */
    public static function log($s, $flush = false)
    {
        if (self::$logCallback == null) {
            return;
        }
        call_user_func(self::$logCallback, "MINI EXPAND:$s", $flush);
    }

    /**
     * Expand folder
     *
     * @param string $archivePath  archive path
     * @param string $relativePath relative path
     * @param string $destPath     dest path
     * @param bool   $ignoreErrors if true ignore errors
     * @param int    $offset       start scan location
     *
     * @return void
     */
    public static function expandDirectory($archivePath, $relativePath, $destPath, $ignoreErrors = false, $offset = 0)
    {
        self::expandItems($archivePath, $relativePath, $destPath, $ignoreErrors, $offset);
    }

    /**
     * Expand items
     *
     * @param string   $archivePath     archive path
     * @param string[] $inclusionFilter filters
     * @param string   $destDirectory   dest path
     * @param bool     $ignoreErrors    if true ignore errors
     * @param int      $offset          start scan location
     *
     * @return void
     */
    private static function expandItems($archivePath, $inclusionFilter, $destDirectory, $ignoreErrors = false, $offset = 0)
    {
        $archiveHandle = fopen($archivePath, 'rb');

        if ($archiveHandle === false) {
            throw new Exception("Can’t open archive at $archivePath!");
        }

        $archiveHeader = DupArchiveReaderHeader::readFromArchive($archiveHandle);
        $writeInfo     = new DupArchiveExpanderInfo();

        $writeInfo->destDirectory = $destDirectory;
        $writeInfo->isCompressed  = $archiveHeader->isCompressed;

        if ($offset > 0) {
            fseek($archiveHandle, $offset);
        }

        $moreToRead = true;

        while ($moreToRead) {
            if ($writeInfo->currentFileHeader != null) {
                try {
                    if (self::passesInclusionFilter($inclusionFilter, $writeInfo->currentFileHeader->relativePath)) {
                        self::writeToFile($archiveHandle, $writeInfo);
                        $writeInfo->fileWriteCount++;
                    } elseif ($writeInfo->currentFileHeader->fileSize > 0) {
                        self::skipFileInArchive($archiveHandle, $writeInfo->currentFileHeader);
                    }
                    $writeInfo->currentFileHeader = null;
                    // Expand state taken care of within the write to file to ensure consistency
                } catch (Exception $ex) {
                    if (!$ignoreErrors) {
                        throw $ex;
                    }
                }
            } else {
                $headerType = self::getNextHeaderType($archiveHandle);

                switch ($headerType) {
                    case self::HEADER_TYPE_FILE:
                        $writeInfo->currentFileHeader = DupArchiveReaderFileHeader::readFromArchive($archiveHandle, false, true);
                        break;
                    case self::HEADER_TYPE_DIR:
                        $directoryHeader = DupArchiveReaderDirectoryHeader::readFromArchive($archiveHandle, true);
                        //   self::log("considering $inclusionFilter and {$directoryHeader->relativePath}");
                        if (self::passesInclusionFilter($inclusionFilter, $directoryHeader->relativePath)) {
                            //    self::log("passed");
                            $directory = "{$writeInfo->destDirectory}/{$directoryHeader->relativePath}";

                            //  $mode = $directoryHeader->permissions;
                            // rodo handle this more elegantly @mkdir($directory, $directoryHeader->permissions, true);
                            if (is_callable(self::$mkdirCallback)) {
                                call_user_func(self::$mkdirCallback, $directory, 'u+rwx', true);
                            } else {
                                mkdir($directory, 0755, true);
                            }
                            $writeInfo->directoryWriteCount++;
                        } else {
                            // self::log("didnt pass");
                        }
                        break;
                    case self::HEADER_TYPE_NONE:
                        $moreToRead = false;
                }
            }
        }

        fclose($archiveHandle);
    }

    /**
     * Write to file
     *
     * @param resource               $archiveHandle archive file handle
     * @param DupArchiveExpanderInfo $writeInfo     write info
     *
     * @return void
     */
    private static function writeToFile($archiveHandle, DupArchiveExpanderInfo $writeInfo)
    {
        $destFilePath = $writeInfo->getCurrentDestFilePath();

        if ($writeInfo->currentFileHeader->fileSize > 0) {
            $parentDir = dirname($destFilePath);
            if (!file_exists($parentDir)) {
                if (is_callable(self::$mkdirCallback)) {
                    $res = call_user_func(self::$mkdirCallback, $parentDir, 'u+rwx', true);
                } else {
                    $res = mkdir($parentDir, 0755, true);
                }
                if (!$res) {
                    throw new Exception("Couldn't create {$parentDir}");
                }
            }

            $destFileHandle = fopen($destFilePath, 'wb+');
            if ($destFileHandle === false) {
                throw new Exception("Couldn't open {$destFilePath} for writing.");
            }

            do {
                self::appendGlobToFile($archiveHandle, $destFileHandle, $writeInfo);

                $currentFileOffset = ftell($destFileHandle);

                $moreGlobstoProcess = $currentFileOffset < $writeInfo->currentFileHeader->fileSize;
            } while ($moreGlobstoProcess);

            fclose($destFileHandle);

            if (is_callable(self::$chmodCallback)) {
                call_user_func(self::$chmodCallback, $destFilePath, 'u+rw');
            } else {
                chmod($destFilePath, 0644);
            }

            self::validateExpandedFile($writeInfo);
        } else {
            if (touch($destFilePath) === false) {
                throw new Exception("Couldn't create $destFilePath");
            }

            if (is_callable(self::$chmodCallback)) {
                call_user_func(self::$chmodCallback, $destFilePath, 'u+rw');
            } else {
                chmod($destFilePath, 0644);
            }
        }
    }

    /**
     * Validate file
     *
     * @param DupArchiveExpanderInfo $writeInfo write info
     *
     * @return void
     */
    private static function validateExpandedFile(DupArchiveExpanderInfo $writeInfo)
    {
        if ($writeInfo->currentFileHeader->hash !== '00000000000000000000000000000000') {
            $hash = hash_file('crc32b', $writeInfo->getCurrentDestFilePath());

            if ($hash !== $writeInfo->currentFileHeader->hash) {
                throw new Exception("MD5 validation fails for {$writeInfo->getCurrentDestFilePath()}");
            }
        }
    }

    /**
     * Undocumented function
     * Assumption is that archive handle points to a glob header on this call
     *
     * @param resource               $archiveHandle  archive handle
     * @param resource               $destFileHandle dest file handle
     * @param DupArchiveExpanderInfo $writeInfo      write info
     *
     * @return void
     */
    private static function appendGlobToFile($archiveHandle, $destFileHandle, DupArchiveExpanderInfo $writeInfo)
    {
        $globHeader   = DupArchiveReaderGlobHeader::readFromArchive($archiveHandle, false);
        $globContents = fread($archiveHandle, $globHeader->storedSize);

        if ($globContents === false) {
            throw new Exception("Error reading glob from " . $writeInfo->getCurrentDestFilePath());
        }

        if ($writeInfo->isCompressed) {
            $globContents = gzinflate($globContents);
        }

        if (fwrite($destFileHandle, $globContents) !== strlen($globContents)) {
            throw new Exception("Unable to write all bytes of data glob to storage.");
        }
    }

    /**
     * Check filter
     *
     * @param string $filter    filter
     * @param string $candidate candidate
     *
     * @return bool
     */
    private static function passesInclusionFilter($filter, $candidate)
    {
        return (substr($candidate, 0, strlen($filter)) == $filter);
    }
}
