<?php
/*
 * Copyright (c) 2025, Tribal Limited
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Zenario, Tribal Limited nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL TRIBAL LTD BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
if (!defined('NOT_ACCESSED_DIRECTLY')) exit('This file may not be directly accessed');



//An implementation of Twig_LoaderInterface
//It works with both raw source code and paths to a twig file.
	//If the $name starts with a \n character, we'll treat it as raw code.
	//Otherwise, we'll assume it's a path to a twig file (relative to the CMS_ROOT).
class Zenario_Twig_Loader implements Twig\Loader\LoaderInterface {
    
    //public function getSource($name) {
    //	if (substr($name, 0, 1) === "\n") {
    //		return $name;
    //	} else {
	//    	return file_get_contents(CMS_ROOT. $name);
	//    }
    //}

    public function getCacheKey(string $name): string {
    	return $name;
    }
    

    public function getSourceContext(string $name): Twig\Source {
    	if (substr($name, 0, 1) === "\n") {
	        return new Twig\Source($name, $name);
    	
    	} elseif (strpos($name, '..') !== false) {
	        return new Twig\Source('Invalid include path: '. dirname($name), $name);
    	
    	} elseif (substr($name, -10) !== '.twig.html') {
	        return new Twig\Source('Include file "'. $name. '" does not end with ".twig.html" and was blocked', $name);
    	
    	} else {
    		$path = CMS_ROOT. $name;
	        return new Twig\Source(file_get_contents($path), $name, $path);
	    }
    }

    public function isFresh(string $name, int $time): bool {
    	if (substr($name, 0, 1) === "\n") {
    		return true;
    	
    	} else {
	        return filemtime(CMS_ROOT. $name) <= $time;
	    }
    }
    

    public function exists(string $name) {
    	if (substr($name, 0, 1) === "\n") {
    		return true;
    	
    	} else {
	        return file_exists(CMS_ROOT. $name);
	    }
    }
}





//A copy of the above that always only works with raw source code
class Zenario_Twig_String_Loader implements Twig\Loader\LoaderInterface {
    
    public function getCacheKey(string $name): string {
    	return $name;
    }
    

    public function getSourceContext(string $name): Twig\Source {
        //return new Twig\Source($name, sha1($name));
        return new Twig\Source($name, $name);
    }

    public function isFresh(string $name, int $time): bool {
   		return true;
    }

    public function exists(string $name) {
   		return true;
    }
}


//An implementation of Twig_CacheInterface that saves files to Zenario's cache directory.
//The main reason for the rewrite is so that we use our ze\cache::createDir() function, which has a working garbage collector.
//(Twig doesn't do any garbage collection so old frameworks can clog up the cache/ directory!)
class Zenario_Twig_Cache implements Twig\Cache\CacheInterface {
	
	public function generateKey(string $name, string $className): string {
		$hash = ze::base16To64(str_replace('__TwigTemplate_', '', $className));
		
		return CMS_ROOT. 'cache/frameworks/'. $hash .'/class.php';
	}

    public function load(string $key): void {
        if (file_exists($key)) {
			touch(dirname($key). '/accessed');
			@include_once $key;
		}
    }

    public function write(string $key, string $content): void {
        $dir = basename(dirname($key));
        ze\cache::createDir($dir, 'cache/frameworks', false);
        file_put_contents($key, $content);
        \ze\cache::chmod($key, 0664);
    }

    public function getTimestamp(string $key): int {
        if (!file_exists($key)) {
            return 0;
        }

        return (int) @filemtime($key);
    }
}


//A version of Zenario_Twig_Cache that uses preg_replace() on the generated code as a hack to implement the following two features:
	//Replace calls to twig_get_attribute() with the ?? operator for better efficiency
	//Implement the ability to set the value of array elements
	//Implement a fix for PHP and division by zero error propagation.
//Note that if you use this class, you can no longer pass objects as inputs as the preg_replace()s break support for this
class Zenario_Phi_Twig_Cache extends Zenario_Twig_Cache {
	
    public function write(string $key, string $content): void {
    	
		//Replace calls to twig_get_attribute() with the ?? operator for better efficiency
    	do {
    		$count = 0;
	    	$content = preg_replace('@\btwig_get_attribute\(\$this\-\>env, \$this\-\>getSourceContext\(\), \(?\$context([\[\]\'"\w-]+) \?\? null\)?, ([\'"]?[\w-]+[\'"]?), array\(\)(, [\'"]array[\'"]|)\)@', '(\$context$1[$2] ?? null)', $content, -1, $count);
	    } while ($count > 0);
    	
		//Implement the ability to set the value of array elements.
		//Twig doesn't support setting array keys, so we'll need to use a hack to work around this.
		//Most of the work is done in the Phi Parser function, however we need a preg_replace here just to make sure
		//that the first arguement to the _zPhiSAK_() function can be passed by reference.
    	do {
    		$count = 0;
	    	$content = preg_replace('@\bze\\\\phi\:\:_zPhiSAK_\(\(?\$context([\[\]\'"\w-]+)( \?\? null)\)?@', 'ze\\\\phi::_zPhiSAK_(\$context$1', $content, -1, $count);
	    } while ($count > 0);
	    
	    
		//A fix for PHP and division by zero error propagation.
		//If you divide by zero in PHP, and you don't catch the error very close to the source, there is a bug where
		//some random class instances have their variables all set to null.
		//We'll solve this by putting a try/catch around the doDisplay() function to catch them in the function they occur.
	    $startFrag = 'protected function doDisplay(array $context, array $blocks = []): iterable';
	    $endFrag = 'public function getTemplateName()';
	    
	    $content = str_replace($startFrag, $startFrag. ' {'. "\n      ". 'try ', $content);
	    $content = str_replace($endFrag, '
      catch (\\DivisionByZeroError $e) {
      	if (\\ze\\phi::suppressErrors()) {
      		return null;
      	} else {
      		throw $e;
      	}
      }
    }'. "\n\n    ". $endFrag, $content);
	    
	    
    	parent::write($key, $content);
    }
}


function zenario_callLibFromTwig($lib, $fun, ...$args) {
	
	if ($lib == '') {
		$className = 'ze';
	} else {
		$className = 'ze\\'. $lib;
	}
	
	//Only allow methods that have been white-listed to be called.
	//Methods are white-listed by creating a constant with a specific name.
	$check = $className. '::'. $fun. 'FromTwig';
	
	if (defined($check) && constant($check)) {
		return call_user_func_array($className. '::'. $fun, $args);
	}
}



//Define the phrase() and the nPhrase() functions for use in Twig frameworks.
//These should map to the phrase/nphrase functions of whatever plugin is currently running
function zenario_phrase($text, $replace = []) {
	if (\ze::$plugin) {
		return \ze::$plugin->phraseInHTML($text, $replace);
	}
}

function zenario_nphrase($text, $pluralText = false, $n = 1, $replace = []) {
	if (\ze::$plugin) {
		return \ze::$plugin->nPhraseInHTML($text, $pluralText, $n, $replace);
	}
}