#!/usr/bin/env php
<?php

/**
 * Natural Earth World Map SVG generator for CodePress CMS
 *
 * Converts the Natural Earth 110m TopoJSON (public domain) into a plain SVG
 * where every country path carries its ISO 3166-1 alpha-2 code as the id,
 * so the admin statistics page can colour countries with plain CSS.
 */

/**
 * ISO 3166-1 numeric -> alpha-2. Keys are integers because the TopoJSON ids
 * are zero padded strings ("032"), which must not be compared as strings.
 */
function isoNumericToAlpha2(): array
{
    return [
        4 => 'AF', 8 => 'AL', 10 => 'AQ', 12 => 'DZ', 16 => 'AS', 20 => 'AD', 24 => 'AO', 28 => 'AG',
        31 => 'AZ', 32 => 'AR', 36 => 'AU', 40 => 'AT', 44 => 'BS', 48 => 'BH', 50 => 'BD', 51 => 'AM',
        52 => 'BB', 56 => 'BE', 60 => 'BM', 64 => 'BT', 68 => 'BO', 70 => 'BA', 72 => 'BW', 76 => 'BR',
        84 => 'BZ', 86 => 'IO', 90 => 'SB', 92 => 'VG', 96 => 'BN', 100 => 'BG', 104 => 'MM', 108 => 'BI',
        112 => 'BY', 116 => 'KH', 120 => 'CM', 124 => 'CA', 132 => 'CV', 136 => 'KY', 140 => 'CF',
        144 => 'LK', 148 => 'TD', 152 => 'CL', 156 => 'CN', 158 => 'TW', 170 => 'CO', 174 => 'KM',
        175 => 'YT', 178 => 'CG', 180 => 'CD', 184 => 'CK', 188 => 'CR', 191 => 'HR', 192 => 'CU',
        196 => 'CY', 203 => 'CZ', 204 => 'BJ', 208 => 'DK', 212 => 'DM', 214 => 'DO', 218 => 'EC',
        222 => 'SV', 226 => 'GQ', 231 => 'ET', 232 => 'ER', 233 => 'EE', 234 => 'FO', 238 => 'FK',
        242 => 'FJ', 246 => 'FI', 248 => 'AX', 250 => 'FR', 254 => 'GF', 258 => 'PF', 260 => 'TF',
        262 => 'DJ', 266 => 'GA', 268 => 'GE', 270 => 'GM', 275 => 'PS', 276 => 'DE', 288 => 'GH',
        292 => 'GI', 296 => 'KI', 300 => 'GR', 304 => 'GL', 308 => 'GD', 312 => 'GP', 316 => 'GU',
        320 => 'GT', 324 => 'GN', 328 => 'GY', 332 => 'HT', 340 => 'HN', 344 => 'HK', 348 => 'HU',
        352 => 'IS', 356 => 'IN', 360 => 'ID', 364 => 'IR', 368 => 'IQ', 372 => 'IE', 376 => 'IL',
        380 => 'IT', 384 => 'CI', 388 => 'JM', 392 => 'JP', 398 => 'KZ', 400 => 'JO', 404 => 'KE',
        408 => 'KP', 410 => 'KR', 414 => 'KW', 417 => 'KG', 418 => 'LA', 422 => 'LB', 426 => 'LS',
        428 => 'LV', 430 => 'LR', 434 => 'LY', 438 => 'LI', 440 => 'LT', 442 => 'LU', 446 => 'MO',
        450 => 'MG', 454 => 'MW', 458 => 'MY', 462 => 'MV', 466 => 'ML', 470 => 'MT', 474 => 'MQ',
        478 => 'MR', 480 => 'MU', 484 => 'MX', 492 => 'MC', 496 => 'MN', 498 => 'MD', 499 => 'ME',
        500 => 'MS', 504 => 'MA', 508 => 'MZ', 512 => 'OM', 516 => 'NA', 520 => 'NR', 524 => 'NP',
        528 => 'NL', 531 => 'CW', 533 => 'AW', 534 => 'SX', 540 => 'NC', 548 => 'VU', 554 => 'NZ',
        558 => 'NI', 562 => 'NE', 566 => 'NG', 570 => 'NU', 578 => 'NO', 580 => 'MP', 583 => 'FM',
        584 => 'MH', 585 => 'PW', 586 => 'PK', 591 => 'PA', 598 => 'PG', 600 => 'PY', 604 => 'PE',
        608 => 'PH', 612 => 'PN', 616 => 'PL', 620 => 'PT', 624 => 'GW', 626 => 'TL', 630 => 'PR',
        634 => 'QA', 638 => 'RE', 642 => 'RO', 643 => 'RU', 646 => 'RW', 652 => 'BL', 654 => 'SH',
        659 => 'KN', 660 => 'AI', 662 => 'LC', 663 => 'MF', 666 => 'PM', 670 => 'VC', 674 => 'SM',
        678 => 'ST', 682 => 'SA', 686 => 'SN', 688 => 'RS', 690 => 'SC', 694 => 'SL', 702 => 'SG',
        703 => 'SK', 704 => 'VN', 705 => 'SI', 706 => 'SO', 710 => 'ZA', 716 => 'ZW', 724 => 'ES',
        728 => 'SS', 729 => 'SD', 732 => 'EH', 740 => 'SR', 744 => 'SJ', 748 => 'SZ', 752 => 'SE',
        756 => 'CH', 760 => 'SY', 762 => 'TJ', 764 => 'TH', 768 => 'TG', 772 => 'TK', 776 => 'TO',
        780 => 'TT', 784 => 'AE', 788 => 'TN', 792 => 'TR', 795 => 'TM', 796 => 'TC', 798 => 'TV',
        800 => 'UG', 804 => 'UA', 807 => 'MK', 818 => 'EG', 826 => 'GB', 831 => 'GG', 832 => 'JE',
        833 => 'IM', 834 => 'TZ', 840 => 'US', 850 => 'VI', 854 => 'BF', 858 => 'UY', 860 => 'UZ',
        862 => 'VE', 876 => 'WF', 882 => 'WS', 887 => 'YE', 894 => 'ZM',
    ];
}

/**
 * Natural Earth includes a few disputed areas without an official numeric id.
 * They are matched on their English name instead.
 */
function nameToAlpha2(): array
{
    return [
        'kosovo' => 'XK',
        'somaliland' => 'SO',
        'n. cyprus' => 'CY',
    ];
}

function generateWorldMapSvg(bool $verbose = false): bool
{
    $outPath = dirname(__DIR__) . '/public/assets/img/world-map.svg';
    $imgDir = dirname($outPath);
    if (!is_dir($imgDir)) {
        @mkdir($imgDir, 0755, true);
    }

    $topoUrl = 'https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json';
    $cacheFile = sys_get_temp_dir() . '/world-atlas-110m.json';

    if (!file_exists($cacheFile) || filesize($cacheFile) < 1000) {
        $ctx = stream_context_create(['http' => ['timeout' => 20, 'user_agent' => 'CodePressCMS']]);
        $data = @file_get_contents($topoUrl, false, $ctx);
        if ($data) {
            file_put_contents($cacheFile, $data);
        }
    }

    if (!file_exists($cacheFile)) {
        return false;
    }

    $topo = json_decode(file_get_contents($cacheFile), true);
    if (!$topo || empty($topo['objects']['countries']['geometries'])) {
        return false;
    }

    // Canvas: equirectangular, cropped to the usual web-map latitude band so
    // Antarctica does not dominate and the Arctic is not overly stretched.
    $canvasW = 1000.0;
    $latTop = 84.0;
    $latBottom = -60.0;
    $canvasH = $canvasW * (($latTop - $latBottom) / 360.0);

    $scale = $topo['transform']['scale'];
    $translate = $topo['transform']['translate'];

    // Decode delta-encoded arcs into plain lon/lat pairs. Projection happens
    // later, after antimeridian handling, because that works in degrees.
    $decodedArcs = [];
    foreach ($topo['arcs'] as $arcIndex => $arc) {
        $x = 0;
        $y = 0;
        $points = [];
        foreach ($arc as $delta) {
            $x += $delta[0];
            $y += $delta[1];
            $points[] = [
                $x * $scale[0] + $translate[0],
                $y * $scale[1] + $translate[1],
            ];
        }
        $decodedArcs[$arcIndex] = $points;
    }

    $numericMap = isoNumericToAlpha2();
    $nameMap = nameToAlpha2();

    // Collect paths per country code (Natural Earth can list a code more than once)
    $pathsByCode = [];
    $names = [];
    $skipped = [];

    foreach ($topo['objects']['countries']['geometries'] as $geo) {
        $rawId = $geo['id'] ?? '';
        $countryName = $geo['properties']['name'] ?? 'Unknown';

        $alpha2 = null;
        if ($rawId !== '' && ctype_digit((string)$rawId)) {
            $alpha2 = $numericMap[(int)$rawId] ?? null;
        }
        if ($alpha2 === null) {
            $alpha2 = $nameMap[strtolower($countryName)] ?? null;
        }

        if ($alpha2 === null) {
            $skipped[] = $rawId . ' ' . $countryName;
            continue;
        }

        // Antarctica falls outside the cropped canvas
        if ($alpha2 === 'AQ') {
            continue;
        }

        $type = $geo['type'];
        if ($type === 'Polygon') {
            $polygons = [$geo['arcs']];
        } elseif ($type === 'MultiPolygon') {
            $polygons = $geo['arcs'];
        } else {
            continue;
        }

        $pathD = '';
        foreach ($polygons as $rings) {
            foreach ($rings as $ring) {
                $ringPoints = [];
                foreach ($ring as $arcIdx) {
                    $reversed = $arcIdx < 0;
                    $actualIdx = $reversed ? ~$arcIdx : $arcIdx;
                    $arcPoints = $decodedArcs[$actualIdx] ?? [];
                    if ($reversed) {
                        $arcPoints = array_reverse($arcPoints);
                    }
                    if (!empty($ringPoints) && !empty($arcPoints)) {
                        array_shift($arcPoints); // drop point shared with previous arc
                    }
                    foreach ($arcPoints as $pt) {
                        $ringPoints[] = $pt;
                    }
                }

                if (count($ringPoints) < 3) {
                    continue;
                }

                // Unwrap longitudes so a ring crossing the antimeridian stays
                // continuous instead of jumping from +180 to -180 and smearing
                // a band right across the map (Russia, Fiji).
                $offset = 0.0;
                $unwrapped = [];
                $prevLon = null;
                foreach ($ringPoints as $pt) {
                    $lon = $pt[0];
                    if ($prevLon !== null) {
                        $step = $lon + $offset - $prevLon;
                        if ($step > 180.0) {
                            $offset -= 360.0;
                        } elseif ($step < -180.0) {
                            $offset += 360.0;
                        }
                    }
                    $lonAdj = $lon + $offset;
                    $unwrapped[] = [$lonAdj, $pt[1]];
                    $prevLon = $lonAdj;
                }

                $lons = array_column($unwrapped, 0);
                $minLon = min($lons);
                $maxLon = max($lons);

                // Draw the ring once normally, and once shifted a full turn when
                // it sticks out past a map edge. Anything outside the viewBox is
                // clipped by the SVG itself, so both edges end up correct.
                $shifts = [0.0];
                if ($maxLon > 180.0) {
                    $shifts[] = -360.0;
                }
                if ($minLon < -180.0) {
                    $shifts[] = 360.0;
                }

                foreach ($shifts as $shift) {
                    if ($shift !== 0.0
                        && ($minLon + $shift > 180.0 || $maxLon + $shift < -180.0)) {
                        continue; // fully off-canvas
                    }

                    $segments = [];
                    $visible = false;
                    foreach ($unwrapped as $i => $pt) {
                        $lon = $pt[0] + $shift;
                        $lat = $pt[1];

                        $svgX = ($lon + 180.0) * ($canvasW / 360.0);
                        $svgY = ($latTop - $lat) * ($canvasH / ($latTop - $latBottom));

                        if ($svgX >= -50 && $svgX <= $canvasW + 50 && $svgY <= $canvasH + 50) {
                            $visible = true;
                        }

                        $segments[] = ($i === 0 ? 'M' : 'L') . round($svgX, 2) . ',' . round($svgY, 2);
                    }

                    if ($visible) {
                        $pathD .= implode(' ', $segments) . ' Z ';
                    }
                }
            }
        }

        $pathD = trim($pathD);
        if ($pathD === '') {
            continue;
        }

        if (!isset($pathsByCode[$alpha2])) {
            $pathsByCode[$alpha2] = '';
            $names[$alpha2] = $countryName;
        }
        $pathsByCode[$alpha2] .= ($pathsByCode[$alpha2] === '' ? '' : ' ') . $pathD;
    }

    ksort($pathsByCode);

    $svgPaths = [];
    foreach ($pathsByCode as $code => $d) {
        $svgPaths[] = sprintf(
            '  <path id="%s" data-name="%s" class="country" d="%s"><title>%s</title></path>',
            $code,
            htmlspecialchars($names[$code], ENT_QUOTES, 'UTF-8'),
            $d,
            htmlspecialchars($names[$code], ENT_QUOTES, 'UTF-8')
        );
    }

    $h = round($canvasH, 2);
    $svg = '<?xml version="1.0" encoding="UTF-8"?>' . "\n"
        . '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' . (int)$canvasW . ' ' . $h . '"'
        . ' width="100%" height="auto" preserveAspectRatio="xMidYMid meet"'
        . ' fill-rule="evenodd" class="codepress-world-map" role="img"'
        . ' aria-label="Wereldkaart met bezoekers per land">' . "\n"
        . '  <style>' . "\n"
        . '    .codepress-world-map { display: block; background: #eaf2fb; }' . "\n"
        . '    .codepress-world-map .country { fill: #dfe4ea; stroke: #ffffff; stroke-width: 0.5;'
        . ' stroke-linejoin: round; vector-effect: non-scaling-stroke; transition: fill .15s ease; }' . "\n"
        . '    .codepress-world-map .country:hover { fill: #ffc107; }' . "\n"
        . '  </style>' . "\n"
        . implode("\n", $svgPaths) . "\n"
        . '</svg>' . "\n";

    file_put_contents($outPath, $svg);

    if ($verbose) {
        echo 'Landen getekend: ' . count($pathsByCode) . PHP_EOL;
        echo 'Canvas: ' . (int)$canvasW . ' x ' . $h . PHP_EOL;
        echo 'Bestandsgrootte: ' . round(filesize($outPath) / 1024) . ' KB' . PHP_EOL;
        if ($skipped) {
            echo 'Overgeslagen (geen ISO-code): ' . implode(', ', $skipped) . PHP_EOL;
        }
    }

    return true;
}

if (php_sapi_name() === 'cli') {
    echo "Wereldkaart SVG genereren uit Natural Earth TopoJSON...\n";
    if (generateWorldMapSvg(true)) {
        echo "Klaar: public/assets/img/world-map.svg\n";
    } else {
        echo "Fout bij genereren van de wereldkaart.\n";
        exit(1);
    }
}
