param(
  [int]$Port = 4174,
  [switch]$NoBrowser
)

$ErrorActionPreference = "Stop"
$root = [IO.Path]::GetFullPath($PSScriptRoot)
$prefix = "http://127.0.0.1:$Port/"
$listener = [Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, $Port)

$contentTypes = @{
  ".css" = "text/css; charset=utf-8"
  ".html" = "text/html; charset=utf-8"
  ".js" = "text/javascript; charset=utf-8"
  ".json" = "application/json; charset=utf-8"
  ".png" = "image/png"
  ".jpg" = "image/jpeg"
  ".svg" = "image/svg+xml"
  ".woff" = "font/woff"
  ".woff2" = "font/woff2"
  ".txt" = "text/plain; charset=utf-8"
}

function Send-Response($client, [int]$status, [string]$reason, [byte[]]$body, [string]$contentType) {
  $stream = $client.GetStream()
  $header = "HTTP/1.1 $status $reason`r`nContent-Type: $contentType`r`nContent-Length: $($body.Length)`r`nConnection: close`r`n`r`n"
  $headerBytes = [Text.Encoding]::ASCII.GetBytes($header)
  $stream.Write($headerBytes, 0, $headerBytes.Length)
  $stream.Write($body, 0, $body.Length)
  $stream.Close()
  $client.Close()
}

try {
  $listener.Start()
} catch {
  Write-Host "Unable to start the offline server. The port may already be in use." -ForegroundColor Red
  Write-Host $_.Exception.Message -ForegroundColor Red
  Read-Host "Press Enter to close"
  exit 1
}

Write-Host "Taoyuan Blockly offline edition" -ForegroundColor Yellow
Write-Host "URL: $prefix"
Write-Host "The browser will open automatically. Close this window to stop the server."
if (-not $NoBrowser) { Start-Process $prefix }

$rootWithSeparator = $root.TrimEnd([IO.Path]::DirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar
try {
  while ($true) {
    $client = $listener.AcceptTcpClient()
    try {
      $stream = $client.GetStream()
      $buffer = New-Object byte[] 8192
      $read = $stream.Read($buffer, 0, $buffer.Length)
      if ($read -le 0) { $client.Close(); continue }
      $request = [Text.Encoding]::ASCII.GetString($buffer, 0, $read)
      if ($request -notmatch "^GET\s+(\S+)") {
        Send-Response $client 405 "Method Not Allowed" ([Text.Encoding]::UTF8.GetBytes("Only GET is supported")) "text/plain; charset=utf-8"
        continue
      }
      $path = [Uri]::UnescapeDataString(($matches[1] -split "\?", 2)[0]).TrimStart("/")
      if ([string]::IsNullOrWhiteSpace($path)) { $path = "index.html" }
      $target = [IO.Path]::GetFullPath((Join-Path $root $path))
      if (-not $target.StartsWith($rootWithSeparator, [StringComparison]::OrdinalIgnoreCase)) {
        Send-Response $client 403 "Forbidden" ([Text.Encoding]::UTF8.GetBytes("Forbidden")) "text/plain; charset=utf-8"
        continue
      }
      if (-not (Test-Path -LiteralPath $target -PathType Leaf)) {
        Send-Response $client 404 "Not Found" ([Text.Encoding]::UTF8.GetBytes("Not found")) "text/plain; charset=utf-8"
        continue
      }
      $body = [IO.File]::ReadAllBytes($target)
      $extension = [IO.Path]::GetExtension($target).ToLowerInvariant()
      $contentType = if ($contentTypes.ContainsKey($extension)) { $contentTypes[$extension] } else { "application/octet-stream" }
      Send-Response $client 200 "OK" $body $contentType
    } catch {
      try { Send-Response $client 500 "Internal Server Error" ([Text.Encoding]::UTF8.GetBytes("Offline server error")) "text/plain; charset=utf-8" } catch { }
    }
  }
} finally {
  $listener.Stop()
  $listener.Close()
}
