<?php
namespace boru\boruai\Vector;

use boru\output\Output;
use boru\qdrant\Models\Collection;
use boru\qdrant\Models\Params\VectorParams;
use DirectoryIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;

class DirectoryCollection extends VectorCollection {
    private $directory;
    private $fileExtension;
    private $fileFilter;
    private $fileFilterRegex;
    private $fileFilterCallback;
    private $fileFilterCallbackArgs = [];
    private $recursive = false;

    private static $verbose = true;

    public function __construct($initOptions=[]) {
        $this->setFromOptions($initOptions);
        parent::__construct($initOptions);
    }

    public static function verbose($verbose=null) {
        if($verbose !== null) {
            self::$verbose = $verbose;
        }
        return self::$verbose;
    }

    public function resetCollection() {
        $this->collection()->delete(null,true);
        $this->collection()->create();
    }

    public function upsertDirectory($subDir="/",$chunkSize=2000,$chunkOverlap=100) {
        $chunks = $this->getChunkPoints($subDir,$chunkSize,$chunkOverlap);
        foreach($chunks as $chunk) {
            $chunk->embed();
            $this->upsert($chunk);
            if(self::$verbose) {
                Output::outLine("Chunk embedded and upserted: ".$chunk->payload()["filename"]." (".$chunk->payload()["index"].")");
            }
        }
    }

    public function getChunkPoints($subDir="/",$chunkSize=2000,$chunkOverlap=100) {
        foreach($this->getFiles($subDir) as $file) {
            $content = file_get_contents($file->getRealPath());
            $chunks = $this->chunkText($content, $chunkSize, $chunkOverlap);
            $i=0;
            foreach($chunks as $chunk) {
                yield $this->makeChunkRecord($file, $i, $chunk);
                $i++;
            }
        }
    }

    public function getFiles($subDir=null,$recursive=null) {
        if($recursive === null) {
            $recursive = $this->recursive();
        }
        $directory = $this->directory;
        if($subDir) {
            $directory = $this->directory . "/" . $subDir;
        }
        if(!is_dir($directory)) {
            return [];
        }
        if(!$recursive) {
            $files = new DirectoryIterator($directory);
            foreach($files as $file) {
                if ($file->isDir()) {
                    continue;
                }
                if($this->validateFile($file)) {
                    yield $file;
                }
            }
            return;
        }
        /** @var \SplFileInfo[] */
        $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST);
        foreach($files as $file) {
            if ($file->isDir() || !$file->isFile()) {
                continue;
            }
            if($this->validateFile($file)) {
                yield $file;
            }
        }
    }

    /**
     * Validates the file based on the set filters.
     * @param \SplFileInfo $file The file to validate.
     * @return bool True if the file passes all filters, false otherwise.
     */
    public function validateFile($file) {
        if ($this->fileExtension && pathinfo($file, PATHINFO_EXTENSION) !== $this->fileExtension) {
            return false;
        }
        if ($this->fileFilter && !$this->fileFilter($file)) {
            return false;
        }
        if ($this->fileFilterRegex && !preg_match($this->fileFilterRegex, $file)) {
            return false;
        }
        if ($this->fileFilterCallback && !call_user_func($this->fileFilterCallback, $file, ...$this->fileFilterCallbackArgs)) {
            return false;
        }
        return true;
    }





    private function chunkText(string $text, int $size = 2000, int $overlap = 200): array {
        $len    = mb_strlen($text);
        $chunks = [];
        for ($start = 0; $start < $len; $start += $size - $overlap) {
            $chunks[] = mb_substr($text, $start, $size);
        }
        return $chunks;
    }

    /**
     * Creates a new chunk point from the file and chunk index.
     * @param \SplFileInfo $file The file to create the chunk point from.
     * @param int $index The index of the chunk.
     * @param string $chunk The chunk text.
     * @return CollectionRecord The created chunk point.
     */
    private function makeChunkRecord($file,$index=0,$chunk) {
        /** @var CollectionRecord */
        $record = $this->newRecord();

        $record->stringId(md5($file->getPathname() . $index));
        $record->embeddingString($chunk);
        $record->payload([
            "filename" => $file->getFilename(),
            "ext" => $file->getExtension(),
            "path" => $file->getRealPath(),
            "dir" => dirname($file->getRealPath()),
            "index" => $index,
        ]);
        return $record;
    }






    //Setters and getters

    /**
     * Will extract options from the array and set them to the class properties, and remove them from the array before it gets passed to the parent constructor.
     * @param array &$options 
     * @return void 
     */
    private function setFromOptions(&$options=[]) {
        foreach($options as $optKey=>$optValue) {
            if($optKey == "collection") {
                if($optValue instanceof Collection) {
                    $this->collection($optValue);
                } else {
                    $this->collection(new Collection($optValue,VectorParams::OpenAI()));
                }
                unset($options[$optKey]);
                continue;
            }
            $camelKey = str_replace(" ","",ucwords(str_replace("_"," ",$optKey)));
            $camelKey = lcfirst($camelKey);
            if(method_exists($this,$camelKey)) {
                $this->$camelKey($optValue);
                unset($options[$optKey]);
            } elseif(method_exists($this,"set".ucfirst($camelKey))) {
                $this->{"set".ucfirst($camelKey)}($optValue);
                unset($options[$optKey]);
            }
        }
    }

    /**
     * Set or get the directory path.
     * @param string|null $directory The directory path to set. If null, the current directory path is returned.
     * @return string|null 
     */
    public function directory($directory=null) {
        if($directory !== null) {
            $this->directory = $directory;
            return $this;
        }
        return $this->directory;
    }

    /**
     * Set or get the file extension.
     * @param string|null $fileExtension The file extension to set. If null, the current file extension is returned.
     * @return string|null 
     */
    public function fileExtension($fileExtension=null) {
        if($fileExtension !== null) {
            $this->fileExtension = $fileExtension;
            return $this;
        }
        return $this->fileExtension;
    }

    /**
     * Set or get the recursive flag.
     * @param bool|null $recursive The recursive flag to set. If null, the current recursive flag is returned.
     * @return bool|null 
     */
    public function recursive($recursive=null) {
        if($recursive !== null) {
            $this->recursive = $recursive;
            return $this;
        }
        return $this->recursive;
    }

    /**
     * Set or get the file filter.
     * @param callable|null $fileFilter The file filter to set. If null, the current file filter is returned.
     * @return callable|null 
     */
    public function fileFilter($fileFilter=null) {
        if($fileFilter !== null) {
            $this->fileFilter = $fileFilter;
            return $this;
        }
        return $this->fileFilter;
    }

    /**
     * Set or get the file filter regex.
     * @param string|null $fileFilterRegex The file filter regex to set. If null, the current file filter regex is returned.
     * @return string|null 
     */
    public function fileFilterRegex($fileFilterRegex=null) {
        if($fileFilterRegex !== null) {
            $this->fileFilterRegex = $fileFilterRegex;
            return $this;
        }
        return $this->fileFilterRegex;
    }

    /**
     * Set or get the file filter callback.
     * @param callable|null $fileFilterCallback The file filter callback to set. If null, the current file filter callback is returned.
     * @return callable|null 
     */
    public function fileFilterCallback($fileFilterCallback=null) {
        if($fileFilterCallback !== null) {
            $this->fileFilterCallback = $fileFilterCallback;
            return $this;
        }
        return $this->fileFilterCallback;
    }

    /**
     * Set or get the file filter callback arguments.
     * @param array|null $fileFilterCallbackArgs The file filter callback arguments to set. If null, the current file filter callback arguments are returned.
     * @return array|null 
     */
    public function fileFilterCallbackArgs($fileFilterCallbackArgs=null) {
        if($fileFilterCallbackArgs !== null) {
            $this->fileFilterCallbackArgs = $fileFilterCallbackArgs;
            return $this;
        }
        return $this->fileFilterCallbackArgs;
    }
}