<?php

namespace OCA\GroupfolderFilesystemSnapshots;

use OCA\GroupfolderFilesystemSnapshots\Helpers\FileHelper;

class RecursiveDiff {

    public string $dir1;
    public string $dir2;

    private $prefix;

    private $newResultCallback;
    private $progressCallback;

    private $scan1files = [];
    private $scan1folders = [];

    private $scan2files = [];
    private $scan2folders = [];

    private $subJobs = [];

    private $subJobProgress = [];
    private $progress = 0;

    public function __construct($dir1, $dir2, $prefix = "", $newResultCallback, $progressCallback){
        $this->dir1 = $dir1;
        $this->dir2 = $dir2;
        $this->prefix = $prefix;

        $this->newResultCallback = $newResultCallback;
        $this->progressCallback = $progressCallback;
    }

    public function scan() {
        $scan_num_files = 0;

        if(file_exists($this->dir1) && is_dir($this->dir1)) {
            [$this->scan1files, $this->scan1folders] = FileHelper::getFilesAndFolders($this->dir1);
        }
        
        if(file_exists($this->dir2) && is_dir($this->dir2)) {
            [$this->scan2files, $this->scan2folders] = FileHelper::getFilesAndFolders($this->dir2);
        }

        $scan_num_files += sizeof($this->scan1files);
        $scan_num_files += sizeof($this->scan2files);
        
        $allSubfolders = array_unique(array_merge($this->scan1folders, $this->scan2folders));

        foreach($allSubfolders as $key=>$folder) {
            $subdir1 = $this->dir1 . DIRECTORY_SEPARATOR . $folder;
            $subdir2 = $this->dir2 . DIRECTORY_SEPARATOR . $folder;
            $subprefix = $this->prefix . DIRECTORY_SEPARATOR . $folder;
            
            $newJob = new RecursiveDiff($subdir1, $subdir2, $subprefix, $this->newResultCallback, function($numDoneFiles) use ($key) {
                $this->subJobProgress[$key] = $numDoneFiles;

                $this->updateProgress();
            });

            $this->subJobs[] = $newJob;

            $scan_num_files += $newJob->scan();
        }

        return $scan_num_files;
    }

    private function updateProgress() {
        ($this->progressCallback)(array_sum($this->subJobProgress) + $this->progress);
    }

    function diff() {
        $diff = [];

        foreach($this->subJobs as $job) {
            $result = $job->diff();
            array_push($diff, ...$result);
        }

        $fileCreations = array_diff($this->scan2files, $this->scan1files);
        $fileCreationsFilesizes = FileHelper::getFilesizesOfFiles($this->dir2, $fileCreations);

        $fileDeletions = array_diff($this->scan1files, $this->scan2files);
        $fileDeletionsFilesizes = FileHelper::getFilesizesOfFiles($this->dir1, $fileDeletions);

        $filePossibleEdits = array_intersect($this->scan1files, $this->scan2files);

        /*$diff[] = [
            "type" => "DEBUG",
            "prefix" => $this->prefix,
            "fileCreations" => $fileCreations,
            "fileCreationsFilesizes" => $fileCreationsFilesizes,
            "fileDeletions" => $fileDeletions,
            "fileDeletionsFilesizes" => $fileDeletionsFilesizes,
            //"folderCreations" => $folderCreations,
            //"folderDeletions" => $folderDeletions,
            "allSubfolders" => $allSubfolders,
        ];*/

        // search for creations and deletions, that are actually renames
        foreach($fileCreations as $creationIndex=>$creation) {
            $creationPath = $this->dir2 . DIRECTORY_SEPARATOR . $creation;
            $creationSize = $fileCreationsFilesizes[$creationIndex];
            
            $renameContenders = array_keys($fileDeletionsFilesizes, $creationSize);

            if(sizeof($renameContenders) != 0) {
                /*$diff[] = [
                    "type" => "DEBUG",
                    "comparing" => [
                        "creation" => $creationIndex,
                        "deletions" => $renameContenders,
                    ],
                ];*/

                $creationSHA = sha1_file($creationPath);
                foreach($renameContenders as $contender) {
                    $deletion = $fileDeletions[$contender];
                    $deletionPath = $this->dir1 . DIRECTORY_SEPARATOR . $deletion;
                    $deletionSHA = sha1_file($deletionPath);

                    if($deletionSHA == $creationSHA) {
                        ($this->newResultCallback)(
                            type: "RENAME",
                            beforeFileExists: True,
                            beforePath: $this->prefix . DIRECTORY_SEPARATOR . $deletion,
                            beforeSize: $creationSize,
                            currentFileExists: True,
                            currentPath: $this->prefix . DIRECTORY_SEPARATOR . $creation,
                            currentSize: $creationSize,
                        );

                        unset($fileCreations[$creationIndex]);
                        unset($fileDeletions[$contender]);

                        $this->progress += 2;
                        $this->updateProgress();

                        break;
                    }
                }
            }
        }

        foreach($fileCreations as $index=>$creation) {
            ($this->newResultCallback)(
                type: "CREATION",
                beforeFileExists: False,
                beforePath: NULL,
                beforeSize: NULL,
                currentFileExists: True,
                currentPath: $this->prefix . DIRECTORY_SEPARATOR . $creation,
                currentSize: $fileCreationsFilesizes[$index],
            );

            $this->progress++;
            $this->updateProgress();
        }

        foreach($fileDeletions as $index=>$deletion) {
            ($this->newResultCallback)(
                type: "DELETION",
                beforeFileExists: True,
                beforePath: $this->prefix . DIRECTORY_SEPARATOR . $deletion,
                beforeSize: $fileDeletionsFilesizes[$index],
                currentFileExists: False,
                currentPath: NULL,
                currentSize: NULL,
            );

            $this->progress++;
            $this->updateProgress();
        }

        foreach($filePossibleEdits as $possibleEdit) {
            $file1 = $this->dir1 . DIRECTORY_SEPARATOR . $possibleEdit;
            $file2 = $this->dir2 . DIRECTORY_SEPARATOR . $possibleEdit;
            $file1Size = filesize($file1);
            $file2Size = filesize($file2);

            $this->progress += 2;
            $this->updateProgress();

            if(filemtime($file1) == filemtime($file2)) {
                //not different because same mtime
                continue;
            } else {
                // mtime different, but could just have gotten touched without modifications
                if($file1Size == $file2Size) {
                    // if filesize is the same check for binary differences
                    $handle1 = fopen($file1, 'rb');
                    $handle2 = fopen($file2, 'rb');

                    $filesdifferent = false;

                    while(!feof($handle1)) {
                        if(fread($handle1, 8192) != fread($handle2, 8192)) {
                            // files are different
                            $filesdifferent = true;
                            break;
                        }
                    }

                    fclose($handle1);
                    fclose($handle2);

                    if(!$filesdifferent) {
                        continue;
                    }
                }
            }

            
            ($this->newResultCallback)(
                type: "EDIT",
                beforeFileExists: True,
                beforePath: $this->prefix . DIRECTORY_SEPARATOR . $possibleEdit,
                beforeSize: $file1Size,
                currentFileExists: True,
                currentPath: $this->prefix . DIRECTORY_SEPARATOR . $possibleEdit,
                currentSize: $file2Size,
            );
        }

        return $diff;
    }
}