import { createContext, useState, useRef, useCallback, useMemo, useEffect, useReducer, useContext, forwardRef, useImperativeHandle } from 'react';
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
import T from 'prop-types';
import Draggable from 'react-draggable';

const calcDims = (width, height, externalMaxWidth, externalMaxHeight) => {
    const ratio = width / height;
    const maxWidth = externalMaxWidth || window.innerWidth;
    const maxHeight = externalMaxHeight || window.innerHeight;
    const calculated = {
        width: maxWidth,
        height: Math.round(maxWidth / ratio),
        ratio: ratio,
    };
    if (calculated.height > maxHeight) {
        calculated.height = maxHeight;
        calculated.width = Math.round(maxHeight * ratio);
    }
    return calculated;
};

function isCrossOriginURL(url) {
    const { location } = window;
    const parts = url.match(/^(\w+:)\/\/([^:/?#]*):?(\d*)/i);
    return (parts !== null &&
        (parts[1] !== location.protocol ||
            parts[2] !== location.hostname ||
            parts[3] !== location.port));
}

/**
 * Takes a file or a string, loads it and turns it into a DataURL
 * @param file as File | string
 * @returns DataURL (as string)
 */
const readFile = (file) => new Promise((resolve, reject) => {
    if (file instanceof File) {
        const reader = new FileReader();
        reader.onload = () => {
            resolve(reader.result);
        };
        reader.onerror = (err) => {
            reject(err);
        };
        reader.readAsDataURL(file);
    }
    else if (typeof file === "string")
        return resolve(file);
    else
        reject();
});

// https://amin-ahmadi.com/2016/03/24/sepia-filter-opencv/
const applyFilter = async (cv, docCanvas, filterCvParams = {}) => {
    // default options
    const options = {
        blur: false,
        th: true,
        thMode: cv.ADAPTIVE_THRESH_MEAN_C,
        thMeanCorrection: 15,
        thBlockSize: 25,
        thMax: 255,
        grayScale: true,
        ...filterCvParams,
    };
    const dst = cv.imread(docCanvas);
    if (options.grayScale) {
        cv.cvtColor(dst, dst, cv.COLOR_RGBA2GRAY, 0);
    }
    if (options.blur) {
        const ksize = new cv.Size(5, 5);
        cv.GaussianBlur(dst, dst, ksize, 0, 0, cv.BORDER_DEFAULT);
    }
    if (options.th) {
        if (options.grayScale) {
            cv.adaptiveThreshold(dst, dst, options.thMax, options.thMode, cv.THRESH_BINARY, options.thBlockSize, options.thMeanCorrection);
        }
        else {
            //@ts-ignore need to fix this type error (add to OpenCV type)
            dst.convertTo(dst, -1, 1, 60);
            cv.threshold(dst, dst, 170, 255, cv.THRESH_BINARY);
        }
    }
    cv.imshow(docCanvas, dst);
};

/**
 * perspective cropping utility (AKA keystone correction)
 * @param cv openCv
 * @param src The source image pointer
 * @param cropPoints
 * @param imageResizeRatio
 */
const warpPerspective = (cv, src, cropPoints, imageResizeRatio) => {
    // const src = cv.imread(docCanvas);
    const bR = cropPoints["right-bottom"];
    const bL = cropPoints["left-bottom"];
    const tR = cropPoints["right-top"];
    const tL = cropPoints["left-top"];
    // create source coordinates matrix
    const sourceCoordinates = [tL, tR, bR, bL].map((point) => [
        point.x / imageResizeRatio,
        point.y / imageResizeRatio,
    ]);
    // get max width
    const maxWidth = Math.max(bR.x - bL.x, tR.x - tL.x) / imageResizeRatio;
    // get max height
    const maxHeight = Math.max(bL.y - tL.y, bR.y - tR.y) / imageResizeRatio;
    // create dest coordinates matrix
    const destCoordinates = [
        [0, 0],
        [maxWidth - 1, 0],
        [maxWidth - 1, maxHeight - 1],
        [0, maxHeight - 1],
    ];
    // convert to open cv matrix objects
    const Ms = cv.matFromArray(4, 1, cv.CV_32FC2, [].concat(...sourceCoordinates));
    const Md = cv.matFromArray(4, 1, cv.CV_32FC2, [].concat(...destCoordinates));
    const transformMatrix = cv.getPerspectiveTransform(Ms, Md);
    // set new image size
    const dsize = new cv.Size(maxWidth, maxHeight);
    // perform warp
    cv.warpPerspective(src, src, transformMatrix, dsize, cv.INTER_LINEAR, cv.BORDER_CONSTANT, new cv.Scalar());
    // cv.imshow(docCanvas, src);
    // src.delete();
    Ms.delete();
    Md.delete();
    transformMatrix.delete();
};

function multiplyMatrices(m1, m2) {
    const result = [];
    for (let i = 0; i < m1.length; i++) {
        result[i] = [];
        for (let j = 0; j < m2[0].length; j++) {
            let sum = 0;
            for (let k = 0; k < m1[0].length; k++) {
                sum += m1[i][k] * m2[k][j];
            }
            result[i][j] = sum;
        }
    }
    return result;
}

/**
 * Function that adds transparent (or whatever the fill color is) to the image canvas
 * @see https://stackoverflow.com/questions/43391205/add-padding-to-images-to-get-them-into-the-same-shape#43391469
 * @example
 * addPadding(cv, canvasRef.current, { left: 500 }, { setPreviewPaneDimensions, showPreview });
 */
const addPadding = (cv, src, { top = 0, bottom = 0, left = 0, right = 0 }, { showPreview, setPreviewPaneDimensions }) => {
    const dst = new cv.Mat();
    // @ts-ignore
    cv.copyMakeBorder(cv.imread(src), dst, top, bottom, left, right, cv.BORDER_CONSTANT);
    setPreviewPaneDimensions({
        width: src.width + left + right,
        height: src.height + top + bottom,
    });
    cv.imshow(src, dst);
    showPreview();
    dst.delete();
};

/**
 * perspective cropping utility (AKA keystone correction)
 * @param cv openCv
 * @param docCanvas
 * @param cropPoints
 * @param imageResizeRatio
 * @param setPreviewPaneDimensions
 */
const cropOpenCV = (cv, dst, cropPoints, imageResizeRatio
// _canvasRef: HTMLCanvasElement,
) => {
    // const dst = cv.imread(docCanvas);
    //   const bR = cropPoints["right-bottom"];
    const bL = cropPoints["left-bottom"];
    const tR = cropPoints["right-top"];
    const tL = cropPoints["left-top"];
    const l = tL.x / imageResizeRatio;
    const t = tL.y / imageResizeRatio;
    const r = tR.x / imageResizeRatio;
    const b = bL.y / imageResizeRatio;
    const M = cv.matFromArray(2, 3, cv.CV_64FC1, [1, 0, -l, 0, 1, -t]);
    //   let dsize = new cv.Size(dst.rows - t - b, dst.cols - l - r);
    //   const dsize = new cv.Size(dst.rows - l - r, dst.cols - t - b);
    const dsize = new cv.Size(dst.cols - l - (dst.cols - r), dst.rows - t - (dst.rows - b));
    cv.warpAffine(dst, dst, M, dsize, cv.INTER_LINEAR, cv.BORDER_CONSTANT, new cv.Scalar());
    // setTimeout(async () => {
    //   cv.imshow(canvasRef, dst);
    // }, 0);
    // dst.delete();
    M.delete();
};

const MAX_GRABCUT_DIM = 800;
// --- Module-level GrabCut state storage ---
let storedMask = null;
let storedBgdModel = null;
let storedFgdModel = null;
let storedScaleFactor = 1;
let storedSmallW = 0;
let storedSmallH = 0;
let lastAppliedStrokes = [];
const clearGrabCutState = () => {
    try {
        storedMask?.delete();
    }
    catch { /* noop */ }
    try {
        storedBgdModel?.delete();
    }
    catch { /* noop */ }
    try {
        storedFgdModel?.delete();
    }
    catch { /* noop */ }
    storedMask = null;
    storedBgdModel = null;
    storedFgdModel = null;
    storedScaleFactor = 1;
    storedSmallW = 0;
    storedSmallH = 0;
    lastAppliedStrokes = [];
};
const hasGrabCutMask = () => storedMask !== null;
const getGrabCutScaleFactor = () => storedScaleFactor;
const setLastAppliedStrokes = (s) => {
    lastAppliedStrokes = s;
};
const getLastAppliedStrokes = () => lastAppliedStrokes;
/**
 * Shared pipeline: convert raw GrabCut mask to binary, apply morphology,
 * blur, upscale, and write to src alpha channel.
 */
const applyMaskToAlpha = (cv, canvas, src, rawMask, origW, origH, scaleFactor) => {
    const mats = [];
    const del = (m) => { mats.push(m); return m; };
    try {
        // Clone so we don't modify the stored mask
        const binMask = del(rawMask.clone());
        const maskData = binMask.data;
        for (let i = 0; i < maskData.length; i++) {
            maskData[i] = (maskData[i] === 1 || maskData[i] === 3) ? 255 : 0;
        }
        // Morphological cleanup if available
        const hasMorphology = typeof cv.getStructuringElement === "function" &&
            typeof cv.morphologyEx === "function";
        if (hasMorphology) {
            const kernel = del(cv.getStructuringElement(cv.MORPH_ELLIPSE, new cv.Size(5, 5)));
            cv.morphologyEx(binMask, binMask, cv.MORPH_CLOSE, kernel);
            cv.morphologyEx(binMask, binMask, cv.MORPH_OPEN, kernel);
        }
        // Edge feathering
        const blurred = del(new cv.Mat());
        cv.GaussianBlur(binMask, blurred, new cv.Size(5, 5), 0, 0, cv.BORDER_DEFAULT);
        // Upscale mask back to original dimensions
        let finalMask;
        if (scaleFactor < 1) {
            finalMask = del(new cv.Mat());
            cv.resize(blurred, finalMask, new cv.Size(origW, origH), 0, 0, cv.INTER_LINEAR);
        }
        else {
            finalMask = blurred;
        }
        // Apply mask as alpha channel
        const srcData = src.data;
        const finalMaskData = finalMask.data;
        const totalPixels = origW * origH;
        for (let i = 0; i < totalPixels; i++) {
            srcData[i * 4 + 3] = finalMaskData[i];
        }
        cv.imshow(canvas, src);
    }
    finally {
        for (const m of mats) {
            try {
                m.delete();
            }
            catch { /* noop */ }
        }
    }
};
/**
 * Remove the background from an image using OpenCV's GrabCut algorithm.
 * Stores the GrabCut mask for subsequent refinement via refineBackground().
 */
const removeBackground = (cv, canvas, src) => {
    if (!cv.grabCut) {
        throw new Error("cv.grabCut is not available in this OpenCV build");
    }
    // Clear any previous stored state
    clearGrabCutState();
    const origW = src.cols;
    const origH = src.rows;
    const maxDim = Math.max(origW, origH);
    const sf = maxDim > MAX_GRABCUT_DIM ? MAX_GRABCUT_DIM / maxDim : 1;
    const sW = Math.round(origW * sf);
    const sH = Math.round(origH * sf);
    const mats = [];
    const del = (m) => { mats.push(m); return m; };
    try {
        // Downscale if needed
        let small;
        if (sf < 1) {
            small = del(new cv.Mat());
            cv.resize(src, small, new cv.Size(sW, sH), 0, 0, cv.INTER_AREA);
        }
        else {
            small = del(src.clone());
        }
        const rgb = del(new cv.Mat());
        cv.cvtColor(small, rgb, cv.COLOR_RGBA2RGB, 0);
        const mask = del(new cv.Mat());
        const bgdModel = del(new cv.Mat());
        const fgdModel = del(new cv.Mat());
        const insetX = Math.max(1, Math.floor(sW * 0.03));
        const insetY = Math.max(1, Math.floor(sH * 0.03));
        const rect = new cv.Rect(insetX, insetY, sW - insetX * 2, sH - insetY * 2);
        // Pass 1: rectangle init
        cv.grabCut(rgb, mask, rect, bgdModel, fgdModel, 5, cv.GC_INIT_WITH_RECT);
        // Pass 2: mask refinement
        cv.grabCut(rgb, mask, rect, bgdModel, fgdModel, 3, cv.GC_INIT_WITH_MASK ?? 1);
        // Store mask & models for future refinement (before binary conversion)
        storedMask = mask.clone();
        storedBgdModel = bgdModel.clone();
        storedFgdModel = fgdModel.clone();
        storedScaleFactor = sf;
        storedSmallW = sW;
        storedSmallH = sH;
        // Apply the mask to the image
        applyMaskToAlpha(cv, canvas, src, mask, origW, origH, sf);
    }
    finally {
        for (const m of mats) {
            try {
                m.delete();
            }
            catch { /* noop */ }
        }
    }
};
/**
 * Refine the background removal using user-supplied brush strokes.
 * Strokes mark pixels as definite foreground or background, then
 * GrabCut re-runs with GC_INIT_WITH_MASK.
 */
const refineBackground = (cv, canvas, src, strokes) => {
    if (!storedMask || !storedBgdModel || !storedFgdModel) {
        throw new Error("No GrabCut mask available. Run removeBackground first.");
    }
    if (!strokes || strokes.length === 0)
        return;
    const origW = src.cols;
    const origH = src.rows;
    const mats = [];
    const del = (m) => { mats.push(m); return m; };
    try {
        // Downscale current image for GrabCut
        let small;
        if (storedScaleFactor < 1) {
            small = del(new cv.Mat());
            cv.resize(src, small, new cv.Size(storedSmallW, storedSmallH), 0, 0, cv.INTER_AREA);
        }
        else {
            small = del(src.clone());
        }
        const rgb = del(new cv.Mat());
        cv.cvtColor(small, rgb, cv.COLOR_RGBA2RGB, 0);
        // Clone the stored mask and paint user strokes onto it
        const refineMask = del(storedMask.clone());
        const maskData = refineMask.data;
        for (const stroke of strokes) {
            const gcValue = stroke.mode === "fg" ? 1 : 0; // GC_FGD=1, GC_BGD=0
            for (const pt of stroke.points) {
                const r = stroke.brushRadius;
                const rSq = r * r;
                const yMin = Math.max(0, pt.y - r);
                const yMax = Math.min(storedSmallH - 1, pt.y + r);
                const xMin = Math.max(0, pt.x - r);
                const xMax = Math.min(storedSmallW - 1, pt.x + r);
                for (let my = yMin; my <= yMax; my++) {
                    const dy = my - pt.y;
                    for (let mx = xMin; mx <= xMax; mx++) {
                        const dx = mx - pt.x;
                        if (dx * dx + dy * dy <= rSq) {
                            maskData[my * storedSmallW + mx] = gcValue;
                        }
                    }
                }
            }
        }
        // Re-run GrabCut with user-updated mask
        const dummyRect = new cv.Rect(0, 0, 1, 1);
        cv.grabCut(rgb, refineMask, dummyRect, storedBgdModel, storedFgdModel, 3, cv.GC_INIT_WITH_MASK ?? 1);
        // Update stored mask with refined result
        storedMask.delete();
        storedMask = refineMask.clone();
        // Apply the refined mask to the image
        applyMaskToAlpha(cv, canvas, src, refineMask, origW, origH, storedScaleFactor);
    }
    finally {
        for (const m of mats) {
            try {
                m.delete();
            }
            catch { /* noop */ }
        }
    }
};

/**
 *
 * @param state RomaineState
 * @param payload HistoryAction["payload"]
 * @todo
 * 1) reduce rotation to one command if previous history is also rotation
 */
const history = (state, payload) => {
    switch (payload.cmd) {
        case "CLEAR": {
            // overwrite with empty array
            return {
                ...state,
                history: { commands: [], pointer: 0 },
            };
        }
        case "PUSH": {
            if (state.mode === "undo" || state.mode === "redo" || state.mode === null)
                return { ...state };
            const pointer = state.history.pointer;
            let newCommands = state.history.commands;
            if (pointer < newCommands.length) {
                newCommands = state.history.commands.reduce((a, c, i) => (pointer > i ? [...a, c] : a), []);
            }
            // Use custom payload from plugin if provided, otherwise derive from state
            const entry = payload.customPayload !== undefined
                ? { cmd: state.mode, payload: payload.customPayload }
                : getHistoryFromState(state);
            return {
                ...state,
                history: {
                    ...state.history,
                    commands: [...newCommands, entry],
                    pointer: pointer + 1,
                },
            };
        }
        case "UNDO": {
            const pointer = state.history.pointer;
            return {
                ...state,
                history: { ...state.history, pointer: pointer ? pointer - 1 : 0 },
            };
        }
        case "REDO": {
            const pointer = state.history.pointer;
            return {
                ...state,
                history: { ...state.history, pointer: pointer ? pointer - 1 : 0 },
            };
        }
        default:
            return { ...state };
    }
};
const getHistoryFromState = ({ mode, angle, scale, cropPoints, }) => {
    switch (mode) {
        case "rotate-left":
            return { cmd: mode, payload: angle % 360 };
        case "rotate-right":
            return { cmd: mode, payload: (360 - angle) % 360 };
        case "flip-horizontal":
            return { cmd: mode, payload: null };
        case "flip-vertical":
            return { cmd: mode, payload: null };
        case "crop":
            return { cmd: mode, payload: cropPoints };
        case "perspective-crop":
            return { cmd: mode, payload: cropPoints };
        case "scale":
            return { cmd: mode, payload: scale };
        case "remove-background":
            return { cmd: mode, payload: null };
        case "refine-background":
            return { cmd: mode, payload: getLastAppliedStrokes() };
        case "full-reset":
            throw new Error('error: action "full-reset" should not call `history`.`PUSH`').stack;
        case null:
            // handles null
            throw new Error("error: action of type null should not call `history`.`PUSH`").stack;
        default:
            // Plugin modes — return generic entry (plugins should use customPayload instead)
            return { cmd: mode, payload: null };
    }
};

const romaineReducer = (state, action) => {
    switch (action.type) {
        case "JOIN_PAYLOAD":
            return { ...state, ...action.payload };
        case "MODE":
            if (state.mode === action.payload)
                return { ...state, mode: null };
            return { ...state, mode: action.payload };
        case "ANGLE":
            return { ...state, angle: action.payload };
        case "SCALE":
            return { ...state, scale: action.payload };
        case "CROP_POINTS":
            if (!action.payload)
                return { ...state };
            return { ...state, cropPoints: action.payload };
        case "HISTORY":
            return { ...history(state, action.payload) };
        case "IMAGE-UPDATE":
            return { ...state, image: action.payload };
        default:
            return { ...state };
    }
};

const initialRomaineState = {
    mode: null,
    angle: 90,
    scale: {
        width: 0,
        height: 0,
    },
    image: {
        width: 0,
        height: 0,
        id: null,
    },
    history: { commands: [], pointer: 0 },
    cropPoints: {
        "left-top": { x: 0, y: 0 },
        "left-bottom": { x: 0, y: 0 },
        "right-bottom": { x: 0, y: 0 },
        "right-top": { x: 0, y: 0 },
    },
};

const moduleConfig = {
    wasmBinaryFile: "opencv_js.wasm",
    usingWasm: true,
};

const OpenCvContext = createContext({
    loaded: false,
    romaine: initialRomaineState,
    setCropPoints: null,
    undo: null,
    redo: null,
});
const { Consumer: OpenCvConsumer, Provider } = OpenCvContext;
const scriptId = "openCvScriptTag";
/**
 * a romaine context for use in getting openCV and the canvas ref element
 * @todo
 * 1) Add ref to provider
 * 2) See if nonce is really required here
 */
const Romaine = ({ openCvPath, children, onLoad, angle = 90, }) => {
    const [loaded, setLoaded] = useState(false);
    const [_image, setImage] = useState(null);
    const canvasApi = useRef(null);
    const handleOnLoad = useCallback(() => {
        onLoad && onLoad(window.cv);
        setLoaded(true);
    }, [onLoad, setLoaded]);
    const generateOpenCvScriptTag = useMemo(() => {
        // make sure we are in the browser
        if (typeof window !== "undefined") {
            if (!document.getElementById(scriptId) && !window.cv) {
                const js = document.createElement("script");
                // append to head
                js.id = scriptId;
                js.nonce = "8IBTHwOdqNKAWeKl7plt8g==";
                js.defer = true;
                js.async = true;
                js.src = openCvPath || "https://docs.opencv.org/3.4.13/opencv.js";
                return js;
            }
            else if (document.getElementById(scriptId) && !window.cv) {
                return document.getElementById(scriptId);
            }
        }
    }, [openCvPath]);
    useEffect(() => {
        if (window.cv) {
            handleOnLoad();
            return;
        }
        // https://docs.opencv.org/3.4/dc/de6/tutorial_js_nodejs.html
        // https://medium.com/code-divoire/integrating-opencv-js-with-an-angular-application-20ae11c7e217
        // https://stackoverflow.com/questions/56671436/cv-mat-is-not-a-constructor-opencv
        moduleConfig.onRuntimeInitialized = handleOnLoad;
        window.Module = moduleConfig;
        // if (!document.getElementById(scriptId))
        if (generateOpenCvScriptTag && !document.getElementById(scriptId))
            document.body.appendChild(generateOpenCvScriptTag);
        // else handleOnLoad();
    }, [openCvPath, handleOnLoad, generateOpenCvScriptTag]);
    const [romaine, dispatchRomaine] = useReducer(romaineReducer, initialRomaineState);
    const setMode = useCallback((mode) => {
        dispatchRomaine({ type: "MODE", payload: mode });
    }, [dispatchRomaine]);
    const setAngle = useCallback((angle) => {
        dispatchRomaine({ type: "ANGLE", payload: angle });
    }, [dispatchRomaine]);
    const setScale = useCallback((scale) => {
        dispatchRomaine({ type: "SCALE", payload: scale });
    }, [dispatchRomaine]);
    const updateImageInformation = useCallback((image) => {
        dispatchRomaine({
            type: "SCALE",
            payload: {
                width: image.width,
                height: image.height,
            },
        });
        dispatchRomaine({ type: "IMAGE-UPDATE", payload: image });
    }, [dispatchRomaine]);
    const { cropPoints, history: { pointer }, } = romaine;
    const setCropPoints = useCallback((payload) => {
        if (typeof payload === "function")
            payload = payload(cropPoints);
        dispatchRomaine({ type: "CROP_POINTS", payload });
    }, [dispatchRomaine, cropPoints]);
    const pushHistory = useCallback((customPayload) => {
        dispatchRomaine({ type: "HISTORY", payload: { cmd: "PUSH", customPayload } });
    }, [dispatchRomaine]);
    const clearHistory = useCallback(() => {
        dispatchRomaine({ type: "HISTORY", payload: { cmd: "CLEAR" } });
    }, [dispatchRomaine]);
    const moveHistory = useCallback((direction) => {
        if (direction)
            return () => {
                dispatchRomaine({ type: "MODE", payload: null });
                dispatchRomaine({ type: "HISTORY", payload: { cmd: "UNDO" } });
            };
        else
            return () => {
                dispatchRomaine({ type: "MODE", payload: null });
                dispatchRomaine({ type: "HISTORY", payload: { cmd: "REDO" } });
            };
    }, [dispatchRomaine]);
    useEffect(() => {
        setAngle(angle);
    }, [angle, setAngle]);
    const memoizedProviderValue = useMemo(() => ({
        loaded,
        cv: typeof window !== "undefined"
            ? window?.cv
            : null,
        romaine: { ...romaine, history: { ...romaine.history }, clearHistory },
        canvasApi,
        setImage,
        setMode,
        setAngle,
        setScale,
        updateImageInformation,
        pushHistory,
        setCropPoints,
        undo: moveHistory(true),
        redo: moveHistory(false),
    }), [
        loaded,
        romaine,
        pointer,
        setMode,
        setAngle,
        setScale,
        pushHistory,
        moveHistory,
        clearHistory,
        setCropPoints,
        updateImageInformation,
    ]);
    // const { canvasRef } = useCanvas({ image });
    // usePreview({ cv: memoizedProviderValue, canvasRef });
    return jsx(Provider, { value: memoizedProviderValue, children: children });
};
Romaine.propTypes = {
    openCvPath: T.string,
    children: T.node,
    onLoad: T.func,
    angle: T.number,
};

const useRomaine = () => useContext(OpenCvContext);

/**
 * Add default styles to the points
 * Makes sure position absolute cannot be overwritten
 * @param cropPointStyles
 */
const buildCropPointStyle = (cropPointStyles = {}) => ({
    backgroundColor: "transparent",
    border: "4px solid #3cabe2",
    zIndex: 1001,
    borderRadius: "100%",
    cursor: "grab",
    ...cropPointStyles,
    position: "absolute",
});
const MAGNIFIER_SIZE = 120;
const ZOOM_LEVEL = 3;
const MAGNIFIER_OFFSET = 30;
const drawMagnifier = (magnifierCanvas, previewCanvas, pointX, pointY) => {
    const ctx = magnifierCanvas.getContext("2d");
    if (!ctx)
        return;
    ctx.clearRect(0, 0, MAGNIFIER_SIZE, MAGNIFIER_SIZE);
    // Circular clip
    ctx.save();
    ctx.beginPath();
    ctx.arc(MAGNIFIER_SIZE / 2, MAGNIFIER_SIZE / 2, MAGNIFIER_SIZE / 2, 0, Math.PI * 2);
    ctx.clip();
    // Draw zoomed region from preview canvas
    const sourceSize = MAGNIFIER_SIZE / ZOOM_LEVEL;
    ctx.drawImage(previewCanvas, pointX - sourceSize / 2, pointY - sourceSize / 2, sourceSize, sourceSize, 0, 0, MAGNIFIER_SIZE, MAGNIFIER_SIZE);
    ctx.restore();
    // Border circle
    ctx.beginPath();
    ctx.arc(MAGNIFIER_SIZE / 2, MAGNIFIER_SIZE / 2, MAGNIFIER_SIZE / 2 - 2, 0, Math.PI * 2);
    ctx.strokeStyle = "#3cabe2";
    ctx.lineWidth = 3;
    ctx.stroke();
    // Crosshair
    ctx.beginPath();
    ctx.moveTo(MAGNIFIER_SIZE / 2 - 8, MAGNIFIER_SIZE / 2);
    ctx.lineTo(MAGNIFIER_SIZE / 2 + 8, MAGNIFIER_SIZE / 2);
    ctx.moveTo(MAGNIFIER_SIZE / 2, MAGNIFIER_SIZE / 2 - 8);
    ctx.lineTo(MAGNIFIER_SIZE / 2, MAGNIFIER_SIZE / 2 + 8);
    ctx.strokeStyle = "rgba(255,255,255,0.8)";
    ctx.lineWidth = 1;
    ctx.stroke();
};
/**
 * @returns A crop point to use during cropping and image rotation
 */
const CropPoint = ({ pointSize, pointArea, defaultPosition, onStop: externalOnStop, onDrag: externalOnDrag, bounds, cropPointStyles = {}, previewCanvasRef, }) => {
    const { romaine: { cropPoints }, } = useRomaine();
    const cropPointStyle = useMemo(() => {
        cropPointStyles.width = pointSize;
        cropPointStyles.height = pointSize;
        return buildCropPointStyle(cropPointStyles);
    }, [cropPointStyles, pointSize]);
    const magnifierRef = useRef(null);
    const [isDragging, setIsDragging] = useState(false);
    const updateMagnifier = useCallback((pointX, pointY) => {
        if (!magnifierRef.current || !previewCanvasRef?.current)
            return;
        drawMagnifier(magnifierRef.current, previewCanvasRef.current, pointX, pointY);
    }, [previewCanvasRef]);
    const onStart = useCallback((_, data) => {
        setIsDragging(true);
        updateMagnifier(data.x + pointSize / 2, data.y + pointSize / 2);
    }, [pointSize, updateMagnifier]);
    const onDrag = useCallback((_, position) => {
        const px = position.x + pointSize / 2;
        const py = position.y + pointSize / 2;
        externalOnDrag({ ...position, x: px, y: py }, pointArea);
        updateMagnifier(px, py);
    }, [externalOnDrag, pointArea, pointSize, updateMagnifier]);
    const onStop = useCallback((_, position) => {
        setIsDragging(false);
        externalOnStop({
            ...position,
            x: position.x + pointSize / 2,
            y: position.y + pointSize / 2,
        }, pointArea, cropPoints);
    }, [externalOnStop, cropPoints, pointArea, pointSize]);
    const nodeRef = useRef(null);
    // Position magnifier above or below with hysteresis to prevent flipping
    const magnifierSide = useRef("above");
    const pointPos = cropPoints[pointArea];
    const threshold = MAGNIFIER_SIZE + MAGNIFIER_OFFSET;
    const buffer = 40;
    if (magnifierSide.current === "above" && pointPos.y < threshold) {
        magnifierSide.current = "below";
    }
    else if (magnifierSide.current === "below" && pointPos.y > threshold + buffer) {
        magnifierSide.current = "above";
    }
    const magX = pointPos.x - MAGNIFIER_SIZE / 2;
    const magY = magnifierSide.current === "above"
        ? pointPos.y - MAGNIFIER_SIZE - MAGNIFIER_OFFSET
        : pointPos.y + pointSize + MAGNIFIER_OFFSET;
    return (jsxs(Fragment, { children: [jsx(Draggable, { nodeRef: nodeRef, bounds: bounds, defaultPosition: defaultPosition, position: {
                    x: pointPos.x - pointSize / 2,
                    y: pointPos.y - pointSize / 2,
                }, onStart: onStart, onDrag: onDrag, onStop: onStop, children: jsx("div", { ref: nodeRef, style: cropPointStyle }) }), jsx("canvas", { ref: magnifierRef, width: MAGNIFIER_SIZE, height: MAGNIFIER_SIZE, style: {
                    position: "absolute",
                    left: magX,
                    top: magY,
                    width: MAGNIFIER_SIZE,
                    height: MAGNIFIER_SIZE,
                    borderRadius: "50%",
                    zIndex: 1002,
                    pointerEvents: "none",
                    boxShadow: "0 2px 10px rgba(0,0,0,0.3)",
                    display: isDragging ? "block" : "none",
                } })] }));
};

const CropPoints = ({ previewDims, ...otherProps }) => {
    return (jsxs(Fragment, { children: [jsx(CropPoint, { pointArea: "left-top", defaultPosition: { x: 0, y: 0 }, ...otherProps }), jsx(CropPoint, { pointArea: "right-top", defaultPosition: { x: previewDims.width, y: 0 }, ...otherProps }), jsx(CropPoint, { pointArea: "right-bottom", defaultPosition: { x: 0, y: previewDims.height }, ...otherProps }), jsx(CropPoint, { pointArea: "left-bottom", defaultPosition: {
                    x: previewDims.width,
                    y: previewDims.height,
                }, ...otherProps })] }));
};
CropPoints.propTypes = {
    previewDims: T.shape({
        ratio: T.number,
        width: T.number,
        height: T.number,
    }),
};

/**
 * Create the lines for the cropper utility
 */
const CropPointsDelimiters = ({ 
// romaineRef,
crop, cropPoints, previewDims, lineWidth = 3, lineColor = "#3cabe2", pointSize, ...props }) => {
    const canvas = useRef(null);
    const clearCanvas = useCallback(() => {
        if (canvas.current) {
            const ctx = canvas.current.getContext("2d");
            ctx && ctx.clearRect(0, 0, previewDims.width, previewDims.height);
        }
    }, [previewDims]);
    /** This needs to do a better job. See Issue #2
     * @link https://github.com/DanielBailey-web/romaine/issues/2
     *
     */
    const sortPoints = useCallback(() => {
        const sortOrder = [
            "left-top",
            "right-top",
            "right-bottom",
            "left-bottom",
        ];
        return sortOrder.reduce((acc, pointPos) => [...acc, cropPoints[pointPos]], []);
    }, [cropPoints]);
    const drawShape = useCallback(([point1, point2, point3, point4]) => {
        const ctx = canvas.current && canvas.current.getContext("2d");
        if (ctx) {
            ctx.lineWidth = lineWidth;
            ctx.strokeStyle = lineColor;
            ctx.beginPath();
            ctx.moveTo(point1.x + pointSize / 2, point1.y);
            ctx.lineTo(point2.x - pointSize / 2, point2.y);
            ctx.moveTo(point2.x, point2.y + pointSize / 2);
            ctx.lineTo(point3.x, point3.y - pointSize / 2);
            ctx.moveTo(point3.x - pointSize / 2, point3.y);
            ctx.lineTo(point4.x + pointSize / 2, point4.y);
            ctx.moveTo(point4.x, point4.y - pointSize / 2);
            ctx.lineTo(point1.x, point1.y + pointSize / 2);
            ctx.closePath();
            ctx.stroke();
        }
    }, [lineColor, lineWidth, pointSize]);
    useEffect(() => {
        if (cropPoints && canvas.current) {
            clearCanvas();
            const sortedPoints = sortPoints();
            drawShape(sortedPoints);
        }
    }, [cropPoints, drawShape, clearCanvas, sortPoints]);
    /**
     * Takes in a `CoordinateXY` and makes sure that it is inside the `ContourCoordinates`
     *
     * @returns boolean
     *
     * @todo Should use point slope formula for when using perspective cropper
     */
    const xyInPoints = ({ x, y }) => {
        if (x > cropPoints["left-top"].x &&
            y > cropPoints["left-top"].y &&
            x < cropPoints["right-top"].x &&
            y > cropPoints["right-top"].y &&
            x < cropPoints["right-bottom"].x &&
            y < cropPoints["right-bottom"].y &&
            x > cropPoints["left-bottom"].x &&
            y < cropPoints["left-bottom"].y)
            return true;
        return false;
    };
    const getMousePosition = (event) => {
        if (canvas?.current?.getBoundingClientRect) {
            const x = event.clientX - canvas.current.getBoundingClientRect().left;
            const y = event.clientY - canvas.current.getBoundingClientRect().top;
            if (xyInPoints({ x, y }))
                return "inside";
            else
                return "outside";
        }
    };
    const [cursor, setCursor] = useState("default");
    const handleMouseMove = (e) => {
        const pos = getMousePosition(e);
        if (pos === "inside" && cursor !== "crosshair")
            setCursor("crosshair");
        else if (pos === "outside" && cursor !== "default")
            setCursor("default");
    };
    const handleClick = (e) => {
        const pos = getMousePosition(e);
        if (pos === "inside")
            crop({
                preview: true,
                filterCvParams: {
                    grayScale: false,
                    th: false,
                },
                image: {
                    quality: 1,
                    type: "image/jpeg",
                },
            });
    };
    return (jsxs(Fragment, { children: [jsx("canvas", { id: `${props.saltId ? props.saltId + "-" : ""}crop-point-delimiters`, ref: canvas, style: {
                    position: "absolute",
                    top: 0,
                    right: 0,
                    left: 0,
                    bottom: 0,
                    zIndex: 5,
                    pointerEvents: "none",
                }, width: previewDims.width, height: previewDims.height }), jsx("div", { style: {
                    position: "absolute",
                    top: 0,
                    right: 0,
                    left: 0,
                    bottom: 0,
                    zIndex: 4,
                    cursor,
                }, onClick: handleClick, onMouseMove: handleMouseMove, onMouseLeave: () => setCursor("default") })] }));
};
CropPointsDelimiters.propTypes = {
    previewDims: T.shape({
        ratio: T.number,
        width: T.number,
        height: T.number,
    }),
    cropPoints: T.shape({
        "left-top": T.shape({ x: T.number, y: T.number }).isRequired,
        "right-top": T.shape({ x: T.number, y: T.number }).isRequired,
        "right-bottom": T.shape({ x: T.number, y: T.number }).isRequired,
        "left-bottom": T.shape({ x: T.number, y: T.number }).isRequired,
    }),
    lineColor: T.string,
    lineWidth: T.number,
    pointSize: T.number,
    saltId: T.string,
};

const CroppingCanvas = ({ image, onDragStop, onChange, previewCanvasRef, setCropFunc, pointSize = 30, lineWidth, lineColor, previewDims, imageResizeRatio, showPreview, setPreviewPaneDimensions, saltId, canvasPtr, }) => {
    const { loaded: cvLoaded, cv, romaine: { mode, cropPoints }, setMode, pushHistory, setCropPoints, } = useRomaine();
    // const [_worker, setWorker] = useState<Worker | null>();
    // useEffect(() => {
    //   if (window.Worker) {
    //     const worker = new Worker("./test.js");
    //     worker.onmessage = function (e) {
    //       console.log(e.data);
    //       console.log("Message received from worker");
    //     };
    //     worker.postMessage([20, 30]);
    //     setWorker(worker);
    //   }
    // }, []);
    const [loading, setLoading] = useState(false);
    const cropCB = useCallback(async (opts = {}) => {
        // need to figure out how to not need this and still render
        setLoading(true);
        // push can be moved to mode
        pushHistory?.();
        return new Promise((resolve) => {
            if (!opts.cropPoints)
                opts.cropPoints = cropPoints;
            if (!opts.imageResizeRatio)
                opts.imageResizeRatio = imageResizeRatio;
            if (!opts.mode)
                opts.mode = mode;
            if (opts.cropPoints) {
                if (opts.mode === "crop")
                    cropOpenCV(cv, canvasPtr.current, opts.cropPoints, opts.imageResizeRatio
                    // canvasRef.current,
                    );
                else if (opts.mode === "perspective-crop")
                    warpPerspective(cv, canvasPtr.current, opts.cropPoints, opts.imageResizeRatio);
                else
                    return setLoading(false);
                // @todo implement this somewhere
                // applyFilter(cv, canvasRef.current, opts.filterCvParams);
                if (opts.preview) {
                    const dims = canvasPtr.current.size();
                    const irr = setPreviewPaneDimensions(dims);
                    showPreview(irr, canvasPtr.current, false);
                    setMode?.(null);
                }
                setLoading(false);
                return resolve();
            }
        });
    }, 
    // disabling because eslint doesn't know that canvasRef is a ref
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [
        cv,
        cropPoints,
        mode,
        pushHistory,
        setLoading,
        setMode,
        setPreviewPaneDimensions,
        imageResizeRatio,
    ]);
    useEffect(() => {
        setCropFunc(() => cropCB);
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [cropCB]);
    const detectContours = () => {
        /*
         * warning, this function will alter the canvasPtr
         *
         * Remember to copy pointer to new variable if you want to keep it
         * Make sure to clean up variable also
         */
        const dst = new cv.Mat();
        canvasPtr.current.copyTo(dst);
        const ksize = new cv.Size(5, 5);
        // convert the image to grayscale, blur it, and find edges in the image
        cv.cvtColor(dst, dst, cv.COLOR_RGBA2GRAY, 0);
        cv.GaussianBlur(dst, dst, ksize, 0, 0, cv.BORDER_DEFAULT);
        cv.Canny(dst, dst, 75, 200);
        // find contours
        cv.threshold(dst, dst, 120, 200, cv.THRESH_BINARY);
        const contours = new cv.MatVector();
        const hierarchy = new cv.Mat();
        cv.findContours(dst, contours, hierarchy, cv.RETR_CCOMP, cv.CHAIN_APPROX_SIMPLE);
        const rect = cv.boundingRect(dst);
        dst.delete();
        hierarchy.delete();
        contours.delete();
        // transform the rectangle into a set of points
        Object.keys(rect).forEach((key) => {
            rect[key] = rect[key] * imageResizeRatio;
        });
        const contourCoordinates = {
            "left-top": { x: rect.x, y: rect.y },
            "right-top": { x: rect.x + rect.width, y: rect.y },
            "right-bottom": {
                x: rect.x + rect.width,
                y: rect.y + rect.height,
            },
            "left-bottom": { x: rect.x, y: rect.y + rect.height },
        };
        setCropPoints(contourCoordinates);
    };
    useEffect(() => {
        if (onChange && cropPoints) {
            onChange({ ...cropPoints, loading });
        }
    }, [cropPoints, loading]);
    /**
     * @todo
     * 1) Need to make sure that this is valid to never run the other code
     *   if it is, then need to remove the code in the else block
     */
    const bootstrap = async () => {
        detectContours();
        setLoading(false);
        // else {
        //   // imread is SLOW
        //   const src = cv.imread(previewCanvasRef.current);
        //   const contourCoordinates = {
        //     "left-top": { x: 0, y: 0 },
        //     "right-top": { x: src.cols, y: 0 },
        //     "right-bottom": {
        //       x: src.cols,
        //       y: src.rows,
        //     },
        //     "left-bottom": { x: 0, y: src.rows },
        //   };
        //   setCropPoints(contourCoordinates);
        // }
    };
    useEffect(() => {
        if (image &&
            previewCanvasRef.current &&
            cvLoaded &&
            (mode === "crop" || mode === "perspective-crop")) {
            bootstrap();
        }
        else {
            setLoading(true);
        }
    }, [image, cvLoaded, mode]);
    const handleNormalCornerMove = useCallback((position, area, cPs) => {
        const { x, y } = position;
        if (cPs) {
            switch (area) {
                case "left-bottom":
                    return {
                        ...cPs,
                        [area]: { x, y },
                        "left-top": { x, y: cPs["left-top"].y },
                        "right-bottom": { x: cPs["right-bottom"].x, y },
                    };
                case "right-bottom":
                    return {
                        ...cPs,
                        [area]: { x, y },
                        "right-top": { x, y: cPs["right-top"].y },
                        "left-bottom": { x: cPs["left-bottom"].x, y },
                    };
                case "right-top":
                    return {
                        ...cPs,
                        [area]: { x, y },
                        "left-top": { x: cPs["left-top"].x, y },
                        "right-bottom": { x, y: cPs["right-bottom"].y },
                    };
                case "left-top":
                    return {
                        ...cPs,
                        [area]: { x, y },
                        "left-bottom": { x, y: cPs["left-bottom"].y },
                        "right-top": { x: cPs["right-top"].x, y },
                    };
            }
        }
    }, []);
    const onCornerDrag = useCallback((position, area) => {
        // if (magnifierCanvasRef.current && previewCanvasRef.current) {
        // const { x, y } = position;
        // const magnCtx = magnifierCanvasRef.current.getContext("2d");
        // clearCanvasByRef(magnifierCanvasRef);
        // TODO we should make those 5, 10 and 20 values proportionate
        // to the point size
        // magnCtx &&
        //   magnCtx.drawImage(
        //     previewCanvasRef.current,
        //     x - (pointSize - 10),
        //     y - (pointSize - 10),
        //     pointSize + 5,
        //     pointSize + 5,
        //     x + 10,
        //     y - 90,
        //     pointSize + 20,
        //     pointSize + 20
        //   );
        setCropPoints((cPs) => {
            if (cPs && mode === "perspective-crop")
                return { ...cPs, [area]: position };
            return handleNormalCornerMove(position, area, cPs);
        });
        // }
    }, [mode, setCropPoints, handleNormalCornerMove]);
    const onCornerStop = useCallback((position, area, cropPoints) => {
        const { x, y } = position;
        // clearCanvasByRef(magnifierCanvasRef);
        setCropPoints((cPs) => {
            if (cPs && mode === "perspective-crop")
                return { ...cPs, [area]: { x, y } };
            return handleNormalCornerMove(position, area, cPs);
        });
        onDragStop?.({ ...cropPoints, [area]: { x, y }, loading });
    }, [mode, setCropPoints, handleNormalCornerMove]);
    return (jsx(Fragment, { children: previewDims &&
            (mode === "crop" || mode === "perspective-crop") &&
            cropPoints && (jsxs(Fragment, { children: [jsx(CropPoints, { pointSize: pointSize, previewDims: previewDims, onDrag: onCornerDrag, onStop: onCornerStop, previewCanvasRef: previewCanvasRef, bounds: {
                        left: (previewCanvasRef?.current?.offsetLeft || 0) - pointSize / 2,
                        top: (previewCanvasRef?.current?.offsetTop || 0) - pointSize / 2,
                        right: (previewCanvasRef?.current?.offsetLeft || 0) -
                            pointSize / 2 +
                            (previewCanvasRef?.current?.offsetWidth || 0),
                        bottom: (previewCanvasRef?.current?.offsetTop || 0) -
                            pointSize / 2 +
                            (previewCanvasRef?.current?.offsetHeight || 0),
                    } }), jsx(CropPointsDelimiters
                // romaineRef={romaineRef as React.RefObject<RomaineRef>}
                , { 
                    // romaineRef={romaineRef as React.RefObject<RomaineRef>}
                    crop: cropCB, previewDims: previewDims, cropPoints: cropPoints, lineWidth: lineWidth, lineColor: lineColor, pointSize: pointSize, saltId: saltId })] })) }));
};

const buildImgContainerStyle = (previewDims) => ({
    width: previewDims.width,
    height: previewDims.height,
});

const usePreview = ({ canvasRef, maxDims, originalImageDims, }) => {
    const { maxHeight, maxWidth } = maxDims;
    const { cv } = useRomaine();
    const previewRef = useRef(null);
    const [imageResizeRatio, setImageResizeRatio] = useState(1);
    const [previewDims, setPreviewDims] = useState({
        height: maxHeight,
        width: maxWidth,
        ratio: 1,
    });
    const createPreview = (resizeRatio = imageResizeRatio, source, cleanup = true) => {
        if (!source && canvasRef.current)
            source == cv.imread(canvasRef.current);
        if (!source?.$$.ptr)
            return;
        if (cv && previewRef.current) {
            const dst = new cv.Mat();
            const dsize = new cv.Size(0, 0);
            cv.resize(source, dst, dsize, resizeRatio, resizeRatio, cv.INTER_AREA);
            cv.imshow(previewRef.current, dst);
            if (cleanup)
                source.delete();
            dst.delete();
        }
    };
    const setPreviewPaneDimensions = useCallback((dims = originalImageDims) => {
        if (dims && previewRef?.current) {
            const newPreviewDims = calcDims(dims.width, dims.height, maxWidth, maxHeight);
            setPreviewDims(newPreviewDims);
            previewRef.current.width = newPreviewDims.width;
            previewRef.current.height = newPreviewDims.height;
            const irr = newPreviewDims.width / dims.width;
            setImageResizeRatio(irr);
            return irr;
        }
    }, [maxHeight, maxWidth]);
    return {
        previewRef,
        createPreview,
        imageResizeRatio,
        setPreviewPaneDimensions,
        previewDims,
    };
};

const useCanvas = ({ image, saltId }) => {
    const { cv, romaine: { mode }, updateImageInformation, } = useRomaine();
    const [loaded, setLoaded] = useState(false);
    const [originalImageDims, setOriginalImageDims] = useState({
        width: 0,
        height: 0,
    });
    const canvasRef = useRef(document.createElement("canvas"));
    const canvasPtr = useRef(undefined);
    const getFile = useCallback(async (image) => await readFile(image), []);
    const createCanvas = useCallback(() => new Promise(async (resolve, reject) => {
        if (!image)
            return reject("Image source is invalid");
        const src = await getFile(image);
        try {
            const img = document.createElement("img");
            img.style.display = "none";
            img.id = `${saltId ? saltId + "-" : ""}working-image`;
            img.onload = async () => {
                // set edited image canvas and dimensions
                canvasRef.current = document.createElement("canvas");
                canvasRef.current.style.display = "none";
                canvasRef.current.id = `${saltId ? saltId + "-" : ""}working-canvas`;
                canvasRef.current.width = img.width;
                canvasRef.current.height = img.height;
                setOriginalImageDims({ height: img.height, width: img.width });
                updateImageInformation?.({
                    height: img.height,
                    width: img.width,
                    id: img.id,
                });
                const ctx = canvasRef.current.getContext("2d");
                if (ctx) {
                    ctx.fillStyle = "#fff0"; // transparent
                    ctx.fillRect(0, 0, img.width, img.height);
                    ctx.drawImage(img, 0, 0);
                    canvasPtr.current = cv.imread(canvasRef.current);
                    setLoaded(true);
                    return resolve();
                }
                return reject();
            };
            if (isCrossOriginURL(src))
                img.crossOrigin = "anonymous";
            img.src = src;
        }
        catch (err) {
            reject("unknown error while creating canvas");
        }
    }), [image]);
    const resetImage = useCallback(() => {
        // set loaded to false before we destroy the pointer to avoid race conditions
        setLoaded(false);
        canvasPtr.current?.delete();
        createCanvas();
    }, [image]);
    useEffect(() => {
        if (image)
            resetImage();
    }, [image]);
    useEffect(() => {
        if (mode === "undo") {
            resetImage();
        }
    }, [mode]);
    return {
        canvasRef,
        createCanvas,
        originalImageDims,
        canvasPtr,
        resetImage,
        /** loaded: Is the image loaded onto the canvas and does the pointer exist */
        loaded,
    };
};

const flip = async (cv, canvas, src, orientation) => {
    if (orientation === "horizontal") {
        cv.flip(src, src, 0);
    }
    else if (orientation === "vertical") {
        cv.flip(src, src, 1);
    }
    else {
        throw new Error("Invalid orientation");
    }
    cv.imshow(canvas, src);
};

const rotate = async (cv, canvas, mode, setPreviewPaneDimensions, showPreview, opts, cvCanvas) => {
    const { preview, angle, cleanup } = {
        preview: false,
        cleanup: true,
        ...opts,
    };
    let src = cvCanvas;
    if (!canvas)
        return;
    if (!cvCanvas)
        src = cv.imread(canvas);
    // const dst = new cv.Mat();
    const center = new cv.Point(src.cols / 2, src.rows / 2);
    const M1_temp = cv.getRotationMatrix2D(center, angle, 1);
    const a = [...M1_temp.data64F];
    M1_temp.delete();
    const cos = Math.abs(a[0]);
    const sin = Math.abs(a[3]);
    // compute the new bounding dimensions of the image
    const newWidth = ~~(src.rows * sin + src.cols * cos);
    const newHeight = ~~(src.rows * cos + src.cols * sin);
    /**
     * Col 3 Row 1 is horizontal transform (numerical position away from y axis)
     *
     * Col 3 Row 2 is vertical transform ("" y axis)
     *
     * @description
     * This code is a modified version of rotate_bound found in python package imutils
     * @link
     * https://github.com/jrosebr1/imutils/blob/c12f15391fcc945d0d644b85194b8c044a392e0a/imutils/convenience.py#L41
     */
    const M1 = [
        [a[0], a[1], a[2] + newWidth / 2 - center.x],
        [a[3], a[4], a[5] + newHeight / 2 - center.y],
    ];
    const oneDimensionalArray = [].concat.apply([], M1);
    const M = cv.matFromArray(2, 3, cv.CV_64FC1, oneDimensionalArray);
    const height = newHeight, width = newWidth;
    canvas.height = height;
    canvas.width = width;
    const irr = setPreviewPaneDimensions({
        height,
        width,
    });
    // this is the slowest step
    cv.warpAffine(src, src, M, { height, width }, cv.INTER_LINEAR, cv.BORDER_CONSTANT, new cv.Scalar());
    // if (mode !== "undo" && mode !== "redo") {
    //   showPreview(irr, dst, false);
    //   setMode?.(null);
    // }
    // setTimeout(() => {
    //   cv.imshow(canvas, dst);
    //   dst.delete();
    // }, 0);
    if (mode !== "undo" && mode !== "redo")
        showPreview(irr, src, false);
    setTimeout(() => {
        // show the real preview first so it works faster for user
        // due to this we must cleanup dst ourselves
        // imshow is being called in showPreview, so for preview this would be redundant
        if (!preview)
            cv.imshow(canvas, src);
        if (cleanup)
            src.delete();
        // finished, set the mode back to null
        M.delete();
    }, 0);
};

const scale = async (cv, canvas, src, dims) => {
    const dsize = new cv.Size(dims.width, dims.height);
    cv.resize(src, src, dsize, 0, 0, cv.INTER_AREA);
    cv.imshow(canvas, src);
};

const handleModeChange = async ({ romaine: { cv, setMode, romaine: { mode, clearHistory, angle, history, scale: newScale }, pushHistory, undo, }, _canvas: { canvasRef, canvasPtr, resetImage }, _preview: { previewRef, createPreview, setPreviewPaneDimensions }, }) => {
    switch (mode) {
        case "full-reset": {
            clearGrabCutState();
            clearHistory();
            resetImage();
            createPreview();
            setMode?.(null);
            break;
        }
        case "undo": {
            let waitingOnPointer = true;
            if (history.pointer === 1) {
                undo();
                setMode?.("preview");
                return;
            }
            // if history.pointer -1 is 0, we're at the beginning of the history
            if (history.pointer - 1 === 0) {
                resetImage();
                setMode?.(null);
                return;
            }
            for (let i = 0; i < history.pointer - 1; i++) {
                if (!canvasPtr.current?.$$.ptr) {
                    continue;
                }
                waitingOnPointer = false;
                switch (history.commands[i].cmd) {
                    case "rotate-left":
                        rotate(cv, canvasRef.current, mode, setPreviewPaneDimensions, createPreview, {
                            angle: history.commands[i].payload,
                            preview: false,
                            cleanup: false,
                        }, canvasPtr.current);
                        break;
                    case "rotate-right": {
                        rotate(cv, canvasRef.current, mode, setPreviewPaneDimensions, createPreview, {
                            angle: history.commands[i].payload,
                            preview: false,
                            cleanup: false,
                        }, canvasPtr.current);
                        break;
                    }
                    case "crop": {
                        cropOpenCV(cv, canvasPtr.current, history.commands[i].payload, setPreviewPaneDimensions({
                            width: canvasPtr.current.cols,
                            height: canvasPtr.current.rows,
                        }));
                        break;
                    }
                    case "perspective-crop":
                        warpPerspective(cv, canvasPtr.current, history.commands[i].payload, setPreviewPaneDimensions({
                            width: canvasPtr.current.cols,
                            height: canvasPtr.current.rows,
                        }));
                        break;
                    case "flip-horizontal":
                        flip(cv, canvasRef.current, canvasPtr.current, "horizontal");
                        break;
                    case "flip-vertical":
                        flip(cv, canvasRef.current, canvasPtr.current, "vertical");
                        break;
                    case "scale":
                        scale(cv, canvasRef.current, canvasPtr.current, newScale);
                        break;
                    case "remove-background":
                        removeBackground(cv, canvasRef.current, canvasPtr.current);
                        break;
                    case "refine-background":
                        if (history.commands[i].payload) {
                            refineBackground(cv, canvasRef.current, canvasPtr.current, history.commands[i].payload);
                        }
                        break;
                    default: {
                        // Plugin modes: if payload is a Blob, apply it to canvasPtr
                        const cmdPayload = history.commands[i].payload;
                        if (cmdPayload instanceof Blob) {
                            const url = URL.createObjectURL(cmdPayload);
                            const img = new Image();
                            img.src = url;
                            await new Promise((resolve) => {
                                img.onload = () => {
                                    const tmp = document.createElement("canvas");
                                    tmp.width = img.width;
                                    tmp.height = img.height;
                                    const ctx = tmp.getContext("2d");
                                    ctx.drawImage(img, 0, 0);
                                    const newMat = cv.imread(tmp);
                                    if (canvasPtr.current)
                                        canvasPtr.current.delete();
                                    canvasPtr.current = newMat;
                                    URL.revokeObjectURL(url);
                                    resolve();
                                };
                            });
                        }
                        break;
                    }
                }
            }
            if (!waitingOnPointer) {
                undo();
                setMode?.("preview");
            }
            return;
        }
        case "crop": {
            // cropping modes are handled in CroppingCanvas.tsx
            break;
        }
        case "perspective-crop": {
            // cropping modes are handled in CroppingCanvas.tsx
            break;
        }
        case "flip-horizontal": {
            if (canvasPtr.current) {
                flip(cv, canvasRef.current, canvasPtr.current, "horizontal");
                pushHistory?.();
            }
            setMode?.("preview");
            break;
        }
        case "flip-vertical": {
            if (canvasPtr.current) {
                flip(cv, canvasRef.current, canvasPtr.current, "vertical");
                pushHistory?.();
            }
            setMode?.("preview");
            break;
        }
        case "scale": {
            pushHistory?.();
            if (canvasPtr.current)
                scale(cv, canvasRef.current, canvasPtr.current, newScale);
            setMode?.("preview");
            break;
        }
        case "remove-background": {
            if (canvasPtr.current) {
                pushHistory?.();
                removeBackground(cv, canvasRef.current, canvasPtr.current);
            }
            setMode?.("preview");
            break;
        }
        case "refine-background": {
            // Persistent mode — handled by BrushCanvas component
            break;
        }
        case "rotate-left": {
            pushHistory?.();
            rotate(cv, previewRef.current, mode, setPreviewPaneDimensions, createPreview, {
                angle,
                preview: true,
            });
            setTimeout(() => {
                rotate(cv, canvasRef.current, mode, setPreviewPaneDimensions, createPreview, {
                    angle,
                    preview: false,
                    cleanup: false,
                }, canvasPtr.current).then(() => setMode?.(null));
            }, 0);
            break;
        }
        case "rotate-right": {
            pushHistory?.();
            if (previewRef.current)
                rotate(cv, previewRef.current, mode, setPreviewPaneDimensions, createPreview, {
                    angle: 360 - angle,
                    preview: true,
                });
            setTimeout(() => {
                rotate(cv, canvasRef.current, mode, setPreviewPaneDimensions, createPreview, {
                    angle: 360 - angle,
                    preview: false,
                    cleanup: false,
                }, canvasPtr.current).then(() => setMode?.(null));
            }, 0);
            break;
        }
        case "preview": {
            if (!canvasPtr.current?.$$.ptr) {
                break;
            }
            createPreview(setPreviewPaneDimensions({
                width: canvasPtr.current?.cols,
                height: canvasPtr.current?.rows,
            }), canvasPtr.current, false);
            setMode?.(null);
            break;
        }
    }
};

const DEFAULT_BRUSH_SIZE = 20;
const MIN_BRUSH_SIZE = 5;
const MAX_BRUSH_SIZE = 100;
const BrushCanvas = ({ canvasRef, canvasPtr, previewDims, imageResizeRatio, showPreview, setPreviewPaneDimensions, }) => {
    const { cv, setMode, pushHistory } = useRomaine();
    const overlayRef = useRef(null);
    const ghostRef = useRef(null);
    const [brushSize, setBrushSize] = useState(DEFAULT_BRUSH_SIZE);
    const [brushMode, setBrushMode] = useState("fg");
    const [isDrawing, setIsDrawing] = useState(false);
    const strokesRef = useRef([]);
    const currentStrokeRef = useRef(null);
    const mousePos = useRef(null);
    // Draggable toolbar state
    const [toolbarPos, setToolbarPos] = useState(null);
    const toolbarDrag = useRef(null);
    const toolbarRef = useRef(null);
    // Guard: exit if no mask is available
    useEffect(() => {
        if (!hasGrabCutMask()) {
            console.warn("No background removal mask. Run Remove Background first.");
            setMode?.(null);
        }
    }, [setMode]);
    // Ghost layer: show the full image (including removed areas) at low opacity
    useEffect(() => {
        const ghost = ghostRef.current;
        const mat = canvasPtr.current;
        if (!ghost || !mat || !previewDims)
            return;
        const fullW = mat.cols;
        const fullH = mat.rows;
        // Create offscreen canvas at full resolution with all alpha = 255
        const offscreen = document.createElement("canvas");
        offscreen.width = fullW;
        offscreen.height = fullH;
        const offCtx = offscreen.getContext("2d");
        if (!offCtx)
            return;
        const data = mat.data;
        const imgData = offCtx.createImageData(fullW, fullH);
        const pixels = imgData.data;
        for (let i = 0; i < fullW * fullH; i++) {
            pixels[i * 4] = data[i * 4];
            pixels[i * 4 + 1] = data[i * 4 + 1];
            pixels[i * 4 + 2] = data[i * 4 + 2];
            pixels[i * 4 + 3] = 255; // force full alpha
        }
        offCtx.putImageData(imgData, 0, 0);
        // Draw scaled to the ghost canvas at preview dimensions
        ghost.width = previewDims.width;
        ghost.height = previewDims.height;
        const gCtx = ghost.getContext("2d");
        if (!gCtx)
            return;
        gCtx.drawImage(offscreen, 0, 0, previewDims.width, previewDims.height);
    }, [canvasPtr, previewDims]);
    const width = previewDims?.width ?? 0;
    const height = previewDims?.height ?? 0;
    // Redraw all strokes + cursor on overlay canvas
    const redraw = useCallback(() => {
        const canvas = overlayRef.current;
        if (!canvas)
            return;
        const ctx = canvas.getContext("2d");
        if (!ctx)
            return;
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        // Draw completed strokes
        const allStrokes = [...strokesRef.current];
        if (currentStrokeRef.current)
            allStrokes.push(currentStrokeRef.current);
        for (const stroke of allStrokes) {
            ctx.fillStyle =
                stroke.mode === "fg" ? "rgba(0, 200, 0, 0.35)" : "rgba(200, 0, 0, 0.35)";
            const r = stroke.brushSize / 2;
            for (const pt of stroke.points) {
                ctx.beginPath();
                ctx.arc(pt.x, pt.y, r, 0, Math.PI * 2);
                ctx.fill();
            }
        }
        // Draw cursor
        if (mousePos.current) {
            ctx.beginPath();
            ctx.arc(mousePos.current.x, mousePos.current.y, brushSize / 2, 0, Math.PI * 2);
            ctx.strokeStyle = brushMode === "fg" ? "#0f0" : "#f00";
            ctx.lineWidth = 2;
            ctx.stroke();
        }
    }, [brushSize, brushMode]);
    // Keyboard shortcuts
    useEffect(() => {
        const handler = (e) => {
            if (e.key === "x" || e.key === "X") {
                setBrushMode((prev) => (prev === "fg" ? "bg" : "fg"));
            }
            if (e.key === "[") {
                setBrushSize((prev) => Math.max(MIN_BRUSH_SIZE, prev - 5));
            }
            if (e.key === "]") {
                setBrushSize((prev) => Math.min(MAX_BRUSH_SIZE, prev + 5));
            }
            if (e.key === "Escape") {
                setMode?.(null);
            }
        };
        window.addEventListener("keydown", handler);
        return () => window.removeEventListener("keydown", handler);
    }, [setMode]);
    const getPos = (e) => {
        const rect = e.currentTarget.getBoundingClientRect();
        return { x: e.clientX - rect.left, y: e.clientY - rect.top };
    };
    const handlePointerDown = useCallback((e) => {
        e.currentTarget.setPointerCapture(e.pointerId);
        const pos = getPos(e);
        setIsDrawing(true);
        currentStrokeRef.current = {
            points: [pos],
            mode: brushMode,
            brushSize,
        };
        mousePos.current = pos;
        redraw();
    }, [brushMode, brushSize, redraw]);
    const handlePointerMove = useCallback((e) => {
        const pos = getPos(e);
        mousePos.current = pos;
        if (isDrawing && currentStrokeRef.current) {
            currentStrokeRef.current.points.push(pos);
        }
        redraw();
    }, [isDrawing, redraw]);
    const handlePointerUp = useCallback(() => {
        setIsDrawing(false);
        if (currentStrokeRef.current) {
            strokesRef.current.push(currentStrokeRef.current);
            currentStrokeRef.current = null;
        }
        redraw();
    }, [redraw]);
    const handleWheel = useCallback((e) => {
        e.preventDefault();
        const delta = e.deltaY > 0 ? -3 : 3;
        setBrushSize((prev) => Math.min(MAX_BRUSH_SIZE, Math.max(MIN_BRUSH_SIZE, prev + delta)));
    }, []);
    const handleApply = useCallback(() => {
        if (strokesRef.current.length === 0)
            return;
        if (!canvasPtr.current || !canvasRef.current)
            return;
        const sf = getGrabCutScaleFactor();
        // Convert strokes from preview coords to mask coords
        const maskStrokes = strokesRef.current.map((s) => ({
            points: s.points.map((p) => ({
                x: Math.round((p.x / imageResizeRatio) * sf),
                y: Math.round((p.y / imageResizeRatio) * sf),
            })),
            mode: s.mode,
            brushRadius: Math.max(1, Math.round((s.brushSize / 2 / imageResizeRatio) * sf)),
        }));
        // Store for history, push history, then apply
        setLastAppliedStrokes(maskStrokes);
        pushHistory?.();
        refineBackground(cv, canvasRef.current, canvasPtr.current, maskStrokes);
        // Update preview
        const dims = {
            width: canvasPtr.current.cols,
            height: canvasPtr.current.rows,
        };
        const irr = setPreviewPaneDimensions(dims);
        showPreview(irr, canvasPtr.current, false);
        // Clear strokes and exit
        strokesRef.current = [];
        setMode?.("preview");
    }, [
        canvasPtr,
        canvasRef,
        cv,
        imageResizeRatio,
        pushHistory,
        setMode,
        setPreviewPaneDimensions,
        showPreview,
    ]);
    const handleCancel = useCallback(() => {
        strokesRef.current = [];
        setMode?.(null);
    }, [setMode]);
    return (jsxs(Fragment, { children: [jsx("canvas", { ref: ghostRef, style: {
                    position: "absolute",
                    top: 0,
                    left: 0,
                    right: 0,
                    bottom: 0,
                    zIndex: 4,
                    pointerEvents: "none",
                    opacity: 0.3,
                } }), jsx("canvas", { ref: overlayRef, width: width, height: height, style: {
                    position: "absolute",
                    top: 0,
                    left: 0,
                    right: 0,
                    bottom: 0,
                    zIndex: 10,
                    pointerEvents: "none",
                } }), jsx("div", { style: {
                    position: "absolute",
                    top: 0,
                    left: 0,
                    right: 0,
                    bottom: 0,
                    zIndex: 9,
                    cursor: "crosshair",
                }, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onPointerLeave: () => {
                    mousePos.current = null;
                    redraw();
                }, onWheel: handleWheel }), jsxs("div", { ref: toolbarRef, style: {
                    position: "absolute",
                    ...(toolbarPos
                        ? { left: toolbarPos.x, top: toolbarPos.y }
                        : { bottom: 10, left: "50%", transform: "translateX(-50%)" }),
                    zIndex: 12,
                    display: "flex",
                    gap: 8,
                    alignItems: "center",
                    background: "rgba(0,0,0,0.8)",
                    borderRadius: 8,
                    padding: "6px 12px",
                    color: "#fff",
                    fontSize: 13,
                    userSelect: "none",
                    whiteSpace: "nowrap",
                    cursor: "grab",
                }, onPointerDown: (e) => {
                    // Only start drag from the toolbar background, not from buttons/inputs
                    if (e.target.tagName === "BUTTON" || e.target.tagName === "INPUT")
                        return;
                    e.stopPropagation();
                    e.currentTarget.setPointerCapture(e.pointerId);
                    const rect = e.currentTarget.getBoundingClientRect();
                    const parentRect = e.currentTarget.offsetParent?.getBoundingClientRect() ?? rect;
                    toolbarDrag.current = {
                        startX: e.clientX,
                        startY: e.clientY,
                        origX: rect.left - parentRect.left,
                        origY: rect.top - parentRect.top,
                    };
                }, onPointerMove: (e) => {
                    if (!toolbarDrag.current)
                        return;
                    e.stopPropagation();
                    const dx = e.clientX - toolbarDrag.current.startX;
                    const dy = e.clientY - toolbarDrag.current.startY;
                    setToolbarPos({
                        x: toolbarDrag.current.origX + dx,
                        y: toolbarDrag.current.origY + dy,
                    });
                }, onPointerUp: (e) => {
                    if (toolbarDrag.current) {
                        e.stopPropagation();
                        toolbarDrag.current = null;
                    }
                }, children: [jsxs("button", { onClick: () => setBrushMode((p) => (p === "fg" ? "bg" : "fg")), style: {
                            background: brushMode === "fg" ? "#2e7d32" : "#c62828",
                            color: "#fff",
                            border: "none",
                            borderRadius: 4,
                            padding: "4px 8px",
                            cursor: "pointer",
                            fontSize: 13,
                        }, children: [brushMode === "fg" ? "+ Foreground" : "- Background", " (X)"] }), jsxs("label", { style: { display: "flex", alignItems: "center", gap: 4 }, children: ["Size:", jsx("input", { type: "range", min: MIN_BRUSH_SIZE, max: MAX_BRUSH_SIZE, value: brushSize, onChange: (e) => setBrushSize(Number(e.target.value)), style: { width: 80 } }), jsx("span", { style: { minWidth: 24, textAlign: "right" }, children: brushSize })] }), jsx("button", { onClick: handleApply, style: {
                            background: "#4caf50",
                            color: "#fff",
                            border: "none",
                            borderRadius: 4,
                            padding: "4px 10px",
                            cursor: "pointer",
                            fontSize: 13,
                            fontWeight: "bold",
                        }, children: "Apply" }), jsx("button", { onClick: handleCancel, style: {
                            background: "#555",
                            color: "#fff",
                            border: "none",
                            borderRadius: 4,
                            padding: "4px 10px",
                            cursor: "pointer",
                            fontSize: 13,
                        }, children: "Cancel" })] })] }));
};

const CanvasActual_ = (props, romaineRef) => {
    // get the props we need
    const { maxHeight, maxWidth, image, saltId } = props;
    const maxDims = useMemo(() => ({ maxHeight, maxWidth }), [maxHeight, maxWidth]);
    const romaine = useRomaine();
    const { cv, canvasApi, romaine: { mode }, setMode, } = romaine;
    const [cropFunc, setCropFunc] = useState(null);
    useImperativeHandle(romaineRef, () => ({
        getBlob: async (opts = {
            jpeg: {
                transparentToWhite: false,
            },
        }) => {
            return new Promise((resolve) => {
                if (canvasPtr.current) {
                    cv.imshow(canvasRef.current, canvasPtr.current);
                    if (opts.jpeg?.transparentToWhite && opts.type === "image/jpeg")
                        transparentToWhite: {
                            const ctx = canvasRef.current.getContext("2d");
                            if (!ctx)
                                break transparentToWhite;
                            const imgData = ctx.getImageData(0, 0, canvasRef.current.width, canvasRef.current.height);
                            const data = imgData.data;
                            for (let i = 0; i < data.length; i += 4) {
                                const alpha = data[i + 3];
                                if (alpha < 255) {
                                    // Alpha-blend against white for semi-transparent pixels
                                    const a = alpha / 255;
                                    data[i] = Math.round(data[i] * a + 255 * (1 - a));
                                    data[i + 1] = Math.round(data[i + 1] * a + 255 * (1 - a));
                                    data[i + 2] = Math.round(data[i + 2] * a + 255 * (1 - a));
                                    data[i + 3] = 255;
                                }
                            }
                            ctx.putImageData(imgData, 0, 0);
                        }
                    canvasRef.current.toBlob((blob) => {
                        resolve(blob);
                        setLoading(false);
                        setMode?.(null);
                    }, opts?.type === "keep-same"
                        ? typeof image !== "string"
                            ? image.type
                            : "image/png"
                        : opts?.type, opts?.quality);
                }
            });
        },
        getDataURL: async (opts = {}) => {
            return new Promise((resolve, reject) => {
                try {
                    if (canvasPtr.current) {
                        cv.imshow(canvasRef.current, canvasPtr.current);
                        resolve(canvasRef.current.toDataURL(opts?.type === "keep-same"
                            ? typeof image !== "string"
                                ? image.type
                                : "image/png"
                            : opts?.type, opts?.quality));
                    }
                }
                catch (err) {
                    reject();
                }
            });
        },
        crop: async () => {
            await cropFunc?.({ preview: true });
            setMode?.(null);
        },
        flip: async (orientation) => {
            setMode?.(`flip-${orientation}`);
        },
        removeBackground: async () => {
            setMode?.("remove-background");
        },
    }));
    const _canvas = useCanvas({
        image,
        saltId,
    });
    const { canvasRef, originalImageDims, loaded, canvasPtr } = _canvas;
    const _preview = usePreview({
        canvasRef,
        maxDims,
        originalImageDims,
    });
    const { createPreview, previewRef: previewCanvasRef, previewDims, setPreviewPaneDimensions, imageResizeRatio, } = _preview;
    // Populate canvasApi for plugin access
    useEffect(() => {
        canvasApi.current = {
            getBlob: async (opts) => {
                return new Promise((resolve) => {
                    if (canvasPtr.current) {
                        cv.imshow(canvasRef.current, canvasPtr.current);
                        canvasRef.current.toBlob((blob) => resolve(blob), opts?.type === "keep-same" ? "image/png" : opts?.type, opts?.quality);
                    }
                    else {
                        resolve(null);
                    }
                });
            },
            setFromBlob: async (blob) => {
                return new Promise((resolve) => {
                    const url = URL.createObjectURL(blob);
                    const img = new Image();
                    img.onload = () => {
                        const tmp = document.createElement("canvas");
                        tmp.width = img.width;
                        tmp.height = img.height;
                        const ctx = tmp.getContext("2d");
                        ctx.drawImage(img, 0, 0);
                        const newMat = cv.imread(tmp);
                        if (canvasPtr.current)
                            canvasPtr.current.delete();
                        canvasPtr.current = newMat;
                        const irr = setPreviewPaneDimensions({
                            width: img.width,
                            height: img.height,
                        });
                        createPreview(irr, canvasPtr.current, false);
                        URL.revokeObjectURL(url);
                        resolve();
                    };
                    img.src = url;
                });
            },
        };
        // No cleanup — the ref is overwritten on each re-run, and nullified
        // naturally when CanvasActual unmounts (no image loaded).
    }, [canvasPtr, canvasRef, cv, createPreview, setPreviewPaneDimensions, canvasApi]);
    // initial preview
    useEffect(() => {
        if (loaded && mode !== "undo")
            createPreview(setPreviewPaneDimensions(originalImageDims), canvasPtr.current, false);
    }, [loaded]);
    // useEffect(() => {
    //   if (loaded && mode !== "undo") {
    //     //   //@ts-ignore
    //     //   console.log("create sepia", cv.bitwise_not);
    //     //   // const M = cv.Mat.eye(3, 3, cv.CV_32FC1);
    //     //   const sepiaKernel = createFilterMat(cv, sepia);
    //     const irr = setPreviewPaneDimensions(originalImageDims);
    //     //   if (canvasPtr.current) {
    //     //     // let anchor = new cv.Point(-1, -1);
    //     //     // stencil
    //     //     //@ts-ignore
    //     //     // cv.adaptiveThreshold(
    //     //     //   canvasPtr.current,
    //     //     //   canvasPtr.current,
    //     //     //   200,
    //     //     //   //@ts-ignore
    //     //     //   cv.ADAPTIVE_THRESH_GAUSSIAN_C,
    //     //     //   cv.THRESH_BINARY,
    //     //     //   3,
    //     //     //   2
    //     //     // );
    //     //     // cv.cvtColor(
    //     //     //   canvasPtr.current,
    //     //     //   canvasPtr.current,
    //     //     //   //@ts-ignore
    //     //     //   cv.COLOR_RGBA2BGRA,
    //     //     //   0
    //     //     // );
    //     //     //@ts-ignore
    //     //     // cv.transform(canvasPtr.current, canvasPtr.current, sepiaKernel);
    //     //     // cv.cvtColor(canvasPtr.current, canvasPtr.current, cv.COLOR_BGR2HSV, 0);
    //     //     cv.bitwise_not(canvasPtr.current, canvasPtr.current);
    //     //   }
    //     createPreview(irr, canvasPtr.current, false);
    //     //   sepiaKernel.delete();
    //   }
    // }, [loaded]);
    const [loading, setLoading] = useState(true);
    useEffect(() => {
        if (loaded && canvasPtr.current?.$$.ptr)
            setLoading(false);
    }, [loaded]);
    // window resizing re-render canvas
    const resizeTimeout = useRef(undefined);
    useEffect(() => {
        const windowResizeEvent = () => {
            // timeout 1s for resize event to finish
            const resizeRatio = setPreviewPaneDimensions(canvasPtr.current && {
                width: canvasPtr.current.cols,
                height: canvasPtr.current.rows,
            });
            if (resizeRatio !== Infinity) {
                clearTimeout(resizeTimeout.current);
                const timeout = setTimeout(() => {
                    createPreview(resizeRatio, canvasPtr.current, false);
                }, 250);
                resizeTimeout.current = timeout;
            }
        };
        if (!loading)
            windowResizeEvent();
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [setPreviewPaneDimensions]);
    // opencv documentation
    // https://docs.opencv.org/3.4/dd/d52/tutorial_js_geometric_transformations.html
    useEffect(() => {
        handleModeChange({ romaine, _preview, _canvas });
        if (canvasRef.current && !loading) ;
    }, [mode, loaded]);
    return (jsxs("div", { style: {
            position: "relative",
            ...(previewDims && buildImgContainerStyle(previewDims)),
            ...((mode === "refine-background" || mode === "ml-refine-brush") && {
                backgroundColor: "#111",
            }),
        }, children: [jsx("canvas", { id: `${saltId ? saltId + "-" : ""}preview-canvas`, style: {
                    ...(mode !== "refine-background" && mode !== "ml-refine-brush" && {
                        backgroundSize: "20px 20px",
                        backgroundImage: "linear-gradient(to bottom, #0001 10px, #0003 10px),linear-gradient(to right, #0002 10px, #0004 10px),linear-gradient(to right, transparent 10px, #ffff 10px),linear-gradient(to bottom, #0004 10px, transparent 10px),linear-gradient(to bottom, #ffff 10px, #ffff 10px)",
                    }),
                    position: "absolute",
                    top: 0,
                    right: 0,
                    left: 0,
                    bottom: 0,
                    zIndex: 5,
                    pointerEvents: "none",
                }, ref: previewCanvasRef, width: maxWidth, height: maxHeight }), (mode === "crop" || mode === "perspective-crop") &&
                !loading &&
                canvasPtr.current?.$$.ptr && (jsx(CroppingCanvas, { setCropFunc: setCropFunc, imageResizeRatio: imageResizeRatio, setPreviewPaneDimensions: setPreviewPaneDimensions, showPreview: createPreview, canvasRef: canvasRef, canvasPtr: canvasPtr, previewCanvasRef: previewCanvasRef, previewDims: previewDims, ...props })), mode === "refine-background" &&
                !loading &&
                canvasPtr.current?.$$.ptr && (jsx(BrushCanvas, { canvasRef: canvasRef, canvasPtr: canvasPtr, previewCanvasRef: previewCanvasRef, previewDims: previewDims, imageResizeRatio: imageResizeRatio, showPreview: createPreview, setPreviewPaneDimensions: setPreviewPaneDimensions }))] }));
};
const CanvasActual = forwardRef(CanvasActual_);
/**
 * This function is to make sure the the user is using Romaine context
 *
 * Can also pass children that can be absolutely positioned
 */
const CanvasInner = ({ openCvPath: _openCvPath, children, image, wrapperProps = {}, ...props }, ref) => {
    const { cv, loaded } = useRomaine();
    const salt = useMemo(() => `${props.saltId ? props.saltId + "-" : ""}romaine-wrapper`, []);
    return cv || loaded ? (jsxs("div", { id: salt, ...wrapperProps, style: {
            position: "relative",
            display: "grid",
            placeItems: "center",
            width: props.maxWidth,
            height: props.maxHeight,
            ...wrapperProps.style,
        }, children: [children, image && cv && jsx(CanvasActual, { image: image, ...props, ref: ref })] })) : null;
};
const Canvas = forwardRef(CanvasInner);

export { Canvas, CropPoint, CropPoints, CropPointsDelimiters, CroppingCanvas, OpenCvConsumer, OpenCvContext, Romaine, addPadding, applyFilter, calcDims, cropOpenCV, hasGrabCutMask, history, initialRomaineState, isCrossOriginURL, multiplyMatrices, readFile, romaineReducer, useRomaine, warpPerspective };
//# sourceMappingURL=index.jsx.map
