0) {
    $type = strtolower($type);
    if ($type === "hex") {
      $charset = $hex;
    } else if ($type === "base64") {
      $charset = $ascii . "/+";
    } else if ($type === "base32") {
      $charset = $uppercase . substr($digits, 2, 6);
    } else {
      $charset = $ascii;
    }
    $numCharacters = $type === "raw" ? 256 : strlen($charset);
    for ($i = 0; $i < $length; $i++) {
      try {
        $num = random_int(0, $numCharacters - 1);
      } catch (Exception $e) {
        $num = rand(0, $numCharacters - 1);
      }
      $randomString .= $type === "raw" ? chr($num) : $charset[$num];
    }
  }
  return $randomString;
}
function base64url_decode($data) {
  $base64 = strtr($data, '-_', '+/');
  return base64_decode($base64);
}
function startsWith($haystack, $needle, bool $ignoreCase = false): bool {
  $length = strlen($needle);
  if ($length === 0) {
    return true;
  }
  if ($ignoreCase) {
    $haystack = strtolower($haystack);
    $needle = strtolower($needle);
  }
  // PHP 8.0 support
  if (function_exists("str_starts_with")) {
    return str_starts_with($haystack, $needle);
  } else {
    return (substr($haystack, 0, $length) === $needle);
  }
}
function endsWith($haystack, $needle, bool $ignoreCase = false): bool {
  $length = strlen($needle);
  if ($length === 0) {
    return true;
  }
  if ($ignoreCase) {
    $haystack = strtolower($haystack);
    $needle = strtolower($needle);
  }
  // PHP 8.0 support
  if (function_exists("str_ends_with")) {
    return str_ends_with($haystack, $needle);
  } else {
    return (substr($haystack, -$length) === $needle);
  }
}
function contains($haystack, $needle, bool $ignoreCase = false): bool {
  if (is_array($haystack)) {
    return in_array($needle, $haystack);
  }
  if ($ignoreCase) {
    $haystack = strtolower($haystack);
    $needle = strtolower($needle);
  }
  // PHP 8.0 support
  if (function_exists("str_contains")) {
    return str_contains($haystack, $needle);
  } else {
    return strpos($haystack, $needle) !== false;
  }
}
function intendCode($code, $escape = true) {
  $newCode = "";
  $first = true;
  $brackets = array();
  $intend = 0;
  foreach (explode("\n", $code) as $line) {
    if (!$first) $newCode .= "\n";
    if ($escape) $line = htmlspecialchars($line);
    $line = trim($line);
    if (count($brackets) > 0 && startsWith($line, current($brackets))) {
      $intend = max(0, $intend - 2);
      array_pop($brackets);
    }
    $newCode .= str_repeat(" ", $intend);
    $newCode .= $line;
    $first = false;
    if (endsWith($line, "{")) {
      $intend += 2;
      array_push($brackets, "}");
    } else if (endsWith($line, "(")) {
      $intend += 2;
      array_push($brackets, ")");
    }
  }
  return $newCode;
}
function replaceCssSelector($sel) {
  return preg_replace("~[.#<>]~", "_", preg_replace("~[:\-]~", "", $sel));
}
function urlId($str) {
  return urlencode(htmlspecialchars(preg_replace("[: ]","-", $str)));
}
function html_attributes(array $attributes): string {
  return implode(" ", array_map(function ($key) use ($attributes) {
    $value = $attributes[$key];
    return "$key=\"$value\"";
  }, array_keys($attributes)));
}
function getClassPath($class, string $suffix = ".class"): string {
  $path = str_replace('\\', '/', $class);
  $path = array_values(array_filter(explode("/", $path)));
  if (count($path) > 2 && strcasecmp($path[0], "api") === 0 && strcasecmp($path[1], "Parameter") !== 0) {
    $path = "Api/" . $path[1] . "API";
  } else {
    $path = implode("/", $path);
  }
  return "core/$path$suffix.php";
}
function createError($msg) {
  return json_encode(array("success" => false, "msg" => $msg));
}
function downloadFile($handle, $offset = 0, $length = null): bool {
  if($handle === false) {
    return false;
  }
  if ($offset > 0) {
    fseek($handle, $offset);
  }
  $bytesRead = 0;
  $bufferSize = 1024*16;
  while (!feof($handle) && ($length === null || $bytesRead < $length)) {
    $chunkSize = ($length === null ? $bufferSize : min($length - $bytesRead, $bufferSize));
    echo fread($handle, $chunkSize);
  }
  fclose($handle);
  return true;
}
function serveStatic(string $webRoot, string $file): string {
  $path = realpath($webRoot . "/" . $file);
  if (!startsWith($path, $webRoot . "/")) {
    http_response_code(406);
    return "Access restricted, requested file outside web root: " . htmlspecialchars($path);
  }
  if (!file_exists($path) || !is_file($path) || !is_readable($path)) {
    http_response_code(500);
    return "Unable to read file: " . htmlspecialchars($path);
  }
  $pathInfo = pathinfo($path);
  // TODO: add more file extensions here, probably add them to settings?
  $allowedExtension = array("html", "htm", "pdf");
  $ext = $pathInfo["extension"] ?? "";
  if (!in_array($ext, $allowedExtension)) {
    http_response_code(406);
    return "Access restricted: Extension '" . htmlspecialchars($ext) . "' not allowed.";
  }
  $size = filesize($path);
  $mimeType = mime_content_type($path);
  header("Content-Type: $mimeType"); // TODO: do we need to check mime type?
  header("Content-Length: $size");
  header('Accept-Ranges: bytes');
  if (strcasecmp($_SERVER["REQUEST_METHOD"], "HEAD") !== 0) {
    $handle = fopen($path, "rb");
    if($handle === false) {
      http_response_code(500);
      return "Unable to read file: " . htmlspecialchars($path);
    }
    $offset = 0;
    $length = $size;
    if (isset($_SERVER['HTTP_RANGE'])) {
      preg_match('/bytes=(\d+)-(\d+)?/', $_SERVER['HTTP_RANGE'], $matches);
      $offset = intval($matches[1]);
      $length = intval($matches[2]) - $offset;
      http_response_code(206);
      header('Content-Range: bytes ' . $offset . '-' . ($offset + $length) . '/' . $size);
    }
    downloadFile($handle, $offset, $length);
  }
  return "";
}
function parseClass($class) {
  if (!startsWith($class, "\\")) {
    $class = "\\$class";
  }
  $parts = explode("\\", $class);
  $parts = array_map('ucfirst', $parts);
  return implode("\\", $parts);
}