<?php
namespace taskman;
use Exception;

function is_release() : bool
{
  return get("GAME_IS_DEV") == 0;
}

function is_dev() : bool
{
  return !is_release();
}

function is_dev_no_env() : bool
{
  return is_dev() && !getenv("GAME_ENV");
}

function is_win() : bool
{
  return !(DIRECTORY_SEPARATOR == '/');
}

function is_linux() : bool
{
  return PHP_OS == 'Linux';
}

function stderr(string $msg)
{
  fwrite(STDERR, $msg);
}

function fatal(string $msg)
{
  fwrite(STDERR, $msg);
  exit(1);
}

function make_tmp_file_name(string $file_name) : string
{
  $meta = stream_get_meta_data(tmpfile());
  $tmp_dir = dirname($meta['uri']);
  $tmp_file = "$tmp_dir/$file_name";
  return $tmp_file;
}

function get_tmp_build_path(string $file) : string
{
  global $GAME_ROOT;

  $name = str_replace(":", "-", str_replace("\\", "-", str_replace("/", "-", normalize_path($file))));
  $name = ltrim($name, "-");
  return normalize_path("$GAME_ROOT/build/tmp/$name");
}

function find_recursive(string $dir, string $ext) : array
{
  if(is_win())
    $cmd = "dir /s /b " . normalize_path("$dir/*$ext");
  else
    $cmd = "find " . normalize_path($dir, true) . " -name '*$ext' -print";
  exec($cmd, $out);
  return $out;
}

function find_files(string $dir, array $fnmatch_patterns = []) : array
{
  $results = array();
  $files = scandir($dir);

  foreach($files as $file) 
  {
    $path = realpath($dir . '/' . $file);
    if(!is_dir($path)) 
    {
      if(!$fnmatch_patterns || fnmatch_patterns($file, $fnmatch_patterns))
        $results[] = $path;
    } 
    else if($file != "." && $file != "..") 
    {
      $results = array_merge($results, find_files($path, $fnmatch_patterns));
    }
  }

  return $results;
}

function scan_files_rec(array $dirs, array $only_extensions = [], int $mode = 1) : array
{
  $files = array();
  foreach($dirs as $dir)
  {
    if(!is_dir($dir))
      continue;

    $dir = normalize_path($dir);

    $iter_mode = $mode == 1 ? \RecursiveIteratorIterator::LEAVES_ONLY : \RecursiveIteratorIterator::SELF_FIRST; 
    $iter = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir), $iter_mode);

    foreach($iter as $filename => $cur) 
    {
      if(($mode == 1 && !$cur->isDir()) || 
         ($mode == 2 && $cur->isDir()))
      {
        if(!$only_extensions)
          $files[] = $filename;
        else
        {
          $flen = strlen($filename); 
          foreach($only_extensions as $ext)
          {
            if(substr_compare($filename, $ext, $flen-strlen($ext)) === 0)
              $files[] = $filename;
          }
        }
      }
    }
  }
  return $files;
}

function normalize_path(string $path, ?bool $unix = null/*null means try to guess*/) : string 
{
  if(is_null($unix)) 
    $unix = !is_win();

  $path = str_replace('\\', '/', $path);
  $path = preg_replace('/\/+/', '/', $path);
  $parts = explode('/', $path);
  $absolutes = array();
  foreach($parts as $part) 
  {
    if('.' == $part) 
      continue;

    if('..' == $part) 
      array_pop($absolutes);
    else
      $absolutes[] = $part;
  }
  $res = implode($unix ? '/' : '\\', $absolutes);
  return $res;
}

function normalize_sed_path(string $path) : string
{
  $path = normalize_path($path);
  if(is_win())
    $path = str_replace('\\', '\\\\', $path);
  return $path;
}

//NOTE: Escaping multibyte unicode chars as \uXXXX impacts decoding performance drastically
//so we unescape them while encoding json
function json_encode_unescaped(array $arr) : string
{
  array_walk_recursive($arr, function (&$item, $key) { if (is_string($item)) $item = mb_encode_numericentity($item, array (0x80, 0xffff, 0, 0xffff), 'UTF-8'); });
  return mb_decode_numericentity(json_encode($arr), array (0x80, 0xffff, 0, 0xffff), 'UTF-8');
}

function json_make_pretty(string $json) : string
{
  return prettyJSON($json);
}

function need_to_regen(string $file, iterable $deps, bool $debug = false) : bool
{
  if(!is_file($file))
  {
    if($debug)
      echo "! $file\n";
    return true;
  }

  $fmtime = filemtime($file); 

  foreach($deps as $dep)
  {
    if($dep && is_file($dep) && (filemtime($dep) > $fmtime))
    {
      if($debug)
        echo "$dep > $file\n";
      return true;
    }
  }

  return false;
}

function need_to_regen_any(array $files, array $deps, bool $debug = false) : bool
{
  $earliest_file = null;
  $earliest_time = 2e32;

  foreach($files as $file)
  {
    if(!is_file($file))
      return true;

    $time = filemtime($file);
    if($time < $earliest_time)
    {
      $earliest_file = $file;
      $earliest_time = $time;
    }
  }

  if($debug)
  {
    $date = date(DATE_RFC2822, $earliest_time);
    echo "need_to_regen_any, earliest file: $earliest_file ($date)\n";
  }

  if($earliest_file === null)
    return true;

  return need_to_regen($earliest_file, $deps, $debug);
}

function fnmatch_patterns(string $file, array $fnmatch_patterns) : bool
{
  foreach($fnmatch_patterns as $pattern)
  {
    if(fnmatch($pattern, $file))
    {
      return true;
    }
  }

  return false;
}

//force mode: 
//0 - none, 
//1 - force write always, 
//2 - write only if content differs
//3 - write only if content differs, don't even touch if they are same
function gen_file(string $tpl_file, string $result_file, $deps = array(), $force = 0, int $perms = 0640)
{
  $deps[] = $tpl_file;
  if($force > 0 || need_to_regen($result_file, $deps))
  {
    $txt = file_get_contents($tpl_file);
    if($txt === false)
      throw new Exception("Bad template settings file $tpl_file");

    //replacing %(FOO)% alike entries with taskman config values
    $out = _($txt);

    if(($force == 2 || $force == 3) && is_file($result_file))
    {
      $prev = file_get_contents($result_file);
      if($prev === false)
        throw new Exception("Could not read file $result_file");

      //if contents is similar no need to write it
      if(strcmp($prev, $out) === 0)
      {
        if($force == 2)
          touch($result_file);
        return;
      }
    }

    ensure_write($result_file, $out);
    chmod($result_file, $perms);
  }
}

function ensure_read($file)
{
  $c = file_get_contents($file);
  if($c === false)
    throw new Exception("Could not read file '$file'");
  return $c;
}

function ensure_write($dst, $txt, $dir_perms = 0777, $flags = 0)
{
  $dir = dirname($dst);

  if(!is_dir($dir))
    mkdir($dir, $dir_perms, true);

  msg_dbg("> $dst ...\n");
  if(file_put_contents($dst, $txt, $flags) === false)
    throw new Exception("Could not write to '$dst'");
}

function ensure_write_if_differs($dst, $txt, $dir_perms = 0777, $flags = 0)
{
  if(is_file($dst) && file_get_contents($dst) == $txt)
    return;

  ensure_write($dst, $txt, $dir_perms, $flags);
}

const COPY_MODE_BUILTIN  = 1;
const COPY_MODE_SYSTEM   = 2;
const COPY_MODE_HARDLINK = 3;

function ensure_copy(string $src, string $dst, int $dir_perms = 0777, array $excludes = array(), array $fnmatches = array())
{
  recurse_copy($src, $dst, $dir_perms, COPY_MODE_BUILTIN, false, $excludes, $fnmatches);
}

function ensure_copy_file_if_differs(string $src_file, string $dst_file, $dir_perms = 0777)
{
  if(!is_file($dst_file) || filesize($src_file) != filesize($dst_file) || crc32_file($src_file) !== crc32_file($dst_file))
    ensure_copy($src_file, $dst_file, $dir_perms);
}

function ensure_sync(string $src, string $dst, int $dir_perms = 0777, array $excludes = array(), array $fnmatches = array())
{
  recurse_copy($src, $dst, $dir_perms, COPY_MODE_BUILTIN, true, $excludes, $fnmatches);
}

function ensure_hardlink(string $src, string $dst, int $dir_perms = 0777, array $excludes = array(), array $fnmatches = array())
{
  recurse_copy($src, $dst, $dir_perms, COPY_MODE_HARDLINK, true, $excludes, $fnmatches);
}

function ensure_duplicate(string $src, string $dst, int $dir_perms = 0777)
{
  recurse_copy($src, $dst, $dir_perms, COPY_MODE_SYSTEM);
}

function recurse_copy(
  string $src, 
  string $dst, 
  int $dir_perms = 0777, 
  int $copy_mode = COPY_MODE_BUILTIN, 
  bool $mtime_check = false, 
  //regex expressions which check full paths
  array $excludes = array(),
  //fnmatch expressions which check file names only
  array $fnmatches = array()
) 
{
  msg_dbg("copying $src => $dst ...\n");

  if(!is_file($src) && !is_dir($src))
    throw new Exception("Bad file or dir '$src'");

  foreach($excludes as $exclude_pattern)
  {
    if(preg_match("~$exclude_pattern~", $src))
      return;
  }

  if(!is_dir($src))
  {
    _ensure_copy_file($src, $dst, $copy_mode, $mtime_check);
    return;
  }

  $dir = opendir($src);
  ensure_mkdir($dst, $dir_perms);
  while(false !== ($file = readdir($dir))) 
  {
    if(($file != '.' ) && ($file != '..')) 
    {
      if(is_dir($src . '/' . $file))
      {
        recurse_copy(
          $src . '/' . $file, 
          $dst . '/' . $file, 
          $dir_perms, 
          $copy_mode, 
          $mtime_check, 
          $excludes, 
          $fnmatches
        );
      }
      else
      {
        $excluded = false;
        foreach($excludes as $exclude_pattern)
          $excluded = $excluded || (bool)preg_match("~$exclude_pattern~", $src . '/' . $file);
        $fnmatched = sizeof($fnmatches) == 0;
        foreach($fnmatches as $fnmatch)
          $fnmatched = $fnmatched || fnmatch($fnmatch, $file);

        if($excluded || !$fnmatched)
          continue;

        _ensure_copy_file($src . '/' . $file, $dst . '/' . $file, $copy_mode, $mtime_check);
      }
    }
  }
  closedir($dir);
} 

function shell(string $cmd, &$out = null)
{
  shell_try($cmd, $ret, $out);
  if($ret != 0)
    throw new Exception("Shell execution error(exit code $ret)");
}

function shell_try(string $cmd, &$ret = null, &$out = null)
{
  msg(" shell: $cmd\n");
  msg(" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n");
  
  if(func_num_args() < 3)
    system($cmd, $ret);
  else
    _execute_proc_cmd($cmd, $ret, $out);
}

function shell_get(string $cmd, bool $as_string = true)
{
  exec($cmd, $out, $code);
  if($code !== 0)
    throw new Exception("Error($code) executing shell cmd '$cmd'");
  return $as_string ? implode("", $out) : $out;
}

function _execute_proc_cmd(string $cmd, &$ret, &$out)
{
  //TODO: do we really need to redirect error stream?
  $proc = popen("$cmd 2>&1", 'r');

  $log = '';
  if(!is_resource($proc))
  {
    $log = "";
    _log($log, 1);
  }
  else
  {
    while($logline = fgets($proc))
    {
      $log .= $logline;
      _log($logline, 1);
    }
  }
  $out = explode("\n", $log);
  $ret = pclose($proc);
}

function _ensure_copy_file(string $src, string $dst, int $copy_mode = COPY_MODE_BUILTIN, bool $mtime_check = false)
{
  if($mtime_check && file_exists($dst) && filemtime($src) <= filemtime($dst))
    return;

  msg_dbg("copy ($copy_mode): $src => $dst\n");
  ensure_mkdir(dirname($dst));

  if($copy_mode == COPY_MODE_SYSTEM)
    shell("cp -a $src $dst");
  else if($copy_mode == COPY_MODE_BUILTIN)
  { 
    if(!copy($src, $dst))
      throw new Exception("Could not copy '$src' to '$dst'");
  }

  else if($copy_mode == COPY_MODE_HARDLINK)
  { 
    if(!link($src, $dst))
      throw new Exception("Could make a hard link '$src' to '$dst'");
  }
  else 
    throw new Exception("Unrecognized copy mode $copy_mode");
}

function ensure_identical(string $src, string $dst, $excludes = array()) 
{
  msg_dbg("deleting files missing in $src from $dst ...\n");

  if(!is_dir($dst) && !is_file($dst))
    throw new Exception("dst '$dst' must be a valid file or dir!");

  foreach($excludes as $exclude_pattern)
  {
    if(preg_match("~$exclude_pattern~", $src))
      return;
  }

  $dir = opendir($dst);
  while(false !== ($file = readdir($dir))) 
  {
    if(($file != '.' ) && ($file != '..')) 
    {
      $src_file = $src . '/' . $file; 
      $dst_file = $dst . '/' . $file; 

      if(is_dir($src_file))
        ensure_identical($src_file, $dst_file, $excludes);
      else
      {
        $excluded = false;
        foreach($excludes as $exclude_pattern)
        {
          $excluded = $excluded || (bool)preg_match("~$exclude_pattern~", $src_file);
        }

        if($excluded)
          continue;

        if(is_file($dst_file) && !is_file($src_file))
          ensure_rm($dst_file);
      }
    }
  }
  closedir($dir);
} 

function ensure_symlink(string $src, string $dst, int $dir_perms = 0777)
{
  if(!is_file($src) && !is_dir($src))
    throw new Exception("Bad file or dir '$src'");

  $dir = dirname($dst);
  if(!is_dir($dir))
    mkdir($dir, $dir_perms, true);

  ensure_rm($dst);

  msg_dbg("symlinking $src -> $dst \n");
  if(!symlink($src, $dst))
    throw new Exception("Could not create symlink");
}

function ensure_rm(string $what)
{
  if(is_dir($what) && !is_link($what))
    rrmdir($what);
  else if(is_file($what) || is_link($what))
    unlink($what);
}

function ensure_mkdir(string $dir, int $perms = 0775)
{
  if(is_dir($dir))
    return;
  
  msg_dbg("mkdir $dir\n");
  if(!mkdir($dir, $perms, true))
    throw new Exception("Could not create dir '$dir'");

  msg_dbg("chmod " . decoct($perms) . " $dir\n");
  if(!chmod($dir, $perms))
    throw new Exception("Could not chmod " . decoct($perms) . " dir '$dir'");
}

function ensure_var_dir(string $dir)
{
  ensure_mkdir($dir, 0777);
  $items = fmatch("$dir/*");
  foreach($items as $item)
  {
    $perms = is_dir($item) ? 0770 : 0660;

    msg_dbg("chmod " . decoct($perms) . " $item\n");
    if(!chmod($item, $perms))
      throw new Exception("Could not chmod " . decoct($perms) . " '$item'");
  }
}

function compare_files_timestamp(string $file_a, string $file_b) : int
{
  $file_a_timestamp = file_exists($file_a) ? filemtime($file_a) : 0;
  $file_b_timestamp = file_exists($file_b) ? filemtime($file_b) : 0;
  if($file_a_timestamp >= $file_b_timestamp)
    return 1;
  else
    return -1;
}

function fmatch(string $path) : array
{
  $res = glob($path);
  if(!is_array($res))
    return array();
  return $res;
}

function rrmdir(string $dir, bool $remove_top_dir = true) 
{
  if(is_dir($dir))
  {
    $objects = scandir($dir);
    foreach($objects as $object) 
    {
      if($object != "." && $object != "..") 
      {
        if(is_dir($dir."/".$object)) 
          rrmdir($dir."/".$object); 
        else 
          unlink($dir."/".$object);
      }
    }
  }

  if($remove_top_dir)
  {
    if(is_link($dir))
      unlink($dir);
    else if(is_dir($dir))
      rmdir($dir);
  }
} 

define("GIT_INFO_REV_HASH"  , 1 << 0);
define("GIT_INFO_BRANCH"    , 1 << 1);
define("GIT_INFO_REV_NUMBER", 1 << 2);
define("GIT_INFO_ALL"       , ~0);

function git_get_info($info = GIT_INFO_ALL) : array
{
  global $GAME_ROOT;
  $rev_hash = "";
  $branch = "";
  $revision_number = 0;

  if(!is_dir("$GAME_ROOT/.git"))
    throw new Exception("Not a Git repository");

  if($info & GIT_INFO_REV_HASH)
  {
    $out = array();
    exec("git rev-parse HEAD", $out);
    $rev_hash = trim($out[0]);
    if(!$rev_hash)
      throw new Exception("Error getting git revision hash");
  }

  if($info & GIT_INFO_BRANCH)
  {
    $out = array();
    exec("git rev-parse --abbrev-ref HEAD", $out);
    $branch = trim($out[0]);
    if(!$branch)
      throw new Exception("Error getting git branch");
  }

  if($info & GIT_INFO_REV_NUMBER)
  {
    $out = array();
    exec("git rev-list HEAD --count", $out);
    $revision_number = (int)$out[0];
    if(!$revision_number)
      throw new Exception("Error getting git revision number");
  }

  return array($rev_hash, $branch, $revision_number);
}

function git_get_rev_hash() : string
{
  list($rev_hash, $_, $__) = git_get_info(GIT_INFO_REV_HASH);
  return $rev_hash;
}

function git_get_branch() : string
{
  list($_, $branch, $__) = git_get_info(GIT_INFO_BRANCH);
  return $branch;
}

function git_get_rev_number() : string
{
  list($_, $__, $rev_number) = git_get_info(GIT_INFO_REV_NUMBER);
  return $rev_number;
}

function git_try_commit(string $paths, string $msg)
{
  try
  {
    exec("git add $paths && git commit -m \"$msg\" && git pull && git push");
  }
  catch(Exception $e)
  {
    echo "Failed to commit changes. Apparently, nothing changed\n";
  }
}

function check_and_decode_jzon(string $json) : array
{
  try
  {
    $arr = jzon_parse($json);
    return array(0, "", $arr);
  }
  catch(Exception $e)
  {
    return array(1, $e->getMessage(), array());
  }
}

function make_file_md5($file, $md5_file)
{
  if(!need_to_regen($md5_file, array($file)))
    return file_get_contents($md5_file);

  $md5 = md5_file($file); 
  ensure_write($md5_file, $md5); 

  return $md5;
}

function make_dir_md5($dir, $md5_file)
{
  $files = scan_files_rec(array($dir));

  if(!need_to_regen($md5_file, $files))
    return;

  $md5s = array();
  foreach($files as $file)
    $md5s[] = md5_file($file); 

  $md5 = md5(implode('', $md5s));
  ensure_write($md5_file, $md5);
}

function file_put_contents_atomic(string $filename, string $content, int $mode = 0644) 
{ 
  $temp = tempnam(dirname($filename), 'atomic'); 
  if(!($f = @fopen($temp, 'wb'))) 
    throw new Exception("Error writing temporary file '$temp'"); 

  fwrite($f, $content); 
  fclose($f); 

  if(!@rename($temp, $filename)) 
  { 
    @unlink($filename); 
    @rename($temp, $filename); 
  } 
  chmod($filename, $mode); 
} 

function gmgetdate($ts = null) : array
{ 
  $k = array('seconds','minutes','hours','mday', 
      'wday','mon','year','yday','weekday','month',0); 
  return array_combine($k, explode(":", 
          gmdate('s:i:G:j:w:n:Y:z:l:F:U',is_null($ts)?time():$ts))); 
} 

function prettyJSON(string $json) : string
{  
  $result = '';
  $level = 0;
  $prev_char = '';
  $in_quotes = false;
  $ends_line_level = NULL;
  $json_length = strlen($json);

  for($i = 0; $i < $json_length; $i++) 
  {
    $char = $json[$i];
    $new_line_level = NULL;
    $post = "";
    if($ends_line_level !== NULL) 
    {
      $new_line_level = $ends_line_level;
      $ends_line_level = NULL;
    }
    if($char === '"' && $prev_char != '\\') 
    {
      $in_quotes = !$in_quotes;
    } 
    else if(!$in_quotes) 
    {
      switch($char) 
      {
        case '}': case ']':
          $level--;
          $ends_line_level = NULL;
          $new_line_level = $level;
          break;
        case '{': case '[':
          $level++;
        case ',':
          $ends_line_level = $level;
          break;
        case ':':
          $post = " ";
          break;
        case " ": case "\t": case "\n": case "\r":
          $char = "";
          $ends_line_level = $new_line_level;
          $new_line_level = NULL;
          break;
      }
    }
    if($new_line_level !== NULL) 
    {
      $result .= "\n".str_repeat("  ", $new_line_level);
    }
    $result .= $char.$post;
    $prev_char = $char;
  }
    
  $result = str_replace('"<%', '<%', $result);
  $result = str_replace('%>"', '%>', $result);  
  $result = str_replace('\"', '"', $result);  
  $result = str_replace('\\\\', '\\', $result);      
  $result = str_replace('\/', '/', $result);
  $result = str_replace('"{', '{', $result);
  $result = str_replace('}"', '}', $result);

  return $result."\n";
}

function crc32_file($file)
{
  $hash = hash_file('crc32b', $file);
  $array = unpack('N', pack('H*', $hash));
  return $array[1];
}

function run_shell_in_project_dir($cmd)
{
  global $GAME_ROOT;

  shell("cd $GAME_ROOT && $cmd", $out);
  return $out[0];
}

function boolstr(bool $b) : string
{
  return $b ? 'true' : 'false';
}

function create_js_template_literal(string $str) : string
{
  $result = str_replace('\\', '\\\\', $str);  // replace  \ -> \\
  $result = str_replace('`', '\\`', $result); // replace  ` -> \`
  $result = str_replace('$', '\\$', $result); // replace  $ -> \$
  return "`$result`";
}

function create_go_string_literal(string $str) : string
{
  $result = str_replace('\\', '\\\\', $str);   // replace  \ -> \\
  $result = str_replace('"', '\\"', $result);  // replace  " -> \"
  $result = str_replace("\n", '\\n', $result); // replace  new_line -> \n
  return '"' . $result . '"';
}

function convertFileFromDos2UnixFormat(string $file)
{
  $file_tmp = $file.".tmp";

  if(is_win())
    replace_text_in_file("\\r", "", $file, $file_tmp);
  else
   shell("sed \$'s/\\r\$//' $file > $file_tmp");

  ensure_copy($file_tmp, $file);
  ensure_rm($file_tmp);
}

function removeCarriageReturn(string $file)
{
  $file_tmp = $file.".tmp";

  if(is_win())
    replace_text_in_file("\\\\r", "", $file, $file_tmp);
  else
    shell("sed 's/\\\\r//' $file > $file_tmp");

  ensure_copy($file_tmp, $file);
  ensure_rm($file_tmp);
}

function replace_text_in_file(string $search_pattern, string $replace_pattern, string $from_file, string $to_file)
{
  $file_content = ensure_read($from_file);
  $res = str_replace($search_pattern, $replace_pattern, $file_content);
  ensure_write($to_file, $res);
}

function gamectl_get_props_as_php_code() : string
{
  $props = props();
  $props_str = "<?php\nnamespace taskman;\n";
  foreach($props as $k => $v)
    $props_str .= "set('$k', " . var_export($v, true). ");\n";
  return $props_str;
}

function run_background_proc($bin, array $args = array(), $redirect_out = '', $redirect_err = '')
{
  if(is_win())
  {
    //TODO: migrate to background jobs
    $cmd = "powershell.exe Start-Process $bin -NoNewWindow";
    if($args)
    {
      $cmd .= ' -ArgumentList ';
      foreach($args as $arg)
        $cmd .= "'$arg',";
      $cmd = rtrim($cmd, ',');
    }
    if($redirect_out)
      $cmd .= " -RedirectStandardOutput '$redirect_out'";
    if($redirect_err)
      $cmd .= " -RedirectStandardError '$redirect_err'";
    pclose(popen($cmd, 'r'));
  }
  else
  {
    $cmd = $bin;
    foreach($args as $arg)
      $cmd .= ' ' . escapeshellarg($arg);
    if($redirect_out)
      $cmd .= ' > ' . escapeshellarg($redirect_out);
    if($redirect_err)
      $cmd .= ' 2> ' . escapeshellarg($redirect_err);
    $cmd .= ' &';
    exec($cmd, $_, $ret);
    if($ret !== 0)
      throw new Exception("Error starting worker: $cmd ($ret)");
  }
}

function run_background_gamectl_workers(string $task, array $worker_args) : array
{
  global $GAME_ROOT;

  $results = array();
  $workers = array();

  foreach($worker_args as $idx => $args)
  {
    $uid = uniqid();
    $in_file = $GAME_ROOT . "/build/tmp/in_$uid.work";
    $out_file = $GAME_ROOT  . "/build/tmp/out_$uid.work";
    $log_file = $GAME_ROOT  . "/build/tmp/log_$uid.work";
    $err_file = $GAME_ROOT  . "/build/tmp/err_$uid.work";

    $workers[] = array($in_file, $out_file, $log_file, $err_file);

    ensure_write($in_file, serialize($args));

    $cwd = getcwd(); 
    chdir($GAME_ROOT);
    run_background_proc(
      is_win() ? 'gamectl.bat' : './gamectl', 
      array('-b', $task, $in_file, $out_file, $err_file), 
      $log_file, $err_file
    );
    chdir($cwd);
  }

  try
  {
    $log_handles = array();
    while(sizeof($results) < sizeof($workers))
    {
      sleep(1);
      clearstatcache();

      foreach($workers as $idx => $worker)
      {
        if(isset($results[$idx]))
          continue;

        list($in_file, $out_file, $log_file, $err_file) = $worker;

        if(!isset($log_handles[$idx]) && is_file($log_file))
          $log_handles[$idx] = fopen($log_file, 'r');

        if(isset($log_handles[$idx]))
        {
          while(($buffer = fgets($log_handles[$idx])) !== false)
            echo $buffer;

          $pos = ftell($log_handles[$idx]);
          fclose($log_handles[$idx]);
          $log_handles[$idx] = fopen($log_file, "r");
          fseek($log_handles[$idx], $pos);
        }

        if(is_file($err_file) && filesize($err_file) > 0)
          throw new Exception("Error in worker $idx:\n" . file_get_contents($err_file));

        if(is_file($out_file))
          $results[$idx] = @unserialize(ensure_read($out_file));
      }
    }
  }
  finally
  {
    foreach($workers as $item)
    {
      list($in_file, $out_file, $log_file, $err_file) = $item;
      @ensure_rm($in_file);
      @ensure_rm($out_file);
      @ensure_rm($log_file);
      @ensure_rm($err_file);
    }
  }

  return $results;
}

function kb(string $data) : string
{
  return kb_len(strlen($data));
}

function kb_len(int $len) : string
{
  return round($len/1024,2) . "kb";
}

function gen_uuid_v4() : string
{
  $UUID_LENGTH_BYTES = 16;
  $data = random_bytes($UUID_LENGTH_BYTES);

  $data[6] = chr(ord($data[6]) & 0x0f | 0x40); // set version to 0100
  $data[8] = chr(ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10

  return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}

function watch_running_process($pid, string $log_file, $exit_matches = array(), $error_matches = array(), bool $verbose = true, $break_after_sec = -1, $ignored_errors = array(), $noted_warnings = array())
{
  $matches_any_fn = function($buffer, array $matches) 
  { 
    foreach($matches as $match)
    {
      $match_idx = strpos($buffer, $match);
      if($match_idx !== false)
        return $match_idx;
    }
    return false;
  };

  $h = fopen($log_file, "r");
  $start = time();
  $warnings_list = array();

  $throw_fn = function($msg) use (&$warnings_list) 
  {
    if(count($warnings_list) > 0) // @phpstan-ignore-line
      $msg .= "\n\nPossible causes:\n" . implode("\n", $warnings_list) . "\n";
    throw new Exception($msg);
  };

  $process_gone = false;

  while(true) 
  {
    $buffer = fgets($h);

    if($buffer !== false)
    {
      if($verbose)
        echo $buffer;

      //log success condition
      if($matches_any_fn($buffer, $exit_matches) !== false)
        break;

      if($matches_any_fn($buffer, $noted_warnings) !== false)
        $warnings_list[] = $buffer;

      $buffer_error_idx = $matches_any_fn($buffer, $error_matches);
      if($buffer_error_idx !== false)
      {
        if($matches_any_fn($buffer, $ignored_errors) !== false)
          echo "Error in log file IGNORED\n";
        else
          $throw_fn("Error condition in log file '$log_file' detected:\n****\n" . 
            trim(substr($buffer, $buffer_error_idx, 300))
            . "\n****\n"
          );
      }
    }
    else if($process_gone)
      $throw_fn("Process with id: '$pid' not found");
    else
    {
      usleep(200000);
      $pos = ftell($h);
      fclose($h);
      $h = fopen($log_file, "r");
      fseek($h, $pos);

      //check if process still exists
      if(check_process($pid) !== 0)
      {
        $process_gone = true;
        continue;
      }
    }

    if($break_after_sec > 0 && (time() - $start) > $break_after_sec)
      break;

  }
  fclose($h);
}

function check_process($pid) : int
{
  if(is_win())
  {
    $cmd = "powershell Get-Process -Id $pid";
  }
  else
  {
    $cmd = "ps -p $pid";
  }

  exec($cmd, $_, $ret);
  return $ret;
}

function which_dir(string $bin) : string
{
  return realpath(dirname(which_path($bin)));
}

function which_path(string $bin) : string
{
  if(is_win())
  {
    shell("where $bin", $out);
    return trim($out[0]);
  }
  else
  {
    shell("which $bin", $out);
    return trim($out[0]);
  }
}

function arg_exists(array $args, string $needle) : bool 
{
  $strict = true;
  return in_array($needle, $args, $strict);
}

if(!function_exists('str_ends_with'))
{
  function str_ends_with($haystack, $needle)
  {
    // search forward starting from end minus needle length characters
    return $needle === "" || (($temp = strlen($haystack) - strlen($needle)) >= 0 && strpos($haystack, $needle, $temp) !== false);
  }
}

function arg_opt(array &$args, $opt, $default, $conv_fn = null)
{
  foreach($args as $idx => $arg)
  {
    if(strpos($arg, $opt) === 0)
    {
      $arg = substr($arg, strlen($opt));
      unset($args[$idx]);
      return $conv_fn === null ? $arg : $conv_fn($arg);
    }
  }
  return $default;
}

function arg_opt_check_no_trailing(array $args)
{
  foreach($args as $arg)
  {
    if(preg_match('~(--\w+)=.*~', $arg, $m))
      throw new Exception("Unknown option '{$m[1]}'");
  }
}

function are_you_sure()
{
  if(!are_you_sure_ask())
  {
    echo "exiting then\n";
    exit();
  }
}

function are_you_sure_ask() : bool
{
  echo "Are you sure you want to proceed?(type YES): ";
  $resp = trim(fread(STDIN, 10));
  return $resp == "YES";
}

function names_hash_changed(string $crc_file, iterable $names) : bool
{
  $ctx = hash_init('crc32');
  foreach($names as $name)
    hash_update($ctx, $name);
  $names_crc = hash_final($ctx, false);
  $changed = !file_exists($crc_file) || ensure_read($crc_file) != $names_crc;
  ensure_write($crc_file, $names_crc);
  return $changed;
}

function is_apple_silicon() 
{
  $arch = php_uname('m');

  // Check if the machine type contains 'arm' (ARM architecture)
  if (strpos($arch, 'arm') !== false) {
    return true;
  }

  // If running on x86_64, check for Apple Silicon using sysctl 
  // (because we might get 'x86_64' when running under Rosetta)
  if ($arch === 'x86_64') {
    $sysctl = shell_exec('sysctl -n machdep.cpu.brand_string');
    return strpos($sysctl, 'Apple') !== false;
  }

  return false;
}