import { colorDiff, intToRGBA, scan } from "@jimp/utils";
import { z } from "zod";
export const CropOptionsSchema = z.object({
    /** the x position to crop form */
    x: z.number(),
    /** the y position to crop form */
    y: z.number(),
    /** the width to crop form */
    w: z.number(),
    /** the height to crop form */
    h: z.number(),
});
const AutocropComplexOptionsSchema = z.object({
    /** percent of color difference tolerance (default value) */
    tolerance: z.number().min(0).max(1).optional(),
    /** flag to force cropping only if the image has a real "frame" i.e. all 4 sides have some border (default value) */
    cropOnlyFrames: z.boolean().optional(),
    /** force cropping top be symmetric */
    cropSymmetric: z.boolean().optional(),
    /** Amount of pixels in border to leave */
    leaveBorder: z.number().optional(),
    ignoreSides: z
        .object({
        north: z.boolean().optional(),
        south: z.boolean().optional(),
        east: z.boolean().optional(),
        west: z.boolean().optional(),
    })
        .optional(),
});
export const methods = {
    /**
     * Crops the image at a given point to a give size.
     *
     * @example
     * ```ts
     * import { Jimp } from "jimp";
     *
     * const image = await Jimp.read("test/image.png");
     * const cropped = image.crop(150, 100);
     * ```
     */
    crop(image, options) {
        let { x, y, w, h } = CropOptionsSchema.parse(options);
        // round input
        x = Math.round(x);
        y = Math.round(y);
        w = Math.round(w);
        h = Math.round(h);
        if (x === 0 && w === image.bitmap.width) {
            // shortcut
            const start = (w * y + x) << 2;
            const end = start + ((h * w) << 2);
            image.bitmap.data = image.bitmap.data.slice(start, end);
        }
        else {
            const bitmap = Buffer.allocUnsafe(w * h * 4);
            let offset = 0;
            scan(image, x, y, w, h, function (_, __, idx) {
                const data = image.bitmap.data.readUInt32BE(idx);
                bitmap.writeUInt32BE(data, offset);
                offset += 4;
            });
            image.bitmap.data = bitmap;
        }
        image.bitmap.width = w;
        image.bitmap.height = h;
        return image;
    },
    /**
     * Autocrop same color borders from this image.
     * This function will attempt to crop out transparent pixels from the image.
     *
     * @example
     * ```ts
     * import { Jimp } from "jimp";
     *
     * const image = await Jimp.read("test/image.png");
     * const cropped = image.autocrop();
     * ```
     */
    autocrop(image, options = {}) {
        const { tolerance = 0.0002, cropOnlyFrames = true, cropSymmetric = false, leaveBorder = 0, ignoreSides: ignoreSidesArg, } = typeof options === "number"
            ? { tolerance: options }
            : AutocropComplexOptionsSchema.parse(options);
        const w = image.bitmap.width;
        const h = image.bitmap.height;
        const minPixelsPerSide = 1; // to avoid cropping completely the image, resulting in an invalid 0 sized image
        // i.e. north and south / east and west are cropped by the same value
        const ignoreSides = {
            north: false,
            south: false,
            east: false,
            west: false,
            ...ignoreSidesArg,
        };
        /**
         * All borders must be of the same color as the top left pixel, to be cropped.
         * It should be possible to crop borders each with a different color,
         * but since there are many ways for corners to intersect, it would
         * introduce unnecessary complexity to the algorithm.
         */
        // scan each side for same color borders
        let colorTarget = image.getPixelColor(0, 0); // top left pixel color is the target color
        const rgba1 = intToRGBA(colorTarget);
        // for north and east sides
        let northPixelsToCrop = 0;
        let eastPixelsToCrop = 0;
        let southPixelsToCrop = 0;
        let westPixelsToCrop = 0;
        // north side (scan rows from north to south)
        colorTarget = image.getPixelColor(0, 0);
        if (!ignoreSides.north) {
            north: for (let y = 0; y < h - minPixelsPerSide; y++) {
                for (let x = 0; x < w; x++) {
                    const colorXY = image.getPixelColor(x, y);
                    const rgba2 = intToRGBA(colorXY);
                    if (colorDiff(rgba1, rgba2) > tolerance) {
                        // this pixel is too distant from the first one: abort this side scan
                        break north;
                    }
                }
                // this row contains all pixels with the same color: increment this side pixels to crop
                northPixelsToCrop++;
            }
        }
        // west side (scan columns from west to east)
        colorTarget = image.getPixelColor(w, 0);
        if (!ignoreSides.west) {
            west: for (let x = 0; x < w - minPixelsPerSide; x++) {
                for (let y = 0 + northPixelsToCrop; y < h; y++) {
                    const colorXY = image.getPixelColor(x, y);
                    const rgba2 = intToRGBA(colorXY);
                    if (colorDiff(rgba1, rgba2) > tolerance) {
                        // this pixel is too distant from the first one: abort this side scan
                        break west;
                    }
                }
                // this column contains all pixels with the same color: increment this side pixels to crop
                westPixelsToCrop++;
            }
        }
        // south side (scan rows from south to north)
        colorTarget = image.getPixelColor(0, h);
        if (!ignoreSides.south) {
            south: for (let y = h - 1; y >= northPixelsToCrop + minPixelsPerSide; y--) {
                for (let x = w - eastPixelsToCrop - 1; x >= 0; x--) {
                    const colorXY = image.getPixelColor(x, y);
                    const rgba2 = intToRGBA(colorXY);
                    if (colorDiff(rgba1, rgba2) > tolerance) {
                        // this pixel is too distant from the first one: abort this side scan
                        break south;
                    }
                }
                // this row contains all pixels with the same color: increment this side pixels to crop
                southPixelsToCrop++;
            }
        }
        // east side (scan columns from east to west)
        colorTarget = image.getPixelColor(w, h);
        if (!ignoreSides.east) {
            east: for (let x = w - 1; x >= 0 + westPixelsToCrop + minPixelsPerSide; x--) {
                for (let y = h - 1; y >= 0 + northPixelsToCrop; y--) {
                    const colorXY = image.getPixelColor(x, y);
                    const rgba2 = intToRGBA(colorXY);
                    if (colorDiff(rgba1, rgba2) > tolerance) {
                        // this pixel is too distant from the first one: abort this side scan
                        break east;
                    }
                }
                // this column contains all pixels with the same color: increment this side pixels to crop
                eastPixelsToCrop++;
            }
        }
        // decide if a crop is needed
        let doCrop = false;
        // apply leaveBorder
        westPixelsToCrop -= leaveBorder;
        eastPixelsToCrop -= leaveBorder;
        northPixelsToCrop -= leaveBorder;
        southPixelsToCrop -= leaveBorder;
        if (cropSymmetric) {
            const horizontal = Math.min(eastPixelsToCrop, westPixelsToCrop);
            const vertical = Math.min(northPixelsToCrop, southPixelsToCrop);
            westPixelsToCrop = horizontal;
            eastPixelsToCrop = horizontal;
            northPixelsToCrop = vertical;
            southPixelsToCrop = vertical;
        }
        // make sure that crops are >= 0
        westPixelsToCrop = westPixelsToCrop >= 0 ? westPixelsToCrop : 0;
        eastPixelsToCrop = eastPixelsToCrop >= 0 ? eastPixelsToCrop : 0;
        northPixelsToCrop = northPixelsToCrop >= 0 ? northPixelsToCrop : 0;
        southPixelsToCrop = southPixelsToCrop >= 0 ? southPixelsToCrop : 0;
        // safety checks
        const widthOfRemainingPixels = w - (westPixelsToCrop + eastPixelsToCrop);
        const heightOfRemainingPixels = h - (southPixelsToCrop + northPixelsToCrop);
        if (cropOnlyFrames) {
            // crop image if all sides should be cropped
            doCrop =
                eastPixelsToCrop !== 0 &&
                    northPixelsToCrop !== 0 &&
                    westPixelsToCrop !== 0 &&
                    southPixelsToCrop !== 0;
        }
        else {
            // crop image if at least one side should be cropped
            doCrop =
                eastPixelsToCrop !== 0 ||
                    northPixelsToCrop !== 0 ||
                    westPixelsToCrop !== 0 ||
                    southPixelsToCrop !== 0;
        }
        if (doCrop) {
            // do the real crop
            this.crop(image, {
                x: westPixelsToCrop,
                y: northPixelsToCrop,
                w: widthOfRemainingPixels,
                h: heightOfRemainingPixels,
            });
        }
        return image;
    },
};