<!doctype html>
<html lang="en-us">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>zstd WebAssembly example</title>
    <style>
      :root {
        color-scheme: light;
        font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
        color: #18202a;
        background: #edf1f4;
      }

      * {
        box-sizing: border-box;
      }

      body {
        min-width: 320px;
        margin: 0;
      }

      main {
        width: min(820px, calc(100% - 32px));
        margin: 40px auto;
      }

      header {
        display: flex;
        align-items: baseline;
        justify-content: space-between;
        gap: 16px;
        margin-bottom: 16px;
      }

      h1 {
        margin: 0;
        font-size: 24px;
        font-weight: 700;
        letter-spacing: 0;
      }

      #runtime-status {
        margin: 0;
        color: #566372;
        font-size: 14px;
      }

      #runtime-status.ready,
      #verification.passed {
        color: #13734b;
      }

      #runtime-status.failed,
      #verification.failed,
      #error {
        color: #b4232a;
      }

      .workspace {
        overflow: hidden;
        border: 1px solid #cbd3db;
        border-radius: 6px;
        background: #fff;
      }

      .editor {
        padding: 20px;
      }

      label {
        display: block;
        margin-bottom: 8px;
        color: #354150;
        font-size: 13px;
        font-weight: 650;
      }

      textarea {
        display: block;
        width: 100%;
        min-height: 180px;
        resize: vertical;
        border: 1px solid #aeb8c3;
        border-radius: 4px;
        padding: 12px;
        color: #18202a;
        background: #fbfcfd;
        font: 14px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
        letter-spacing: 0;
      }

      textarea:focus,
      input:focus-visible,
      button:focus-visible {
        outline: 3px solid rgb(37 99 235 / 25%);
        outline-offset: 1px;
        border-color: #2563eb;
      }

      .controls {
        display: grid;
        grid-template-columns: minmax(180px, 1fr) auto;
        align-items: end;
        gap: 20px;
        margin-top: 16px;
      }

      .level-header {
        display: flex;
        justify-content: space-between;
        gap: 12px;
      }

      #level-value {
        min-width: 3ch;
        color: #18202a;
        text-align: right;
      }

      input[type="range"] {
        width: 100%;
        accent-color: #2563eb;
      }

      button {
        min-height: 40px;
        border: 1px solid #1d4ed8;
        border-radius: 4px;
        padding: 9px 16px;
        color: #fff;
        background: #2563eb;
        font: inherit;
        font-weight: 650;
        letter-spacing: 0;
        cursor: pointer;
      }

      button:hover:not(:disabled) {
        background: #1d4ed8;
      }

      button:disabled {
        cursor: wait;
        opacity: 0.55;
      }

      .results {
        border-top: 1px solid #d8dee5;
        background: #f8fafb;
      }

      .metrics {
        display: grid;
        grid-template-columns: repeat(3, 1fr);
        margin: 0;
      }

      .metric {
        min-width: 0;
        padding: 16px 20px;
        border-right: 1px solid #d8dee5;
      }

      .metric:last-child {
        border-right: 0;
      }

      dt {
        margin-bottom: 5px;
        color: #697686;
        font-size: 12px;
      }

      dd {
        margin: 0;
        overflow-wrap: anywhere;
        font-size: 20px;
        font-weight: 700;
      }

      .output {
        padding: 0 20px 20px;
      }

      .output-header {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 16px;
        margin-top: 16px;
      }

      .output-header label,
      #verification {
        margin: 0;
      }

      #verification {
        font-size: 13px;
        font-weight: 650;
      }

      #compressed-output,
      #error {
        min-height: 64px;
        margin: 8px 0 0;
        overflow-wrap: anywhere;
        white-space: pre-wrap;
        font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
        letter-spacing: 0;
      }

      #compressed-output {
        padding: 10px;
        border: 1px solid #d8dee5;
        border-radius: 4px;
        color: #354150;
        background: #fff;
      }

      #error {
        min-height: 0;
      }

      @media (max-width: 560px) {
        main {
          width: min(100% - 20px, 820px);
          margin: 20px auto;
        }

        header,
        .controls {
          align-items: stretch;
          grid-template-columns: 1fr;
        }

        header {
          display: grid;
        }

        .metrics {
          grid-template-columns: 1fr;
        }

        .metric {
          border-right: 0;
          border-bottom: 1px solid #d8dee5;
        }

        .metric:last-child {
          border-bottom: 0;
        }
      }
    </style>
  </head>
  <body>
    <main>
      <header>
        <h1>zstd WebAssembly</h1>
        <p id="runtime-status" role="status">Loading module...</p>
      </header>

      <section class="workspace" aria-label="zstd example">
        <div class="editor">
          <label for="source-input">Source text</label>
          <textarea id="source-input" spellcheck="false">WebAssembly keeps this zstd example portable across browsers and native runtimes. 你好,zstd!</textarea>

          <div class="controls">
            <div>
              <div class="level-header">
                <label for="compression-level">Compression level</label>
                <output id="level-value" for="compression-level">3</output>
              </div>
              <input id="compression-level" type="range" min="-5" max="22" value="3">
            </div>
            <button id="run" type="button" disabled>Compress and decompress</button>
          </div>
        </div>

        <div class="results" aria-live="polite">
          <dl class="metrics">
            <div class="metric">
              <dt>Input</dt>
              <dd id="input-size">-</dd>
            </div>
            <div class="metric">
              <dt>Compressed</dt>
              <dd id="compressed-size">-</dd>
            </div>
            <div class="metric">
              <dt>Ratio</dt>
              <dd id="compression-ratio">-</dd>
            </div>
          </dl>

          <div class="output">
            <div class="output-header">
              <label for="compressed-output">Compressed bytes</label>
              <p id="verification">Pending</p>
            </div>
            <pre id="compressed-output">-</pre>
            <pre id="error" hidden></pre>
          </div>
        </div>
      </section>
    </main>

    <script type="module">
      const WASM_FILE_NAME = "./debug/zstd.js";
      const runtimeStatus = document.querySelector("#runtime-status");
      const sourceInput = document.querySelector("#source-input");
      const compressionLevel = document.querySelector("#compression-level");
      const levelValue = document.querySelector("#level-value");
      const runButton = document.querySelector("#run");
      const verification = document.querySelector("#verification");
      const errorOutput = document.querySelector("#error");
      const compressedOutput = document.querySelector("#compressed-output");
      const textEncoder = new TextEncoder();
      const textDecoder = new TextDecoder("utf-8", { fatal: true });
      let zstd;

      function formatBytes(size) {
        return `${size.toLocaleString()} B`;
      }

      function formatHex(bytes) {
        const limit = Math.min(bytes.length, 160);
        const values = [];
        for (let index = 0; index < limit; index += 1) {
          values.push(bytes[index].toString(16).padStart(2, "0"));
        }
        return `${values.join(" ")}${bytes.length > limit ? " ..." : ""}`;
      }

      function showError(error) {
        verification.textContent = "Failed";
        verification.className = "failed";
        errorOutput.textContent = error instanceof Error ? error.message : String(error);
        errorOutput.hidden = false;
      }

      function runExample() {
        runButton.disabled = true;
        errorOutput.hidden = true;

        try {
          const source = sourceInput.value;
          const inputBytes = textEncoder.encode(source);
          const compressed = zstd.compress(source, Number(compressionLevel.value));
          const restored = textDecoder.decode(zstd.decompress(compressed));

          if (restored !== source) {
            throw new Error("Round-trip output does not match the source text.");
          }

          document.querySelector("#input-size").textContent = formatBytes(inputBytes.length);
          document.querySelector("#compressed-size").textContent = formatBytes(compressed.length);
          document.querySelector("#compression-ratio").textContent = inputBytes.length === 0
            ? "N/A"
            : `${((compressed.length / inputBytes.length) * 100).toFixed(1)}%`;
          compressedOutput.textContent = formatHex(compressed);
          verification.textContent = "Round-trip passed";
          verification.className = "passed";
        } catch (error) {
          showError(error);
        } finally {
          runButton.disabled = false;
        }
      }

      compressionLevel.addEventListener("input", () => {
        levelValue.value = compressionLevel.value;
      });
      runButton.addEventListener("click", runExample);

      try {
        const module = await import(WASM_FILE_NAME);
        zstd = await module.default();
        runtimeStatus.textContent = "Ready";
        runtimeStatus.className = "ready";
        runButton.disabled = false;
        runExample();
      } catch (error) {
        runtimeStatus.textContent = "Module failed to load";
        runtimeStatus.className = "failed";
        showError(error);
      }
    </script>
  </body>
</html>