/*
 This file is part of GNU Taler
 (C) 2022 Taler Systems S.A.

 GNU Taler is free software; you can redistribute it and/or modify it under the
 terms of the GNU General Public License as published by the Free Software
 Foundation; either version 3, or (at your option) any later version.

 GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

 You should have received a copy of the GNU General Public License along with
 GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */

import {
  classifyTalerUri,
  TalerUriType,
  TranslatedString,
} from "@gnu-taler/taler-util";
import { styled } from "@linaria/react";
import { css } from "@linaria/core";
import { Fragment, h, VNode } from "preact";
import { Ref, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { useTranslationContext } from "../context/translation.js";
import { Alert } from "../mui/Alert.js";
import { Button } from "../mui/Button.js";
import { TextField } from "../mui/TextField.js";
import jsQR, * as pr from "jsqr";
import { InputFile } from "../mui/InputFile.js";
import { Grid } from "../mui/Grid.js";
import { notDeepEqual } from "assert";

const QrCanvas = css`
  width: 80%;
  margin-left: auto;
  margin-right: auto;
  padding: 8px;
  background-color: black;
`;

const LINE_COLOR = "#FF3B58";

const Container = styled.div`
  display: flex;
  flex-direction: column;
  & > * {
    margin-bottom: 20px;
  }
`;

interface Props {
  onDetected: (url: string) => void;
}

type XY = { x: number; y: number };

function drawLine(
  canvas: CanvasRenderingContext2D,
  begin: XY,
  end: XY,
  color: string,
) {
  canvas.beginPath();
  canvas.moveTo(begin.x, begin.y);
  canvas.lineTo(end.x, end.y);
  canvas.lineWidth = 4;
  canvas.strokeStyle = color;
  canvas.stroke();
}

function drawBox(context: CanvasRenderingContext2D, code: pr.QRCode) {
  drawLine(
    context,
    code.location.topLeftCorner,
    code.location.topRightCorner,
    LINE_COLOR,
  );
  drawLine(
    context,
    code.location.topRightCorner,
    code.location.bottomRightCorner,
    LINE_COLOR,
  );
  drawLine(
    context,
    code.location.bottomRightCorner,
    code.location.bottomLeftCorner,
    LINE_COLOR,
  );
  drawLine(
    context,
    code.location.bottomLeftCorner,
    code.location.topLeftCorner,
    LINE_COLOR,
  );
}

const SCAN_PER_SECONDS = 3;
const TIME_BETWEEN_FRAMES = 1000 / SCAN_PER_SECONDS;

async function delay(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function drawIntoCanvasAndGetQR(
  tag: HTMLVideoElement | HTMLImageElement,
  canvas: HTMLCanvasElement,
): string | undefined {
  const context = canvas.getContext("2d");
  if (!context) {
    throw Error("no 2d canvas context");
  }
  context.clearRect(0, 0, canvas.width, canvas.height);
  context.drawImage(tag, 0, 0, canvas.width, canvas.height);
  const imgData = context.getImageData(0, 0, canvas.width, canvas.height);
  const code = jsQR(imgData.data, canvas.width, canvas.height, {
    inversionAttempts: "attemptBoth",
  });
  if (code) {
    drawBox(context, code);
    return code.data;
  }
  return undefined;
}

async function readNextFrame(
  video: HTMLVideoElement,
  canvas: HTMLCanvasElement,
): Promise<string | undefined> {
  const requestFrame =
    "requestVideoFrameCallback" in video
      ? video.requestVideoFrameCallback.bind(video)
      : requestAnimationFrame;

  return new Promise<string | undefined>((ok, bad) => {
    requestFrame(() => {
      try {
        const code = drawIntoCanvasAndGetQR(video, canvas);
        ok(code);
      } catch (error) {
        bad(error);
      }
    });
  });
}

async function createCanvasFromVideo(
  video: HTMLVideoElement,
  canvas: HTMLCanvasElement,
): Promise<string> {
  const context = canvas.getContext("2d", {
    willReadFrequently: true,
  });
  if (!context) {
    throw Error("no 2d canvas context");
  }
  canvas.width = video.videoWidth;
  canvas.height = video.videoHeight;

  let last = Date.now();

  let found: string | undefined = undefined;
  while (!found) {
    const timeSinceLast = Date.now() - last;
    if (timeSinceLast < TIME_BETWEEN_FRAMES) {
      await delay(TIME_BETWEEN_FRAMES - timeSinceLast);
    }
    last = Date.now();
    found = await readNextFrame(video, canvas);
  }
  video.pause();
  return found;
}

async function createCanvasFromFile(
  source: string,
  canvas: HTMLCanvasElement,
): Promise<string | undefined> {
  const img = new Image(300, 300);
  img.src = source;
  canvas.width = img.width;
  canvas.height = img.height;
  return new Promise<string | undefined>((ok, bad) => {
    img.addEventListener("load", (e) => {
      try {
        const code = drawIntoCanvasAndGetQR(img, canvas);
        ok(code);
      } catch (error) {
        bad(error);
      }
    });
  });
}

async function waitUntilReady(video: HTMLVideoElement): Promise<void> {
  return new Promise((ok, bad) => {
    if (video.readyState === video.HAVE_ENOUGH_DATA) {
      return ok();
    }
    setTimeout(waitUntilReady, 100);
  });
}

export function QrReaderPage({ onDetected }: Props): VNode {
  const videoRef = useRef<HTMLVideoElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [error, setError] = useState<TranslatedString | undefined>();
  const [value, setValue] = useState("");
  const [show, setShow] = useState<"canvas" | "video" | "nothing">("nothing");

  const { i18n } = useTranslationContext();

  function onChange(str: string) {
    if (!!str) {
      if (!str.startsWith("taler://")) {
        setError(
          i18n.str`URI is not valid. Taler URI should start with "taler://"`,
        );
      } else if (classifyTalerUri(str) === TalerUriType.Unknown) {
        setError(i18n.str`Unknown type of Taler URI`);
      } else {
        setError(undefined);
      }
    } else {
      setError(undefined);
    }
    setValue(str);
  }

  async function startVideo() {
    if (!videoRef.current || !canvasRef.current) {
      return;
    }
    const video = videoRef.current;
    if (!video || !video.played) return;
    const stream = await navigator.mediaDevices.getUserMedia({
      video: { facingMode: "environment" },
      audio: false,
    });
    setShow("video");
    setError(undefined);
    video.srcObject = stream;
    await video.play();
    await waitUntilReady(video);
    try {
      const code = await createCanvasFromVideo(video, canvasRef.current);
      if (code) {
        onChange(code);
        setShow("canvas");
      }
      stream.getTracks().forEach((e) => {
        e.stop();
      });
    } catch (error) {
      setError(i18n.str`something unexpected happen: ${error}`);
    }
  }

  async function onFileRead(fileContent: string) {
    if (!canvasRef.current) {
      return;
    }
    setShow("nothing");
    setError(undefined);
    try {
      const code = await createCanvasFromFile(fileContent, canvasRef.current);
      if (code) {
        onChange(code);
        setShow("canvas");
      } else {
        setError(i18n.str`Could not found a QR code in the file`);
      }
    } catch (error) {
      setError(i18n.str`something unexpected happen: ${error}`);
    }
  }

  const active = value === "";
  return (
    <Container>
      <section>
        <h1>
          <i18n.Translate>
            Scan a QR code or enter taler:// URI below
          </i18n.Translate>
        </h1>

        <p>
          <TextField
            label="Taler URI"
            variant="standard"
            fullWidth
            value={value}
            onChange={onChange}
          />
        </p>
        <Grid container justifyContent="space-around" columns={2}>
          <Grid item xs={2}>
            <p>{error && <Alert severity="error">{error}</Alert>}</p>
          </Grid>
          <Grid item xs={1}>
            {!active && (
              <Button
                variant="contained"
                onClick={async () => {
                  setShow("nothing");
                  onChange("");
                }}
                color="error"
              >
                <i18n.Translate>Clear</i18n.Translate>
              </Button>
            )}
          </Grid>
          <Grid item xs={1}>
            {value && (
              <Button
                disabled={!!error}
                variant="contained"
                color="success"
                onClick={async () => onDetected(value)}
              >
                <i18n.Translate>Open</i18n.Translate>
              </Button>
            )}
          </Grid>
          <Grid item xs={1}>
            <InputFile onChange={onFileRead}>Read QR from file</InputFile>
          </Grid>
          <Grid item xs={1}>
            <p>
              <Button variant="contained" onClick={startVideo}>
                Use Camera
              </Button>
            </p>
          </Grid>
        </Grid>
      </section>
      <div>
        <video
          ref={videoRef}
          style={{ display: show === "video" ? "unset" : "none" }}
          playsInline={true}
        />
        <canvas
          id="este"
          class={QrCanvas}
          ref={canvasRef}
          style={{ display: show === "canvas" ? "unset  " : "none" }}
        />
      </div>
    </Container>
  );
}
