<?php
function fail($app_id, $message) {
    file_put_contents("/dev/stderr", $app_id . ": " . $message . "\n", 8);
    exit(1);
}

function path_join($left, $right) {
    if (string_equals($left, "")) {
        return $right;
    }
    if (string_equals($right, "")) {
        return $left;
    }
    return rtrim($left, "/") . "/" . ltrim($right, "/");
}

function default_home($app_id) {
    $home = getenv("HOME");
    if ($home === false || string_equals($home, "")) {
        fail($app_id, "HOME is not set");
    }
    return $home;
}

function app_id_from_argv($argv) {
    $raw = "";
    if (count($argv) > 0) {
        $raw = $argv[0];
    }
    if (string_equals($raw, "")) {
        return "pw-wine-prefix-delta-startwrapper";
    }
    return basename($raw);
}

function ensure_dir($app_id, $path) {
    if (string_equals($path, "") || is_dir($path)) {
        return;
    }
    if (!mkdir($path, 0775, true) && !is_dir($path)) {
        fail($app_id, "cannot create directory: " . $path);
    }
}

function sha256_file_checked($app_id, $path) {
    $digest = hash_file("sha256", $path);
    if ($digest === false) {
        fail($app_id, "cannot hash file: " . $path);
    }
    return $digest;
}

function manifest_digest($manifest, $relpath) {
    if (!is_file($manifest)) {
        return "";
    }
    $lines = file($manifest, 2);
    if ($lines === false) {
        return "";
    }
    foreach ($lines as $raw_line) {
        $line = trim($raw_line);
        if (string_equals($line, "") || string_equals(substr($line, 0, 1), "#")) {
            continue;
        }
        $parts = explode("\t", $line);
        if (count($parts) >= 2 && string_equals($parts[1], $relpath)) {
            return $parts[0];
        }
    }
    return "";
}

function append_manifest_entry($app_id, $manifest, $digest, $relpath) {
    ensure_dir($app_id, dirname($manifest));
    file_put_contents($manifest, $digest . "\t" . $relpath . "\n", 8);
}

function shell_env_assignment($name, $value) {
    return $name . "=" . escapeshellarg($value) . " ";
}

function run_shell($command, $cwd, $quiet) {
    if (!string_equals($cwd, "")) {
        $command = "cd " . escapeshellarg($cwd) . " && " . $command;
    }
    $code = 0;
    if ($quiet) {
        passthru($command . " >/dev/null 2>&1", $code);
    } else {
        passthru($command, $code);
    }
    return $code;
}

function run_shell_background($command, $cwd) {
    if (!string_equals($cwd, "")) {
        $command = "cd " . escapeshellarg($cwd) . " && " . $command;
    }
    $code = 0;
    passthru("nohup sh -c " . escapeshellarg($command) . " >/dev/null 2>&1 &", $code);
    return $code;
}

function command_path($name) {
    $output = shell_exec("command -v " . escapeshellarg($name) . " 2>/dev/null");
    if ($output === null || $output === false) {
        return "";
    }
    return trim($output);
}

function start_gui_status($app_id, $message) {
    $display = getenv("DISPLAY");
    if ($display === false || string_equals($display, "")) {
        return "";
    }

    $parent_pid = getmypid();
    $watch_prefix = "parent=" . escapeshellarg((string)$parent_pid) . "; (";
    $watch_suffix = ' & child=$!; trap "kill $child 2>/dev/null || true" TERM INT EXIT; while kill -0 $parent 2>/dev/null; do sleep 1; done) >/dev/null 2>&1 & echo $!';

    $zenity = command_path("zenity");
    if (!string_equals($zenity, "")) {
        $command = $watch_prefix . escapeshellarg($zenity)
            . " --info --title " . escapeshellarg($app_id)
            . " --text " . escapeshellarg($message)
            . " --no-wrap --timeout=3600" . $watch_suffix;
        $pid = shell_exec("sh -c " . escapeshellarg($command));
        return $pid === null || $pid === false ? "" : trim($pid);
    }

    $yad = command_path("yad");
    if (!string_equals($yad, "")) {
        $command = $watch_prefix . escapeshellarg($yad)
            . " --title " . escapeshellarg($app_id)
            . " --text " . escapeshellarg($message)
            . " --no-buttons --timeout=3600" . $watch_suffix;
        $pid = shell_exec("sh -c " . escapeshellarg($command));
        return $pid === null || $pid === false ? "" : trim($pid);
    }

    $xmessage = command_path("xmessage");
    if (!string_equals($xmessage, "")) {
        $command = $watch_prefix . escapeshellarg($xmessage)
            . " -center -title " . escapeshellarg($app_id)
            . " " . escapeshellarg($message) . $watch_suffix;
        $pid = shell_exec("sh -c " . escapeshellarg($command));
        return $pid === null || $pid === false ? "" : trim($pid);
    }

    return "";
}

function stop_gui_status($pid) {
    $pid = trim($pid);
    if (string_equals($pid, "")) {
        return;
    }
    run_shell("kill " . escapeshellarg($pid), "", true);
}

function config_value($config, $key) {
    $lines = file($config, 2);
    if ($lines === false) {
        return "";
    }
    foreach ($lines as $raw_line) {
        $line = trim($raw_line);
        if (string_equals($line, "") || string_equals(substr($line, 0, 1), "#")) {
            continue;
        }
        $parts = explode("\t", $line);
        if (count($parts) >= 2 && string_equals($parts[0], $key)) {
            return $parts[1];
        }
    }
    return "";
}

function config_values_text($config, $key) {
    $values = "";
    $lines = file($config, 2);
    if ($lines === false) {
        return "";
    }
    foreach ($lines as $raw_line) {
        $line = trim($raw_line);
        if (string_equals($line, "") || string_equals(substr($line, 0, 1), "#")) {
            continue;
        }
        $parts = explode("\t", $line);
        if (count($parts) >= 2 && string_equals($parts[0], $key)) {
            if (!string_equals($values, "")) {
                $values = $values . "\n";
            }
            $values = $values . $parts[1];
        }
    }
    return $values;
}

function ensure_wine_prefix($app_id, $user_prefix) {
    if (is_dir($user_prefix)) {
        return false;
    }
    file_put_contents("/dev/stderr", "pw-wine-prefix-delta: " . "creating Wine prefix: " . $user_prefix . "\n", 8);
    ensure_dir($app_id, dirname($user_prefix));
    $code = run_shell(shell_env_assignment("WINEPREFIX", $user_prefix) . "wineboot -u", "", false);
    if ($code !== 0) {
        fail($app_id, "wineboot failed for prefix: " . $user_prefix);
    }
    $code = run_shell(shell_env_assignment("WINEPREFIX", $user_prefix) . "wineserver -w", "", false);
    if ($code !== 0) {
        fail($app_id, "wineserver wait failed for prefix: " . $user_prefix);
    }
    return true;
}

function copy_file_preserving_parent($app_id, $source, $target) {
    ensure_dir($app_id, dirname($target));
    if (!copy($source, $target)) {
        fail($app_id, "cannot copy " . $source . " to " . $target);
    }
    $mtime = filemtime($source);
    if ($mtime !== false) {
        touch($target, $mtime);
    }
}

function file_mtime_size_matches($source, $target) {
    if (!is_file($source) || !is_file($target)) {
        return false;
    }
    $source_size = filesize($source);
    $target_size = filesize($target);
    $source_mtime = filemtime($source);
    $target_mtime = filemtime($target);
    if ($source_size === false || $target_size === false || $source_mtime === false || $target_mtime === false) {
        return false;
    }
    return $source_size === $target_size && $source_mtime === $target_mtime;
}

function string_equals($left, $right) {
    if (strlen($left) !== strlen($right)) {
        return false;
    }
    return strpos($left, $right) === 0;
}

function env_flag($name) {
    $value = getenv($name);
    if ($value === false) {
        return false;
    }
    return string_equals($value, "1");
}

function file_state_token($app_id, $path, $prefix) {
    if (!is_file($path)) {
        return "";
    }
    $size = filesize($path);
    $mtime = filemtime($path);
    if ($size === false || $mtime === false) {
        fail($app_id, "cannot stat file: " . $path);
    }
    return $prefix . ":" . $size . ":" . $mtime;
}

function state_manifest_has_token($manifest, $token) {
    if (!is_file($manifest)) {
        return false;
    }
    $lines = file($manifest, 2);
    if ($lines === false || count($lines) < 1) {
        return false;
    }
    $first = trim($lines[0]);
    return string_equals($first, "# " . $token);
}

function registry_section_name($line) {
    $trimmed = trim($line);
    if (string_equals($trimmed, "") || !string_equals(substr($trimmed, 0, 1), "[")) {
        return "";
    }
    $end = strpos($trimmed, "]");
    if ($end === false || $end < 2) {
        return "";
    }
    return substr($trimmed, 1, $end - 1);
}

function registry_value_name($line) {
    $trimmed = trim($line);
    if (string_equals($trimmed, "")) {
        return "";
    }
    if (string_equals(substr($trimmed, 0, 2), "@=")) {
        return "@";
    }
    if (!string_equals(substr($trimmed, 0, 1), "\"")) {
        return "";
    }
    $i = 1;
    $escaped = false;
    while ($i < strlen($trimmed)) {
        $ch = substr($trimmed, $i, 1);
        if ($escaped) {
            $escaped = false;
        } elseif (string_equals($ch, "\\")) {
            $escaped = true;
        } elseif (string_equals($ch, "\"")) {
            if (string_equals(substr($trimmed, $i + 1, 1), "=")) {
                return substr($trimmed, 0, $i + 1);
            }
            return "";
        }
        $i++;
    }
    return "";
}

function registry_section_has_value($content, $section, $value_name) {
    $header = "[" . $section . "]";
    $pos = strpos($content, $header);
    while ($pos !== false) {
        $start = $pos + strlen($header);
        $next = strpos($content, "\n[", $start);
        if ($next === false) {
            $chunk = substr($content, $start);
        } else {
            $chunk = substr($content, $start, $next - $start);
        }
        if (strpos("\n" . $chunk, "\n" . $value_name . "=") !== false) {
            return true;
        }
        $pos = strpos($content, $header, $start);
    }
    return false;
}

function apply_missing_registry_entries($app_id, $target, $patch_file) {
    if (!is_file($target)) {
        fail($app_id, "registry target is missing: " . $target);
    }
    if (!is_file($patch_file)) {
        fail($app_id, "registry patch is missing: " . $patch_file);
    }
    $content = file_get_contents($target);
    if ($content === false) {
        fail($app_id, "cannot read registry file: " . $target);
    }
    $lines = file($patch_file, 2);
    if ($lines === false) {
        fail($app_id, "cannot read registry patch: " . $patch_file);
    }

    $append = "";
    $current_section = "";
    $current_section_header = "";
    $append_section = "";
    foreach ($lines as $raw_line) {
        if (string_equals(substr($raw_line, 0, 3), "+++")) {
            continue;
        }
        if (!string_equals(substr($raw_line, 0, 1), "+")) {
            continue;
        }
        $entry = rtrim(substr($raw_line, 1), "\r\n");
        if (string_equals($entry, "") || string_equals(substr(trim($entry), 0, 6), "#time=")) {
            continue;
        }
        $section = registry_section_name($entry);
        if (!string_equals($section, "")) {
            $current_section = $section;
            $current_section_header = $entry;
            continue;
        }
        if (string_equals($current_section, "")) {
            continue;
        }
        $value_name = registry_value_name($entry);
        if (string_equals($value_name, "")) {
            continue;
        }
        $combined = $content . $append;
        if (registry_section_has_value($combined, $current_section, $value_name)) {
            continue;
        }
        if (!string_equals($append_section, $current_section)) {
            $append = $append . "\n" . $current_section_header . "\n";
            $append_section = $current_section;
        }
        $append = $append . $entry . "\n";
    }

    if (string_equals($append, "")) {
        return false;
    }
    if (file_put_contents($target, rtrim($content, "\r\n") . "\n" . $append) === false) {
        fail($app_id, "cannot write registry file: " . $target);
    }
    return true;
}

function apply_registry_patches($app_id, $opt_root, $user_prefix, $state_manifest, $prefix_created) {
    $manifest = path_join($opt_root, "registry-patches.tsv");
    if (!is_file($manifest)) {
        return;
    }
    $lines = file($manifest, 2);
    if ($lines === false) {
        if ($prefix_created) {
            fail($app_id, "cannot read registry patch manifest: " . $manifest);
        }
        return;
    }
    $patch_root = path_join($opt_root, "registry-patches");
    $next = $state_manifest . ".next";
    file_put_contents($next, "");
    $registry_changed = 0;
    $registry_unchanged = 0;
    $registry_current = 0;
    file_put_contents("/dev/stderr", "pw-wine-prefix-delta: " . "checking registry merge patches" . "\n", 8);
    foreach ($lines as $raw_line) {
        $line = trim($raw_line);
        if (string_equals($line, "") || string_equals(substr($line, 0, 1), "#")) {
            continue;
        }
        $parts = explode("\t", $line);
        if (count($parts) < 5) {
            continue;
        }
        $relpath = $parts[0];
        $patch_digest = $parts[3];
        $patch_file = path_join($patch_root, $parts[4]);
        $target = path_join($user_prefix, $relpath);
        $token_prefix = "registry-merge-v2:" . $patch_digest;
        $state_token = file_state_token($app_id, $target, $token_prefix);
        $previous = manifest_digest($state_manifest, $relpath);

        if (!string_equals($state_token, "") && string_equals($previous, $state_token)) {
            $registry_current++;
            append_manifest_entry($app_id, $next, $state_token, $relpath);
            continue;
        }

        if (apply_missing_registry_entries($app_id, $target, $patch_file)) {
            $registry_changed++;
        } else {
            $registry_unchanged++;
        }
        $state_token = file_state_token($app_id, $target, $token_prefix);
        append_manifest_entry($app_id, $next, $state_token, $relpath);
    }
    if (!rename($next, $state_manifest)) {
        fail($app_id, "cannot update registry state manifest: " . $state_manifest);
    }
    file_put_contents("/dev/stderr", "pw-wine-prefix-delta: " . "registry merge done: changed=" . $registry_changed . " unchanged=" . $registry_unchanged . " current=" . $registry_current . "\n", 8);
}


function sync_user_empty_dirs($app_id, $dir_manifest, $user_prefix) {
    if (!is_file($dir_manifest)) {
        return;
    }
    $empty_dir_created = 0;
    $lines = file($dir_manifest, 2);
    if ($lines === false) {
        fail($app_id, "cannot read empty directory manifest: " . $dir_manifest);
    }
    foreach ($lines as $raw_line) {
        $relpath = trim($raw_line);
        if (string_equals($relpath, "") || string_equals(substr($relpath, 0, 1), "#")) {
            continue;
        }
        $target_dir = path_join($user_prefix, $relpath);
        if (!is_dir($target_dir)) {
            $empty_dir_created++;
        }
        ensure_dir($app_id, $target_dir);
    }
    if ($empty_dir_created > 0) {
        file_put_contents("/dev/stderr", "pw-wine-prefix-delta: " . "created empty directories: " . $empty_dir_created . "\n", 8);
    }
}

function sync_user_prefix($app_id, $opt_root, $user_prefix, $state_manifest) {
    $manifest = path_join($opt_root, "manifest.tsv");
    if (!is_file($manifest)) {
        fail($app_id, "missing package manifest: " . $manifest);
    }
    ensure_wine_prefix($app_id, $user_prefix);
    ensure_dir($app_id, dirname($state_manifest));
    sync_user_empty_dirs($app_id, path_join($opt_root, "empty-dirs.tsv"), $user_prefix);

    $sync_token = file_state_token($app_id, $manifest, "prefix-sync-v2");
    if (!env_flag("PW_WINE_PREFIX_DELTA_FORCE_SYNC") && state_manifest_has_token($state_manifest, $sync_token)) {
        file_put_contents("/dev/stderr", "pw-wine-prefix-delta: " . "managed prefix files already current" . "\n", 8);
        return;
    }

    file_put_contents("/dev/stderr", "pw-wine-prefix-delta: " . "syncing managed prefix files" . "\n", 8);
    $sync_status_pid = start_gui_status($app_id, "Syncing managed Wine prefix files...");

    $lines = file($manifest, 2);
    if ($lines === false) {
        fail($app_id, "cannot read package manifest: " . $manifest);
    }

    $delta_root = path_join($opt_root, "prefix-delta");
    $next = $state_manifest . ".next";
    $next_content = "# " . $sync_token . "\n";
    $files_copied = 0;
    $files_current = 0;
    $files_user_modified = 0;
    foreach ($lines as $raw_line) {
        $line = trim($raw_line);
        if (string_equals($line, "") || string_equals(substr($line, 0, 1), "#")) {
            continue;
        }
        $parts = explode("\t", $line);
        if (count($parts) < 2) {
            continue;
        }
        $new_digest = $parts[0];
        $relpath = $parts[1];
        $source = path_join($delta_root, $relpath);
        $target = path_join($user_prefix, $relpath);
        $copy_needed = false;

        if (!file_exists($target)) {
            $copy_needed = true;
        } elseif (is_file($target) && file_mtime_size_matches($source, $target)) {
            $files_current++;
            $next_content = $next_content . $new_digest . "\t" . $relpath . "\n";
            continue;
        }

        $old_digest = manifest_digest($state_manifest, $relpath);
        if (!$copy_needed && is_file($target) && string_equals($old_digest, "")) {
            $copy_needed = true;
        } elseif (!$copy_needed && is_file($target) && !string_equals($old_digest, "") && string_equals(sha256_file_checked($app_id, $target), $old_digest)) {
            $copy_needed = true;
        }

        if ($copy_needed) {
            $files_copied++;
            copy_file_preserving_parent($app_id, $source, $target);
            $next_content = $next_content . $new_digest . "\t" . $relpath . "\n";
        } elseif (is_file($target) && string_equals(sha256_file_checked($app_id, $target), $new_digest)) {
            $files_current++;
            touch($target, filemtime($source));
            $next_content = $next_content . $new_digest . "\t" . $relpath . "\n";
        } elseif (!string_equals($old_digest, "")) {
            $files_user_modified++;
            $next_content = $next_content . $old_digest . "\t" . $relpath . "\n";
        } else {
            $next_content = $next_content . $new_digest . "\t" . $relpath . "\n";
        }
    }
    if (file_put_contents($next, $next_content) === false) {
        fail($app_id, "cannot write state manifest: " . $next);
    }
    if (!rename($next, $state_manifest)) {
        fail($app_id, "cannot update state manifest: " . $state_manifest);
    }
    stop_gui_status($sync_status_pid);
    file_put_contents("/dev/stderr", "pw-wine-prefix-delta: " . "managed prefix sync done: copied=" . $files_copied . " current=" . $files_current . " user_modified=" . $files_user_modified . "\n", 8);
}

function line_matches_patterns($line, $patterns_text) {
    $lower = strtolower($line);
    $patterns = explode("\n", $patterns_text);
    foreach ($patterns as $raw_pattern) {
        $pattern = trim($raw_pattern);
        if (string_equals($pattern, "")) {
            continue;
        }
        if (strpos($lower, strtolower($pattern)) !== false) {
            return true;
        }
    }
    return false;
}

function unhide_matching_windows($patterns_text) {
    if (string_equals($patterns_text, "")) {
        return;
    }
    $output = shell_exec("wmctrl -lx 2>/dev/null");
    if ($output === null || $output === false) {
        return;
    }
    $lines = explode("\n", $output);
    foreach ($lines as $line) {
        if (!line_matches_patterns($line, $patterns_text)) {
            continue;
        }
        $parts = explode(" ", trim($line));
        if (count($parts) < 1 || string_equals($parts[0], "")) {
            continue;
        }
        run_shell("wmctrl -i -r " . escapeshellarg($parts[0]) . " -b remove,skip_taskbar,skip_pager", "", true);
    }
}

function run_wine_program($app_id, $user_prefix, $exe, $argv, $patterns_text) {
    $command = shell_env_assignment("WINEPREFIX", $user_prefix) . "wine " . escapeshellarg($exe);
    $i = 1;
    while ($i < count($argv)) {
        $command = $command . " " . escapeshellarg($argv[$i]);
        $i++;
    }
    unhide_matching_windows($patterns_text);
    if (env_flag("PW_WINE_PREFIX_DELTA_DETACH")) {
        file_put_contents("/dev/stderr", "pw-wine-prefix-delta: " . "starting detached: " . $exe . "\n", 8);
        return run_shell_background($command, dirname($exe));
    }
    file_put_contents("/dev/stderr", "pw-wine-prefix-delta: " . "starting foreground: " . $exe . "\n", 8);
    return run_shell($command, dirname($exe), false);
}

function app_main($argv) {
    $app_id = app_id_from_argv($argv);
    $home = default_home($app_id);
    $opt_root = path_join("/usr/share/pw-wine-delta", $app_id);
    $config = path_join($opt_root, "startwrapper.conf");
    if (!is_file($config)) {
        fail($app_id, "missing startwrapper config: " . $config);
    }
    $program_exe = config_value($config, "program_exe");
    if (string_equals($program_exe, "")) {
        fail($app_id, "missing program_exe in " . $config);
    }
    $patterns_text = config_values_text($config, "window_pattern");

    $user_prefix = path_join(path_join($home, ".wine-pwdelta"), $app_id);
    $state_dir = path_join($user_prefix, ".pw-delta-state");
    $state_manifest = path_join($state_dir, "managed.tsv");
    $registry_state_manifest = path_join($state_dir, "registry-patches.tsv");

    file_put_contents("/dev/stderr", "pw-wine-prefix-delta: " . "using prefix: " . $user_prefix . "\n", 8);
    file_put_contents("/dev/stderr", "pw-wine-prefix-delta: " . "checking Wine prefix" . "\n", 8);
    $prefix_created = ensure_wine_prefix($app_id, $user_prefix);
    ensure_dir($app_id, $state_dir);
    file_put_contents("/dev/stderr", "pw-wine-prefix-delta: " . "checking registry state" . "\n", 8);
    apply_registry_patches($app_id, $opt_root, $user_prefix, $registry_state_manifest, $prefix_created);
    file_put_contents("/dev/stderr", "pw-wine-prefix-delta: " . "checking managed files" . "\n", 8);
    sync_user_prefix($app_id, $opt_root, $user_prefix, $state_manifest);

    if (env_flag("PW_WINE_PREFIX_DELTA_SYNC_ONLY")) {
        file_put_contents("/dev/stderr", "pw-wine-prefix-delta: " . "sync-only complete" . "\n", 8);
        return 0;
    }

    $exe = path_join($user_prefix, $program_exe);
    if (!is_file($exe)) {
        fail($app_id, "installed program not found: " . $exe);
    }
    return run_wine_program($app_id, $user_prefix, $exe, $argv, $patterns_text);
}

exit(app_main($argv));
