<?php
namespace boru\boruai\Tools;

use boru\boruai\BoruAI;
use boru\boruai\Models\File;
use boru\boruai\Openai\OpenAIFile;
use boru\boruai\Openai\OpenAIAssistant;
use boru\boruai\Openai\OpenAIChat;
use boru\boruai\Openai\Models\Content;

class OCR {

    /** @var string */
    private $fileId;
    /** @var string */
    private $fileUrl;
    /** @var string[] */
    private $before;
    /** @var string[] */
    private $after;
    /** @var string[] */
    private $prompt;
    /** @var OpenAIAssistant */
    private $assistant;

    /** @var OpenAIChat */
    private $chat;

    /** @var string "high|low" */
    private $detail = "high";

    /** @var bool */
    ///private $deleteFile = false;

    /** @var int */
    private $retryLimit = 3;
    /** @var float */
    private $retryDelay = 0.6;

    private $cleanupFiles = [];

    /** @var File */
    private $fileModel;

    private $result;

    public function __construct($fileOrUrl=null,$options=[]) {
        if($fileOrUrl) {
            if(is_array($fileOrUrl)) {
                $options = $fileOrUrl;
            } else {
                if(is_object($fileOrUrl) && $fileOrUrl instanceof OpenAIFile) {
                    $this->fileId($fileOrUrl->id());
                } elseif(is_object($fileOrUrl) && $fileOrUrl instanceof File) {
                    $this->fileModel = $fileOrUrl;
                } elseif(substr($fileOrUrl,0,4) == "http") {
                    $options['url'] = $fileOrUrl;
                } else {
                    $options['file'] = $fileOrUrl;
                }
            }
        }
        if(isset($options['file'])) {
            $this->fileId($options['file']);
        }
        if(isset($options['url'])) {
            $this->fileUrl($options['url']);
        }
        if(isset($options['before'])) {
            $this->before($options['before']);
        }
        if(isset($options['after'])) {
            $this->after($options['after']);
        }
        if(isset($options['prompt'])) {
            $this->prompt($options['prompt']);
        }
        if(isset($options['assistant'])) {
            $this->assistant($options['assistant']);
        }
        if(isset($options['detail'])) {
            $this->detail($options['detail']);
        }
        if(isset($options['retryLimit'])) {
            $this->retryLimit($options['retryLimit']);
        }
        if(isset($options['retryDelay'])) {
            $this->retryDelay($options['retryDelay']);
        }
    }

    //actions

    public function cleanup() {
        foreach($this->cleanupFiles as $i=>$file) {
            if(file_exists($file)) {
                unlink($file);
                unset($this->cleanupFiles[$i]);
            }
        }
    }

    public function tokens() {
        if($this->result) {
            if($this->assistant) {
                $encode = new Encode($this->assistant);
            } else {
                $encode = new Encode($this->chat);
            }
            $encode->encode($this->result);
            return $encode->count();
        }
    }

    /**
     * @param bool $force
     * @param OpenAIAssistant|string $assistant
     * @return string
     */
    public function run($assistant=null) {
        BoruAI::printDebug("OCR","Running OCR");
        if($assistant) {
            $this->assistant($assistant);
        }
        $runs=0;
        $limit = $this->retryLimit();
        if($limit < 1) {
            $limit = 1;
        }
        $result = "";
        while($runs < $limit) {
            if($this->assistant()) {
                $result = $this->runAssistant();
            } else {
                $result = $this->runChat();
            }
            //if not ##ERROR##, return.. otherwise we keep going
            if(substr($result,0,9) != "##ERROR##") {
                BoruAI::printDebug("OCR","Completed");
                $this->result = $result;
                return $result;
            }
            $runs++;
            usleep($this->retryDelay() * 1000000);
        }
        $result = $this->runFallback();
        if(empty($result)) {
            BoruAI::printDebug("OCR","Failed");
        } else {
            BoruAI::printDebug("OCR","Completed");
        }
        $this->result = $result;
        return $result;
    }

    public function runAssistant() {
        $assistant = $this->assistant();
        if($this->fileModel && !$this->fileModel->fileid()) {
            $this->fileModel->upload();
            if(!$this->fileModel->fileid()) {
                throw new \Exception("File could not be uploaded");
            }
            $this->fileId($this->fileModel->fileid());
        }
        $messages = $this->makeMessages();
        foreach($messages as $message) {
            $assistant->addMessage("user",$message);
        }
        $assistant->addMessage("user","Only return the OCR text. Do not include any other information. If there is an error, please return ##ERROR##");
        
        $result = $assistant->output("\n");
        $result = self::parseResult($result);

        $this->result = $result;
        return $result;
    }

    public function runChat() {
        $chat = new OpenAIChat();
        $this->chat = $chat;
        $chat->addMessage("user","Please OCR this image for me.");
        $messages = $this->makeMessages();
        foreach($messages as $message) {
            $chat->addMessage("user",$message);
        }
        $chat->addMessage("user","Only return the OCR text. Do not include any other information. If there is an error, please return ##ERROR##");
        
        $result = $chat->run();
        $result = self::parseResult($result);

        $this->result = $result;
        return $result;
    }

    public function runFallback(){
        $result="";
        
        $ocrSpaceApiKey = BoruAI::config("ocrspace.api_key", false);      
        if($ocrSpaceApiKey === false || empty($ocrSpaceApiKey)) { 
            /// API key not found
            return $result;  // ""
        }
        BoruAI::printDebug("OCR","Running OCR Fallback");
        $ocrUrl = BoruAI::config("ocrspace.url", "https://api.ocr.space/parse/image");
        $ocrEngine = BoruAI::config("ocrspace.engine", 2);

        // send to OCRSpace (from mva's OCR), need to get Boru API Key
        $http_post_fields = array('apikey'=>$ocrSpaceApiKey,'OCREngine'=>$ocrEngine);
        $http_post_fields = $this->makeOcrSpaceParameters($http_post_fields);
        $curl_handle = curl_init();
        curl_setopt($curl_handle, CURLOPT_URL, $ocrUrl);
        curl_setopt($curl_handle, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl_handle, CURLOPT_POST, true);
        curl_setopt($curl_handle, CURLOPT_POSTFIELDS, $http_post_fields);
         
        $returned_data = curl_exec($curl_handle);
        curl_close($curl_handle);
        $response = json_decode($returned_data,true);
        if(!$response || !isset($response['ParsedResults']) || !isset($response['ParsedResults'][0]) || !isset($response['ParsedResults'][0]['ParsedText']))
        {
            return $result;  // ""
        }
        $this->result = $response['ParsedResults'][0]['ParsedText'];
        $this->cleanup();
        return $this->result;
    }

    public function makeMessages() {
        $messages = [];
        if(!$this->fileId() && !$this->fileUrl()) {
            if($this->fileModel && file_exists($this->fileModel->path())) {
                
            } else {
                throw new \Exception("fileId or fileUrl must be provided");
            }
        }
        if(!empty($this->before)) {
            $content = new Content();
            foreach($this->before as $before) {
                $content->addText($before);
            }
            $messages[] = $content;
        }
        $content = new Content();
        if(!empty($this->prompt)) {
            foreach($this->prompt as $prompt) {
                $content->addText($prompt);
            }
        }
        if($this->fileId() && $this->assistant()) {
            $content->addImageFile($this->fileId(),$this->detail());
        } elseif($this->fileId() && !$this->fileModel) {
            $file = OpenAIFile::fromId($this->fileModel->fileid());
            $fileContent = $file->asJpg(true);
            $content->addBase64Image($fileContent,"image/jpeg");
        } elseif(!$this->fileUrl() && $this->fileModel) {
            if(file_exists($this->fileModel->path())) {
                $content->addBase64Image($this->fileModel->path());
                $this->cleanupFiles[] = $this->fileModel->path();
            } else {
                throw new \Exception("File not found");
            }
        } else {
            $content->addImageUrl($this->fileUrl());
        }

        $messages[] = $content;
        if(!empty($this->after)) {
            $content = new Content();
            foreach($this->after as $after) {
                $content->addText($after);
            }
            $messages[] = $content;
        }
        return $messages;
    }

    //getter and setter methods

    public function fileId($fileId=null) {
        if ($fileId) {
            $this->fileId = $fileId;
        }
        return $this->fileId;
    }
    public function fileUrl($fileUrl=null) {
        if ($fileUrl) {
            $this->fileUrl = $fileUrl;
        }
        return $this->fileUrl;
    }
    public function before($before=null) {
        if ($before) {
            if(is_array($before)) {
                $this->before = $before;
            } else {
                $this->before[] = $before;
            }
        }
        return $this->before;
    }
    public function after($after=null) {
        if ($after) {
            if(is_array($after)) {
                $this->after = $after;
            } else {
                $this->after[] = $after;
            }
        }
        return $this->after;
    }
    public function prompt($prompts=null) {
        if ($prompts) {
            if(is_array($prompts)) {
                $this->prompt = $prompts;
            } else {
                $this->prompt[] = $prompts;
            }
        }
        return $this->prompt;
    }
    public function assistant($assistant=null) {
        if ($assistant) {
            if(is_string($assistant)) {
                $this->assistant = OpenAIAssistant::fromOpenAI($assistant);
            } else {
                $this->assistant = $assistant;
            }
        }
        return $this->assistant;
    }
    public function result() {
        return $this->result;
    }

    public function detail($detail=null) {
        if ($detail !== null) {
            $this->detail = $detail == "high" ? "high" : "low";
            return $this;
        }
        return $this->detail == "high" ? "high" : "low";
    }
    public function highDetail() {
        return $this->detail("high");
    }
    public function lowDetail() {
        return $this->detail("low");
    }

    public function retryLimit($retryLimit=null) {
        if($retryLimit!==null) {
            if(!is_numeric($retryLimit)) {
                throw new \Exception("Retry limit must be a number");
            }
            $this->retryLimit = (int) $retryLimit;
        }
        return $this->retryLimit;
    }
    public function retryDelay($retryDelay=null) {
        if($retryDelay!==null) {
            if(!is_numeric($retryDelay)) {
                throw new \Exception("Retry delay must be a number");
            }
            $this->retryDelay = (float) $retryDelay;
        }
        if(!is_numeric($this->retryDelay)) {
            $this->retryDelay = 0.2;
        }
        return $this->retryDelay;
    }

    //private helpers

    private function makeOcrSpaceParameters($parameters=[]) {
        if($this->fileId()) {
            $file = OpenAIFile::fromId($this->fileId());
            $parameters['base64Image'] = "data:image/jpeg;base64,".$file->asJpg(true);
        } elseif($this->fileUrl()) {
            $parameters['url'] = $this->fileUrl();
        } elseif(!$this->fileUrl() && $this->fileModel) {
            if(file_exists($this->fileModel->path())) {
                //$content->addBase64Image($this->fileModel->path());
                $parameters['base64Image'] = "data:image/jpeg;base64,".$this->fileModel->asJpg(true);
            }
        }
        return $parameters;
    }

    //static methods
    public static function parseResult($result) {
        if(substr($result,0,3) == "```") {
            $result = substr($result,3);
            //remove "plaintext" if it is the next characters
            if(substr($result,0,9) == "plaintext") {
                $result = substr($result,9);
            }
            //remove "markdown" if it is the next characters
            if(substr($result,0,8) == "markdown") {
                $result = substr($result,8);
            }
            //remove "json" if it is the next characters
            if(substr($result,0,4) == "json") {
                $result = substr($result,4);
            }
            if(substr($result,-3) == "```") {
                $result = substr($result,0,-3);
            }
        }
        if(substr($result,0,11) == "##SUCCESS##") {
            $result = substr($result,11);
        }
        return $result;
    }
}