<script context="module" lang="ts">
 export type MaskCanvasData = {
     hasMask: boolean,
     maskCanvas: HTMLCanvasElement | null,
     curLineGroup: LineGroup,
     redoCurLines: LineGroup,
 }
 export type LinePoint = {
     x: number,
     y: number
 }
 export interface Line {
     size?: number,
     points: LinePoint[]
 }
 export type LineGroup = Line[];

</script>

<script lang="ts">
 import { loadImage } from "$lib/widgets/utils"
 import { tick, createEventDispatcher } from "svelte";
 import { ArrowClockwise, ArrowCounterclockwise, XSquare, Exclude, Circle, Grid3x3Gap, ArrowsFullscreen, FullscreenExit } from "svelte-bootstrap-icons";

 export let fileURL: string | null = null;
 export let fullscreen: boolean = false;

 const dispatch = createEventDispatcher<{
     change: MaskCanvasData;
     release: MaskCanvasData;
     loaded: MaskCanvasData
 }>();

 let canvasCursor: string | undefined = undefined;

 let container: HTMLDivElement | null;
 let canvas: HTMLCanvasElement | null;
 let maskCanvas: HTMLCanvasElement | null;
 let renders: HTMLImageElement[] = [];
 let context: CanvasRenderingContext2D | null;
 let maskContext: CanvasRenderingContext2D | null;
 let curLineGroup: LineGroup = [];
 let redoCurLines: LineGroup = []

 let original: HTMLImageElement | null;
 let isImageLoaded: boolean = false;
 let imageWidth: number = 512;
 let imageHeight: number = 512;
 let scale: number = 1.0;
 let minScale: number = 1.0;
 let brushSize: number = 100;
 let maskBlur: number = 0;
 let clipMask: boolean = false;
 let hasMask: boolean = false;

 let isDrawing: boolean = false;
 let isPanning: boolean = false;
 let isBrushShowing: boolean = false;

 let transform = ""
 $: transform = `translate(${imx}px, ${imy}px) scale(${scale})`

 let imx: number = 0;
 let imy: number = 0;

 let x: number = 0;
 let y: number = 0;

 let panX: number = 0;
 let panY: number = 0;

 const BRUSH_COLOR = "#000"

 enum MouseButton {
     Left = 0,
     Middle = 1,
     Right = 2,
     Back = 3,
     Forward = 4
 }

 $: if (isPanning) {
     canvasCursor = "grab";
 }
 else if (isImageLoaded && isBrushShowing) {
     canvasCursor = "none";
 }
 else {
     canvasCursor = undefined;
 }

 $: {
     context = canvas ? canvas.getContext("2d") : null
 }

 function clearState() {
     hasMask = false;
     maskCanvas = null;
     maskContext = null;
     isImageLoaded = false;
     original = null;
     renders = []
     // curLineGroup = [];
     // redoCurLines = []
     imageWidth = 512;
     imageHeight = 512;
     scale = 1.0;
     minScale = 0.5;
 }

 let loadedFileURL: string | null = null
 let dispatchLoaded: boolean = false;
 $: if (fileURL !== loadedFileURL) {
     clearState();
     if (fileURL) {
         loadImage(fileURL).then(i => {
             original = i;
             isImageLoaded = true;
             dispatchLoaded = true;
         })
                           .catch(i => {
                               isImageLoaded = false;
                           })
     }
     else {
         isImageLoaded = false;
         original = null;
     }
     loadedFileURL = fileURL
 }

 $: {
     // initSizeAndScale(isImageLoaded, original);
     [imageWidth, imageHeight] = getCurrentWidthAndHeight(isImageLoaded, original)
     scale = initScale(imageWidth, imageHeight)
     initImagePos()

     // in case mask strokes were preserved after new image load
     // (use case: sending an inpainted image back while reusing the same mask)
     tick().then(() => {
         loaded();
     })
 }

 function loaded() {
     if (!dispatchLoaded)
         return;

     dispatchLoaded = false;
     redrawCurLines()
     hasMask = curLineGroup.length > 0;
     console.warn("[MaskCanvas] LOADED", maskCanvas, hasMask)
     dispatch("loaded", {
         hasMask,
         maskCanvas,
         curLineGroup,
         redoCurLines
     })
 }

 $: hasMask = curLineGroup.length > 0;

 function initImagePos() {
     if (!container)
         return
     const rect = container.getBoundingClientRect();
     imx = rect.width / 2 - (imageWidth / 2) * scale
     imy = rect.height / 2 - (imageHeight / 2) * scale
 }

 function initScale(width: number, height: number): number {
     const s = getScale(width, height);
     minScale = s / 2;
     return s;
 }

 function initSizeAndScale(isImageLoaded: boolean, original: HTMLImageElement | null) {
     [imageWidth, imageHeight] = getCurrentWidthAndHeight(isImageLoaded, original)
     scale = getScale(imageWidth, imageHeight)
     minScale = scale;
 }

 function drawOnCurrentRender(lineGroup: LineGroup) {
     draw(lineGroup)
     dispatch("change", {
         hasMask,
         maskCanvas,
         curLineGroup,
         redoCurLines
     })
 }

 function draw(lineGroup: LineGroup) {
     if (!context || !maskContext)
         return

     context.clearRect(0, 0, context.canvas.width, context.canvas.height)
     maskContext.clearRect(0, 0, maskContext.canvas.width, maskContext.canvas.height)

     const color = BRUSH_COLOR

     const drawMask = (ctx: CanvasRenderingContext2D) => {
         ctx.save();
         ctx.filter = `blur(${maskBlur}px)`
         drawLines(ctx, lineGroup, color)
         ctx.restore();
     }

     drawMask(maskContext);

     if (clipMask) {
         context.save();
         context.filter = `blur(${maskBlur}px)`
         drawLines(context, lineGroup, color)
         context.restore();
         context.globalCompositeOperation = "source-in"
         context.drawImage(original!!, 0, 0, imageWidth, imageHeight)
         context.globalCompositeOperation = "source-over";
     }
     else {
         drawMask(context);
     }
 }

 function updateMaskImage() {
     drawOnCurrentRender(curLineGroup);
 }

 function drawLines(ctx: CanvasRenderingContext2D, lines: LineGroup, color: string) {
     ctx.strokeStyle = color
     ctx.lineCap = 'round'
     ctx.lineJoin = 'round'

     lines.forEach(line => {
         if (!line?.points.length || !line.size) {
             return
         }
         ctx.lineWidth = line.size
         ctx.beginPath()
         ctx.moveTo(line.points[0].x, line.points[0].y)
         line.points.forEach(point => ctx.lineTo(point.x, point.y))
         ctx.stroke()
     })
 }

 function redrawCurLines() {
     drawOnCurrentRender(curLineGroup || [])
 }

 $: if (canvas && original) {
     console.warn("INITCANVAS", imageWidth, imageHeight, original.src)
     maskCanvas = document.createElement("canvas");
     maskContext = maskCanvas.getContext("2d")!;
     maskCanvas.width = imageWidth;
     maskCanvas.height = imageHeight;
     canvas.width = imageWidth;
     canvas.height = imageHeight;
     redrawCurLines() // no react on curLineGroup
 }

 function getCurrentWidthAndHeight(isImageLoaded: boolean, original: HTMLImageElement | null) {
     if (isImageLoaded && original){
         return [original.naturalWidth, original.naturalHeight]
     }
     return [512, 512]
 }

 function getScale(width: number, height: number): number {
     const size = container?.getBoundingClientRect();
     if (!size) {
         return 1.0
     }

     const ratioWidth = size.width / width
     const ratioHeight = (size.height) / height

     let scale: number = 1.0
     if (ratioWidth < 1 || ratioHeight < 1) {
         scale = Math.min(ratioWidth, ratioHeight)
     }

     return scale
 }

 function undoStroke() {
     if (curLineGroup.length === 0) {
         return
     }

     const lastLine = curLineGroup.pop()!
     const newRedoCurLines = [...redoCurLines, lastLine]
     redoCurLines = newRedoCurLines

     const newLineGroup = [...curLineGroup]
     curLineGroup = newLineGroup
     drawOnCurrentRender(newLineGroup)
 }

 function redoStroke() {
     if (redoCurLines.length === 0) {
         return
     }

     const line = redoCurLines.pop()!
     redoCurLines = [...redoCurLines]

     const newLineGroup = [...curLineGroup, line]
     curLineGroup = newLineGroup
     drawOnCurrentRender(newLineGroup)
 }

 export function clearStrokes() {
     redoCurLines = []
     const newLineGroup: LineGroup = []
     curLineGroup = newLineGroup
     drawOnCurrentRender(newLineGroup)
 }

 export function recenterImage() {
     scale = initScale(imageWidth, imageHeight)
     initImagePos();
 }

 async function toggleFullscreen() {
     fullscreen = !fullscreen;
     updateMaskImage();
     await tick();
     recenterImage();
 }

 function onCanvasMouseOver() {
     isBrushShowing = true;
 }

 function onCanvasFocus() {
     isBrushShowing = true;
 }

 function onCanvasMouseLeave() {
     isBrushShowing = false;
 }

 function mouseXY(e: MouseEvent): LinePoint {
     return { x: e.offsetX, y: e.offsetY }
 }

 function onCanvasMouseDown(e: MouseEvent) {
     if (!original?.src)
         return;

     if (isPanning)
         return;

     if (canvas == null)
         return;

     switch (e.button) {
         case MouseButton.Right:
             return;
         case MouseButton.Middle:
             isPanning = true;
             panX = e.offsetX * scale;
             panY = e.offsetY * scale;
             return;
     }

     isDrawing = true;

     redoCurLines = []

     let lineGroup: LineGroup = [...curLineGroup]
     lineGroup.push({size: brushSize, points: [mouseXY(e)] })
     curLineGroup = lineGroup
     drawOnCurrentRender(curLineGroup);
 }

 function onCanvasMouseUp() {

 }

 function onCanvasMouseMove(e: MouseEvent) {
     if (isPanning)
         return;

     if (!isDrawing)
         return;

     if (curLineGroup.length === 0)
         return

     curLineGroup[curLineGroup.length-1].points.push(mouseXY(e))
     curLineGroup = curLineGroup; // react
     drawOnCurrentRender(curLineGroup)
 }

 function onCanvasMouseWheel(e: WheelEvent) {
     e.preventDefault();
     if (!container || e.target != canvas)
         return;

     const bound = container.getBoundingClientRect()

     // coodinates on the image that were zoomed
     const x_ = e.clientX - bound.x
     const y_ = e.clientY - bound.y

     e.preventDefault();
     var delta = e.deltaY * -0.001
     delta = Math.max(-1,Math.min(1,delta)) // cap the delta to [-1,1] for cross browser consistency

     const zx = (x_ - imx)/scale
     const zy = (y_ - imy)/scale

     scale += delta * scale
     scale = Math.max(minScale,scale)

     imx = -zx * scale + x_
     imy = -zy * scale + y_

     x = e.offsetX * scale;
     y = e.offsetY * scale;
 }

 function onMouseMove(e: MouseEvent) {
     if (e.target != canvas)
         return;

     x = e.offsetX * scale;
     y = e.offsetY * scale;

     if (isPanning) {
         imx += x - panX;
         imy += y - panY;
     }
 }

 function onMouseUp(e: MouseEvent) {
     if (e.button === MouseButton.Middle) {
         isPanning = false
         panX = 0
         panY = 0
     }

     if (isPanning)
         return;

     if (!original?.src)
         return;

     if (!canvas)
         return;

     if (!isDrawing)
         return;

     isDrawing = false;
     dispatch("release", { hasMask, maskCanvas, curLineGroup, redoCurLines })
 }

 function dispatchRelease() {
     updateMaskImage()
     dispatch("release", { hasMask, maskCanvas, curLineGroup, redoCurLines })
 }
</script>

<svelte:window on:mouseup={onMouseUp} />

<div class="me-container" class:fullscreen bind:this={container} on:mousemove={onMouseMove}>
    {#if !isImageLoaded}
        <div>
            (empty)
        </div>
    {:else}
        <div class="me-transform" style:transform={transform} style:--scale={scale}>
            <div class="me-canvas-container">
                <div class="me-original-image-container"
                     style:width="{imageWidth}px"
                     style:height="{imageHeight}px">
                    {#if original}
                        {@const showOriginal = !clipMask}
                        <img class="me-original-image"
                             src={original.src}
                             style:width={imageWidth}
                             style:height={imageHeight}
                             style:display={showOriginal ? "block" : "none"}
                        />
                    {/if}
                </div>
                <canvas class="me-canvas"
                        bind:this={canvas}
                        style:cursor={canvasCursor}
                        on:mouseover={onCanvasMouseOver}
                        on:focus={onCanvasFocus}
                        on:wheel={onCanvasMouseWheel}
                        on:mouseleave={onCanvasMouseLeave}
                        on:mousedown|preventDefault={onCanvasMouseDown}
                        on:mouseup|preventDefault={onCanvasMouseUp}
                        on:mousemove={onCanvasMouseMove}
                />
            </div>
        </div>
    {/if}
    {#if isImageLoaded && isBrushShowing && !isPanning}
        <div class="me-brush-cursor"
             style:width="{brushSize * scale}px"
             style:height="{brushSize * scale}px"
             style:left="{x + imx}px"
             style:top="{y + imy}px"
             style:transform="translate(-50%, -50%)"
        />
    {/if}
    <div class="me-toolkit-bar">
        <button disabled={curLineGroup.length === 0} on:click={undoStroke}>
            <ArrowCounterclockwise />
        </button>
        <button disabled={redoCurLines.length === 0} on:click={redoStroke}>
            <ArrowClockwise />
        </button>
        <button on:click={clearStrokes} disabled={curLineGroup.length === 0 && redoCurLines.length === 0}>
            <XSquare/>
        </button>
        <label>
            <Circle />
            <input type="range" min="1" max="200" bind:value={brushSize} step="0.1"
                   on:change={updateMaskImage}
                   on:pointerup={dispatchRelease}/>
        </label>
        <label>
            <Grid3x3Gap/>
            <input type="range" min="1" max="100" bind:value={maskBlur} step="0.1"
                   on:change={updateMaskImage}
                   on:pointerup={dispatchRelease}/>
        </label>
        <div class="toggle-button" class:toggled={clipMask} on:click={() => {clipMask = !clipMask; updateMaskImage()}}>
            <Exclude />
        </div>
        <div class="toggle-button" class:toggled={fullscreen} on:click={() => {toggleFullscreen()}}>
            {#if fullscreen}
                <FullscreenExit />
            {:else}
                <ArrowsFullscreen />
            {/if}
        </div>
    </div>
</div>

<style lang="scss">
 $bg-color: #a0a0a0;
 .me-container {
     width: 100%;
     height: 100%;
     position: relative;
     overflow: hidden;
     background-color: white;
     background-image:
         linear-gradient(45deg, #ccc 25%, transparent 25%),
         linear-gradient(135deg, #ccc 25%, transparent 25%),
         linear-gradient(45deg, transparent 75%, #ccc 75%),
         linear-gradient(135deg, transparent 75%, #ccc 75%);
     background-size:25px 25px; /* Must be a square */
     background-position:0 0, 12.5px 0, 12.5px -12.5px, 0px 12.5px; /* Must be half of one side of the square */

     &.fullscreen {
         position: fixed;
         top: 0;
         left: 0;
         width: 100vw;
         height: 100vh;
         margin: auto;
         z-index: var(--layer-top);
     }
 }

 .me-transform {
     --scale: 1;
     display: flex;
     flex-wrap: wrap;
     width: -moz-fit-content;
     width: fit-content;
     height: -moz-fit-content;
     height: fit-content;
     margin: 0;
     padding: 0;
     transform-origin: 0% 0%;
 }

 .me-original-image-container {
     position: absolute;
     top: 0;
     left: 0;
     grid-area: editor-content;
     pointer-events: none;
     user-select: none;
     display: grid;
     grid-template-areas: 'original-image-content';
     border: calc((1 / var(--scale)) * 5px) dashed grey;

     img.me-original-image {
         grid-area: original-image-content;
     }
 }

 .me-canvas {
     position: absolute;
     z-index: 1;
 }

 .me-brush-cursor {
     position: absolute;
     border-radius: 50%;
     background-color: #000;
     border: 1px solid var(--yellow-accent);
     pointer-events: none;
 }

 .me-toolkit-bar {
     position: absolute;
     bottom: 0.5rem;
     border-radius: 3rem;
     padding: 0.4rem 24px;
     display: flex;
     margin: 0.5rem auto;
     gap: 16px;
     left: 0;
     right: 0;
     width: 80%;
     height: 3rem;
     align-items: center;
     justify-content: space-evenly;
     backdrop-filter: blur(12px);
     background-color: white;
     animation: slideUp 0.2s ease-out;
     border: var(--editor-toolkit-panel-border);
     box-shadow: 0 0 0 4px #0000001a, 0 3px 16px #00000014, 0 2px 6px 1px #00000017;

     label {
         display: flex;
         flex-direction: row;
         gap: 4px;
         input {
             width: 5rem;
         }
     }

     button {
         &:not(:disabled) {
             cursor: pointer;
         }
         &:hover:not(:disabled) {
             color: var(--secondary-600);
         }
         &:disabled {
             opacity: 40%;
         }
     }

     .toggle-button {
         &:hover:not(:disabled) {
             color: var(--secondary-600);
         }
         &:not(:disabled) {
             cursor: pointer;
         }
         &.toggled {
             color: var(--secondary-400);
         }
     }
 }
</style>
