<?php

/**
 * Vvveb
 *
 * Copyright (C) 2022  Ziadin Givan
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

namespace Vvveb\System;

use function Vvveb\globBrace;
use Vvveb\System\Core\FrontController;

class PageCache {
	const STALE_EXT = '.old';

	const LOCK_EXT = '.new';

	const CACHE_DIR = PAGE_CACHE_DIR;

	const MAX_LOCK_SECONDS = 10;

	static $cacheCookies = ['user', 'cart', 'admin', 'nocache'];

	private $fileName;

	private $uri;

	private $canCache; //can serve cached page

	private $canSaveCache; //can save page in cache

	private $cacheFolder;

	static private $enabled;

	static private $instance;

	static function getInstance() {
		if (! self :: $instance) {
			self :: $instance = new self();
		}

		return self :: $instance;
	}

	function __construct($host = null) {
		$this->cacheFolder = $this->cacheFolder($host);
		$this->uri         = $this->uri();
		if ($this->uri) {
			$this->fileName    = $this->cacheFolder . $this->uri;
		}
		//$this->canSaveCache    = $this->canSaveCache();
	}

	function cacheFolder($host) {
		$host    = $host ?? $_SERVER['HTTP_HOST'] ?? '';
		$hostWp  = substr($host, 0, strpos($host, ':') ?: null);

		return DIR_PUBLIC . self :: CACHE_DIR . ($hostWp ?? 'default');
	}

	function uri($url = null) {
		$uri = $url ?? $_SERVER['REQUEST_URI'] ?? '/';

		if (strlen($uri) > 300) {
			return false;
		}

		if (substr($uri, -1) == '/') {
			$uri .= 'index.html';
		}

		return $uri;
	}

	function isGenerating() {
		return is_file($this->fileName . self :: LOCK_EXT);
	}

	function isGeneratingStuck() {
		return time() - filemtime($this->fileName . self :: LOCK_EXT) > self :: MAX_LOCK_SECONDS;
	}

	function isStale() {
		return is_file($this->fileName . self :: STALE_EXT);
	}

	function getStale() {
		return $this->fileName . self :: STALE_EXT;
	}

	function startGenerating() {
		$dir = dirname($this->fileName);

		if (is_dir($this->fileName)) {
			$this->fileName .= DS . 'index.html';
		}

		if (! is_dir($dir)) {
			//if page with the same name as folder remove
			if (file_exists($dir)) {
				unlink($dir);
			}

			if (! mkdir($dir, (0755 & ~umask()), true)) {
				return false;
			}
		}

		//keep old cache to serve while generating
		if (file_exists($this->fileName)) {
			rename($this->fileName, $this->fileName . self :: STALE_EXT);
		}

		return touch($this->fileName . self :: LOCK_EXT);
	}

	function startCapture() {
		ob_start();
	}

	function validUrl($url) {
		//no ?&\ or .. in the url and the number of levels should not exceed 4
		$invalid = strpbrk($url, '?&\\') || (strpos($url, '..') !== false) || (substr_count($url,'/') > 4);

		if (! $invalid) {
			foreach (['/user', '/cart', '/checkout', '/feed'] as $a) {
				if (strncmp($url, $a, strlen($a)) === 0) {
					$invalid = true;

					break;
				}
			}
		}

		return ! $invalid;
	}

	function canCache() {
		if ($this->canCache !== NULL) {
			return $this->canCache;
		}

		if (! $this->uri ||
			APP == 'admin' || //no cache for admin
			APP == 'install' || //no cache for install
			! empty($_POST) || //forms posts
			isset($_COOKIE['nocache']) || //cookie set by plugin
			isset($_COOKIE['cart']) || // cookie set by add to cart
			isset($_COOKIE['user']) || //cookie set by login
			isset($_COOKIE['admin']) || //cookie set by admin login
			! $this->validUrl($this->uri) //valid url
		) {
			return $this->canCache = false;
		}

		return $this->canCache = true;
	}

	function canSaveCache() {
		if ($this->canSaveCache !== NULL) {
			return $this->canSaveCache;
		}

		if (! $this->canCache ||
			($user = \Vvveb\System\User\User::current()) || //not logged in
			($admin = \Vvveb\System\User\Admin::current())) {
			return $this->canSaveCache = false;
		}

		return $this->canSaveCache = true;
	}

	function hasCache() {
		return is_file($this->fileName);
	}

	function getCache() {
		return readfile($this->fileName);
	}

	function cleanUp() {
		//remove lock and stale cache
		foreach ([$this->fileName . self :: LOCK_EXT,
			$this->fileName . self :: STALE_EXT, ] as $file) {
			if (file_exists($file)) {
				return unlink($file);
			}
		}

		return false;
	}

	function saveCache() {
		$data = ob_get_contents();

		$lock = $this->fileName . self :: LOCK_EXT;

		//if page not found or server error don't cache
		if (FrontController::getStatus() != 200) {
			//remove lock
			if (file_exists($lock)) {
				unlink($lock);
			}

			//remove all empty created folders
			while (
				($dir = dirname($this->fileName)) &&
				(strpos($dir, $this->cacheFolder . DS) !== false) && //don't go above site cache folder
				@rmdir($dir) //try to remove empty folder
			) {
				//go one level up
				$this->fileName = $dir;
			}

			return $data;
		}

		if ($this->canSaveCache &&
			$this->fileName && $data &&
			http_response_code() == 200) {
			//create directory structure
			if (is_dir($this->fileName)) {
				$this->fileName .= DS . 'index.html';
			}

			$dir = dirname($this->fileName);

			if (! file_exists($dir)) {
				if (! mkdir($dir, (0755 & ~umask()), true)) {
					return false;
				}
			}
			//save cache
			file_put_contents($this->fileName, $data);
			//remove lock
			if (file_exists($lock)) {
				unlink($lock);
			}

			//remove stale
			if (file_exists($this->fileName . self :: STALE_EXT)) {
				@unlink($this->fileName . self :: STALE_EXT);
			}
		}

		return $data;
	}

	function purge($path = '/') {
		$folder = $this->cacheFolder . $path;
		$glob   = ['', '*/*/', '*/'];

		//$files = glob($name, GLOB_BRACE);
		$files = globBrace($folder, $glob, '*');

		if ($files) {
			foreach ($files as $file) {
				if ($file[0] === '.') {
					continue;
				}

				if (is_file($file)) {
					if (! @unlink($file)) {
						clearstatcache(false, $file);
					}
				} else {
					/*
					if (! @rmdir($file)) {
						clearstatcache(false, $file);
					}
					*/
				}
			}
		}

		return true;
	}

	static function enable($type = false) {
		$cookie = (in_array($type, self :: $cacheCookies) ? $type : 'nocache');

		if (! self :: $enabled && isset($_COOKIE[$cookie])) {
			@setcookie($cookie, '', time() - 3600, '/');
		}

		self :: $enabled = true;
	}

	static function disable($type = false) {
		$cookie = (in_array($type, self :: $cacheCookies) ? $type : 'nocache');

		if (self :: $enabled && ! isset($_COOKIE[$cookie])) {
			@setcookie($cookie, '1', 0, '/');
		}

		self :: $enabled = false;
	}
}
