import { useCallback, useContext, useEffect, useState } from "react";
import * as sodium from "libsodium-wrappers-sumo";
import useWebSocket from "react-use-websocket";
import CBOR from "cbor";
import { wsUrl } from "./constants";
import { RoomContext } from "./lib/roomContext";
import { useDebouncedCallback } from "use-debounce";
import {
  Row,
  Col,
  Button,
  Form,
  Badge,
  OverlayTrigger,
  Tooltip,
} from "react-bootstrap";

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

  const setHashedPassphrase = useDebouncedCallback(
    (passphrase) => {
      if (passphrase) {
        const salt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
        console.log("salt", salt);
        setEncryptionKey(
          sodium.crypto_pwhash(
            sodium.crypto_box_SEEDBYTES,
            passphrase,
            salt,
            sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
            sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
            sodium.crypto_pwhash_ALG_ARGON2ID13
          )
        );
      }
    },
    // delay in ms
    1000
  );

  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 } = useWebSocket(wsUrl, {
    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
        );
        setDecryptState(state);
      }

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

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

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

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

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

    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,
        });
      }

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

  async function selectSaveFile() {
    const newHandle = await window.showSaveFilePicker();
    setSelectedSaveFile(newHandle);

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

  return (
    <>
      <h3>How does this work?</h3>
      <p style={{ color: "darkred" }}>
        <b>
          RECEIVER MUST BE USING CHROME{" "}
          <a
            href="https://caniuse.com/mdn-api_window_showsavefilepicker"
            target="_blank"
          >
            [?]
          </a>
        </b>
      </p>
      <ol>
        <li>
          <b>Uploader:</b> Copy the key and send it to whoever you're sending
          files to
        </li>
        <li>
          <b>Receiver:</b> Paste the key into the "Paste key here" box
        </li>
        <li>
          <b>Receiver:</b> Press the "select save file" button to choose where
          to save the file to
        </li>
        <li>
          <b>Receiver:</b> Tell the uploader to press "upload"
        </li>
        <li>
          <b>Uploader:</b> Press "upload"
        </li>
      </ol>
      <p>Only one person can upload at a time.</p>

      <Row>
        <Col md={6}>
          <h3>Send file</h3>

          {/* <p>
            Upload passphrase:
            <Form.Control
              value={passphrase}
              onChange={(e) => {
                setEncryptionKey(null);
                setPassphrase(e.target.value);
              }}
            />
          </p> */}

          <p>
            Upload Key:{" "}
            <Form.Control
              type="text"
              disabled
              value={encryptionKey ? bytes2Hex(encryptionKey) : "generating..."}
            />
          </p>

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

        <Col md={6}>
          <h3>
            Receive file
            {readyToDownload ? (
              <Badge bg="success">ready</Badge>
            ) : (
              <OverlayTrigger
                placement="top"
                overlay={
                  <Tooltip>
                    you are not ready to download, make sure you enter the key
                    and select a place to save
                  </Tooltip>
                }
              >
                <Badge bg="warning">not ready</Badge>
              </OverlayTrigger>
            )}
          </h3>

          <ol>
            <li>
              <Form.Control
                type="text"
                value={decryptionKey}
                onChange={(e) => setDecryptionKey(e.target.value)}
                placeholder="Paste key here..."
              />
            </li>
            <li>
              <Button onClick={selectSaveFile} disabled={!!selectedSaveFile}>
                {selectedSaveFile
                  ? `Saving to "${selectedSaveFile.name}"`
                  : "select save file"}
              </Button>
            </li>
          </ol>
        </Col>
      </Row>
    </>
  );
}

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);
}
