<?php declare(strict_types=1);
/**
 * @package messenger
 * @author kputyr
 * @date 17.04.2025
 */

namespace IMATHUZH\Messenger;

use Iterator;

/**
 * Implements a notification system based on a file system.
 * 
 * Each channel of a certain type is associated with a specific file
 * and publishing a message on such channel modifies the access time
 * of the file that can be monitored with inotify. This publisher
 * ignores the content of messages.
 * 
 * In order to better structure the channel files, channel names
 * of supported channels are combined from two parts separated by
 * TouchPublisher::SEPARATOR chacarater: the channel family and
 * the file name. Each family represents a folder in the filesystem
 * and they are provided to the constructor of the publisher.
 * 
 * Example
 * -------
 * $touchPublisher = new TouchPublisher(
 *   '/var/messenger/signals',    // root folder
 *   [                            // channel families
 *      'p' => 'page',
 *      'pa' => 'page/admin',
 *      'cmd' => 'command'
 *   ]  
 * );
 * 
 * // This touches files
 * //   /var/messenger/signals/page/admin/one
 * //   /var/messenger/signals/command/two
 * $touchPublisher(['pa_one', 'cmd_two']);
 */
class TouchPublisher implements MessagePublisherInterface
{
	/** A separator between the family and the file name of a channel */
	const SEPARATOR = '_';

	/** The root folder of channel files */
	protected string $rootDir;

	/** A dictionary of channel families to subfolders */
	protected array $directories;

	public function __construct(
		string $rootDir,
		array $directories
	) {
		// Ensure that $rootDir ends with a slash
		$this->rootDir = rtrim($rootDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
		// Ensure that no path starts with a slash and each ends with a slash
		// unless empty, so that $rootDir . $directories[$key] is a valid
		// path that ends with a slash.
		$this->directories = array_map(
			function($str) {
				$trimmed = trim($str, DIRECTORY_SEPARATOR);
				return $trimmed ? $trimmed . DIRECTORY_SEPARATOR : ''; },
			$directories
		);
	}
	
	/**
	 * Translates channels to a list of file paths
	 */
	protected function filesFromChannels(array $channels): Iterator
	{
		foreach ($channels as $channel) {
			// Supress warnings about undefined keys when $channel
			// has no SEPARATOR - this is tested on the next line
			@list($type, $name) = explode(self::SEPARATOR, $channel, 2);
			if ($name && isset($this->directories[$type])) {
				yield $this->rootDir . $this->directories[$type] . $name;
			}
		}
	}

	/**
	 * Triggers the touch action on a file.
	 * 
	 * Making this a separate method simplifies unit tests.
	 * 
	 * @param string $file   the path to the file to be touched
	 * @return bool          the result of the action
	 */
	protected function action(string $file): bool
	{
		return @touch($file);
	}

	/**
	 * @inheritdoc
	 */
	public function publishMessage(array $channels, string $msg): Iterator
	{
		foreach ($this->filesFromChannels($channels) as $file) {
			if (!$this->action($file)) yield $file => "failed to touch $file";
		}
	}
}
