import { useCallback, useEffect, useState } from "react";
import * as sodium from "libsodium-wrappers";
import useWebSocket from "react-use-websocket";
import CBOR from "cbor";

export default function Upload({ roomId }) {
  const [selectedFile, setSelectedFile] = useState<File | null>(null);
  const [encryptionKey, setEncryptionKey] = useState(null);
  const [uploadProgress, setUploadProgress] = useState(null);

  const [decryptionKey, setDecryptionKey] = useState("");
  const [selectedSaveFile, setSelectedSaveFile] = useState(null);
  const [decryptState, setDecryptState] = useState(null);
  const [pendingDecryption, setPendingDecryption] = useState([]);

  let decryptionKeyInternal: Uint8Array;
  if (decryptionKey) {
    decryptionKeyInternal = hex2Bytes(decryptionKey);
  }
  const [writeStream, setWriteStream] = useState(null);
  const readyToDownload =
    !!decryptionKeyInternal && !!selectedSaveFile && !!writeStream;

  const {
    sendMessage,
    lastJsonMessage,
    readyState: newReadyState,
  } = useWebSocket("ws://localhost:3100/ws", {
    share: true,
    onMessage: async (event) => {
      const message = CBOR.decode(await event.data.arrayBuffer());

      if (message.type === "FileUploadBegin") {
        const state = sodium.crypto_secretstream_xchacha20poly1305_init_pull(
          message.header,
          decryptionKeyInternal
        );
        console.log("header", message.header);
        console.log("state", state);
        setDecryptState(state);
      }

      if (message.type === "FileUploadChunk") {
        console.log("data", message);

        if (readyToDownload && decryptState) {
          for (const { data } of [...pendingDecryption, message]) {
            const { message } =
              sodium.crypto_secretstream_xchacha20poly1305_pull(
                decryptState,
                data
              );
            const result = await writeStream.write(message);
            console.log("output", result, message);
          }
          setPendingDecryption([]);
        } else {
          setPendingDecryption([...pendingDecryption, message]);
        }
      }

      if (message.type === "FileUploadComplete") {
        await writeStream.close();
        console.log("done!");
      }
    },
  });

  function sendCborMessage(data) {
    let cbor = CBOR.encode(data);
    sendMessage(cbor);
  }

  useEffect(() => {
    (async () => {
      await sodium.ready;
      const key = sodium.crypto_secretstream_xchacha20poly1305_keygen();
      setEncryptionKey(key);
    })();
  }, []);

  const uploadFile = useCallback(async () => {
    if (!encryptionKey) return;
    if (!selectedFile) return;

    console.log("file", selectedFile);
    const { state, header } =
      sodium.crypto_secretstream_xchacha20poly1305_init_push(encryptionKey);
    sendCborMessage({
      type: "FileUploadBegin",
      room_id: roomId,
      header,
    });

    const stream = selectedFile.stream();
    const reader = stream.getReader();

    try {
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        // Encrypt this chunk
        const encryptedValue =
          sodium.crypto_secretstream_xchacha20poly1305_push(
            state,
            value,
            null,
            0
          );

        sendCborMessage({
          type: "FileUploadChunk",
          room_id: roomId,
          data: encryptedValue,
        });
        console.log("sent encrypted chunk", encryptedValue);
      }

      sendCborMessage({
        type: "FileUploadComplete",
        room_id: roomId,
      });
      console.log("sent done");
    } finally {
      reader.releaseLock();
    }
  }, [encryptionKey, selectedFile]);

  async function selectSaveFile() {
    const newHandle = await window.showSaveFilePicker();
    console.log("handle", newHandle);
    setSelectedSaveFile(newHandle);

    const writableStream = await newHandle.createWritable();
    setWriteStream(writableStream);
  }

  return (
    <div>
      <h3>Send file</h3>

      <p>Upload Key: {encryptionKey && bytes2Hex(encryptionKey)}</p>

      <input type="file" onChange={(e) => setSelectedFile(e.target.files[0])} />
      <button onClick={uploadFile}>upload</button>

      <hr />

      <h3>Receive file</h3>

      <p>
        <ol>
          <li>
            <input
              type="text"
              value={decryptionKey}
              onChange={(e) => setDecryptionKey(e.target.value)}
              placeholder="Download Key..."
            />
          </li>
          <li>
            <button onClick={selectSaveFile} disabled={!!selectedSaveFile}>
              {selectedSaveFile
                ? `Saving to "${selectedSaveFile.name}"`
                : "select save file"}
            </button>
          </li>
        </ol>
        (
        {readyToDownload
          ? "you are ready to download"
          : "you are not ready to download, make sure you enter the key and select a place to save"}
        )
      </p>
    </div>
  );
}

function bytes2Hex(byteArray: Uint8Array): string {
  return Array.prototype.map
    .call(byteArray, function (byte: number) {
      return ("0" + (byte & 0xff).toString(16)).slice(-2);
    })
    .join("");
}

function hex2Bytes(hexString: string): Uint8Array {
  let result = [];
  for (var i = 0; i < hexString.length; i += 2) {
    result.push(parseInt(hexString.substr(i, 2), 16));
  }
  return new Uint8Array(result);
}
