<?php
/**
 * Emoji picker data generator (dev tool). Ported from ListoWP's Listo_Emoji_Make_Data.
 *
 * Produces TWO things from the Unicode standard, so the emoji set is grounded in it rather than
 * hand-curated:
 *
 *  1. THE SET — `assets/emoji/set.json` (ships in the plugin; loaded lazily by PeepSoEmoji).
 *     Shape: { "😀": { "g": "Smileys & Emotion", "en": "grinning face grin smile" }, … } in
 *     emoji-test order. Derived from Unicode **emoji-test.txt** at a pinned version (EMOJI_VERSION) —
 *     because each version's file is cumulative, using the 12.0 file IS the "≤ E12.0" cutoff. The filter
 *     (UTS #51): keep status `fully-qualified`, then EXCLUDE skin-tone (1F3FB–1F3FF), ZWJ (200D),
 *     regional-indicator flags (1F1E6–1F1FF), tags (E0020–E007F), keycaps (20E3); finally a
 *     SINGLE-SCALAR test — after dropping a trailing VS16 (FE0F) exactly one codepoint remains (this,
 *     not the status, is what guarantees a single OS-rendered glyph). The emitted glyph KEEPS its FE0F
 *     so it renders emoji- (not text-) presentation. English keywords come from CLDR `en` if available,
 *     else the emoji name — the always-available offline fallback.
 *
 *  2. THE KEYWORDS — `language/devops/emoji/<VER>/<locale>.json` (dev-only output; the site owner uploads
 *     them to `<base>/<VER>/` for runtime localized search). Per-locale CLDR annotation keywords.
 *
 * Source data — Unicode CLDR annotations (the data OS emoji keyboards search):
 *     npm i cldr-annotations-full cldr-annotations-derived-full
 * Run (Local's bundled php; e.g. .../lightning-services/php-8.3.../bin/.../php):
 *     php 3/classes/developer-tools/emoji-make-data.php --emoji-test=/path/emoji-test.txt --cldr=./node_modules --out=language/devops/emoji
 * Flags (all optional): --emoji-test (path or URL; default fetches unicode.org/Public/emoji/<VER>/emoji-test.txt),
 *   --cldr ($PEEPSO_CLDR_DIR or ./node_modules), --out (language/devops/emoji).
 *
 * NOTE: PeepSo's autoloader is lazy (by class name), so this file is NOT included on normal requests —
 * CLI is the primary entry point. The admin web trigger below only fires if the file is explicitly
 * required by a developer. Stripped from the release zip.
 */
class PeepSoEmoji_Make_Data
{
	/** Pinned emoji-test.txt version == the safe baseline. The file is cumulative, so this is the cutoff. */
	const EMOJI_VERSION = '12.0';

	/** Our keyword-file codes → the CLDR locale code(s) to try, most-specific first. */
	const CLDR_LOCALE = array(
		'en'    => array('en'),
		'pl'    => array('pl'),
		'es'    => array('es'),
		'it'    => array('it'),
		'de'    => array('de'),
		'fr'    => array('fr'),
		'nl'    => array('nl'),
		'pt'    => array('pt-PT', 'pt'),
		'pt_BR' => array('pt', 'pt-PT'),
		'ru'    => array('ru'),
		'uk'    => array('uk'),
		'ja'    => array('ja'),
		'bg'    => array('bg'),
		'mk'    => array('mk'),
		'tr'    => array('tr'),
		'vi'    => array('vi'),
		'id'    => array('id'),
		'zh'    => array('zh', 'zh-Hans'),
	);

	/** Generate set.json + the per-locale keyword files. Returns a plain-text report. */
	public static function run($cldr_dir = NULL, $out_dir = NULL, $emoji_test = NULL)
	{
		$plugin_dir = dirname(__DIR__, 3) . '/';

		$emoji_test = $emoji_test ?: ('https://www.unicode.org/Public/emoji/' . self::EMOJI_VERSION . '/emoji-test.txt');
		$cldr_dir   = $cldr_dir ?: (getenv('PEEPSO_CLDR_DIR') ?: $plugin_dir . 'node_modules');
		$out_dir    = $out_dir ?: ($plugin_dir . 'language/devops/emoji');

		$raw = self::fetch($emoji_test);
		if ('' === $raw) {
			return "ERROR: could not read emoji-test.txt from $emoji_test\n";
		}

		// 1. Derive the set from emoji-test.txt (ordered).
		$derived = self::parse_emoji_test($raw); // glyph => [ 'g' => group, 'name' => name ]
		$glyphs  = array_keys($derived);
		if (!$glyphs) {
			return "ERROR: parsed 0 emoji from emoji-test.txt — wrong format?\n";
		}

		// 2. English keywords: CLDR `en` if present, else the emoji name. Bake into the set.
		$en_ann = self::load_annotations($cldr_dir, 'en');
		$set    = array();
		foreach ($derived as $glyph => $meta) {
			$kw = self::cldr_terms($en_ann, $glyph);
			if (!$kw) {
				$kw = self::name_terms($meta['name']);
			}
			$set[$glyph] = array('g' => $meta['g'], 'en' => implode(' ', $kw));
		}

		// 3. Write the shipped set.json (lazily loaded by PeepSoEmoji).
		$set_file = $plugin_dir . 'assets/emoji/set.json';
		if (!is_dir(dirname($set_file)) && !mkdir(dirname($set_file), 0775, TRUE) && !is_dir(dirname($set_file))) {
			return "ERROR: cannot create " . dirname($set_file) . "\n";
		}
		file_put_contents($set_file, json_encode($set, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));

		$report  = "Emoji SET (E" . self::EMOJI_VERSION . ", RGI, single, no skin-tone/ZWJ/flags): " . count($set) . " glyphs\n";
		$report .= "  → $set_file\n";
		$report .= "CLDR source: $cldr_dir\n\nPer-locale keyword files:\n";

		// 4. Per-locale keyword JSON for the derived glyphs.
		$out_v = rtrim($out_dir, '/') . '/' . self::EMOJI_VERSION;
		if (!is_dir($out_v) && !mkdir($out_v, 0775, TRUE) && !is_dir($out_v)) {
			return $report . "ERROR: cannot create output dir $out_v\n";
		}

		foreach (self::CLDR_LOCALE as $code => $cldr_codes) {
			$map = self::keywords_for($cldr_dir, $cldr_codes, $glyphs);

			// English always backfills from the baked set so en.json is never empty.
			if ('en' === $code) {
				foreach ($glyphs as $g) {
					if (empty($map[$g])) {
						$map[$g] = preg_split('/\s+/u', $set[$g]['en']);
					}
				}
			}

			$found = count(array_filter($map));
			$file  = $out_v . '/' . $code . '.json';
			file_put_contents($file, json_encode($map, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
			$report .= sprintf("  %-6s %4d/%d → %s\n", $code, $found, count($glyphs), basename($file));
		}

		$report .= "\nDone. Upload $out_v to the keyword host so it serves <base>/" . self::EMOJI_VERSION . "/<locale>.json,\n";
		$report .= "i.e. https://peepso.com/static/emoji/" . self::EMOJI_VERSION . "/<locale>.json (the default base in PeepSoEmoji::KEYWORDS_BASE_URL).\n";
		$report .= "Override the host with the PEEPSO_EMOJI_KEYWORDS_URL constant or the peepso_emoji_keywords_base_url filter.\n";
		return $report;
	}

	/** Parse emoji-test.txt → ordered [ glyph => ['g'=>group,'name'=>name] ], applying the UTS #51 filter. */
	private static function parse_emoji_test($raw)
	{
		$out   = array();
		$group = '';
		foreach (preg_split('/\R/', $raw) as $line) {
			if ('' === $line) {
				continue;
			}
			if ('#' === $line[0]) {
				if (0 === strpos($line, '# group:')) {
					$group = trim(substr($line, strlen('# group:')));
				}
				continue;
			}
			// "CODEPOINTS ; status # GLYPH NAME"
			$parts = explode(';', $line, 2);
			if (count($parts) < 2) {
				continue;
			}
			$codepoints = preg_split('/\s+/', trim($parts[0]));
			$rest       = explode('#', $parts[1], 2);
			$status     = trim($rest[0]);
			$comment    = isset($rest[1]) ? trim($rest[1]) : '';

			if ('fully-qualified' !== $status) {
				continue;
			}
			if (!self::is_safe_single($codepoints)) {
				continue;
			}

			$glyph = self::build_glyph($codepoints);
			if ('' === $glyph) {
				continue;
			}
			// Comment is "GLYPH NAME" (12.0 has no E-version token; strip one defensively if present).
			$bits = explode(' ', $comment, 2);
			$name = isset($bits[1]) ? $bits[1] : '';
			$name = preg_replace('/^E\d+\.\d+\s+/', '', $name);

			$out[$glyph] = array('g' => $group, 'name' => $name);
		}
		return $out;
	}

	/**
	 * UTS #51 exclusions + single-scalar test. $codepoints = array of hex strings. Returns true for a
	 * single OS-renderable glyph: no skin-tone / ZWJ / regional-indicator / tag / keycap, and exactly
	 * one codepoint after dropping a trailing VS16 (FE0F).
	 */
	private static function is_safe_single($codepoints)
	{
		$scalars = array();
		foreach ($codepoints as $hex) {
			$cp = hexdec($hex);
			if ($cp >= 0x1F3FB && $cp <= 0x1F3FF) { return FALSE; } // skin tone
			if (0x200D === $cp)                    { return FALSE; } // ZWJ
			if ($cp >= 0x1F1E6 && $cp <= 0x1F1FF) { return FALSE; } // regional indicator (flags)
			if ($cp >= 0xE0020 && $cp <= 0xE007F) { return FALSE; } // tag chars (subdivision flags)
			if (0x20E3 === $cp)                    { return FALSE; } // keycap combiner
			if (0xFE0F !== $cp) { $scalars[] = $cp; } // ignore VS16 for the count
		}
		return 1 === count($scalars);
	}

	/** Build the UTF-8 glyph from hex codepoints (keeps FE0F for emoji presentation). */
	private static function build_glyph($codepoints)
	{
		$g = '';
		foreach ($codepoints as $hex) {
			$g .= mb_chr(hexdec($hex), 'UTF-8');
		}
		return $g;
	}

	/** Build { glyph => [keywords] } for the first CLDR locale that resolves, for the given glyphs. */
	private static function keywords_for($cldr_dir, $cldr_codes, $glyphs)
	{
		$ann = array();
		foreach ($cldr_codes as $cc) {
			$ann = self::load_annotations($cldr_dir, $cc);
			if ($ann) {
				break;
			}
		}
		$map = array();
		foreach ($glyphs as $g) {
			$map[$g] = self::cldr_terms($ann, $g);
		}
		return $map;
	}

	/** Terms for one glyph from a loaded annotation map (tries FE0F variants). */
	private static function cldr_terms($ann, $glyph)
	{
		foreach (self::glyph_variants($glyph) as $key) {
			if (!empty($ann[$key])) {
				return self::clean_terms($ann[$key]);
			}
		}
		return array();
	}

	/**
	 * Merge a CLDR locale's base + derived annotations into { glyph => [default…, tts…] }. Reads the
	 * cldr-json layout under any *<pkg>* dir (recursive glob), so node_modules or a raw checkout works.
	 */
	private static function load_annotations($cldr_dir, $loc)
	{
		$out = array();
		foreach (array('annotations', 'annotationsDerived') as $kind) {
			$paths = glob(rtrim($cldr_dir, '/') . '/*/' . $kind . '/' . $loc . '/annotations.json');
			foreach ((array) $paths as $path) {
				$json = json_decode((string) file_get_contents($path), TRUE);
				$node = isset($json[$kind]['annotations']) ? $json[$kind]['annotations'] : array();
				foreach ((array) $node as $glyph => $a) {
					$d = array_merge((array) (isset($a['default']) ? $a['default'] : array()), (array) (isset($a['tts']) ? $a['tts'] : array()));
					if ($d) {
						$out[$glyph] = array_merge(isset($out[$glyph]) ? $out[$glyph] : array(), $d);
					}
				}
			}
		}
		return $out;
	}

	/** A glyph and its FE0F-variation forms, so we match CLDR keys whether or not they carry U+FE0F. */
	private static function glyph_variants($g)
	{
		$bare = str_replace("\xEF\xB8\x8F", '', $g);
		return array_values(array_unique(array($g, $bare, $bare . "\xEF\xB8\x8F")));
	}

	/** Words from an emoji name (lower-case, de-duplicated) — the English fallback keyword source. */
	private static function name_terms($name)
	{
		return self::clean_terms(preg_split('/[^\p{L}\p{N}]+/u', (string) $name));
	}

	/** Lower-case, trim, drop blanks, de-duplicate, cap to a sane number of terms. */
	private static function clean_terms($terms)
	{
		$out = array();
		foreach ((array) $terms as $t) {
			$t = trim(function_exists('mb_strtolower') ? mb_strtolower($t) : strtolower($t));
			if ('' !== $t && !in_array($t, $out, TRUE)) {
				$out[] = $t;
			}
		}
		return array_slice($out, 0, 12);
	}

	/** Read a local path or fetch a URL (wp_remote_get if available, else file_get_contents). */
	private static function fetch($src)
	{
		if (preg_match('#^https?://#', $src)) {
			if (function_exists('wp_remote_get')) {
				$resp = wp_remote_get($src, array('timeout' => 20));
				if (!is_wp_error($resp) && 200 === (int) wp_remote_retrieve_response_code($resp)) {
					return (string) wp_remote_retrieve_body($resp);
				}
				return '';
			}
			return (string) @file_get_contents($src);
		}
		return is_readable($src) ? (string) file_get_contents($src) : '';
	}
}

// Web trigger — admins only. PeepSo's lazy autoloader does NOT include this file on normal requests, so
// this fires only when a developer has explicitly required the file. CLI (below) is the primary path.
if (function_exists('add_action')) {
	add_action('template_redirect', function () {
		if (isset($_GET['peepso_make_emoji']) && current_user_can('manage_options')) {
			header('Content-Type: text/plain; charset=utf-8');
			$cldr = isset($_GET['cldr']) ? sanitize_text_field(wp_unslash($_GET['cldr'])) : NULL;
			echo PeepSoEmoji_Make_Data::run($cldr);
			exit;
		}
	});
}

// CLI: only when this file is the script being executed directly. Flags: --emoji-test --cldr --out.
if (PHP_SAPI === 'cli' && isset($argv[0]) && realpath($argv[0]) === __FILE__) {
	$opts = getopt('', array('cldr::', 'out::', 'emoji-test::'));
	echo PeepSoEmoji_Make_Data::run(
		isset($opts['cldr']) ? $opts['cldr'] : NULL,
		isset($opts['out']) ? $opts['out'] : NULL,
		isset($opts['emoji-test']) ? $opts['emoji-test'] : NULL
	);
}
