import { CollageConfig, CollageContext, CollageImage, CollageMode, Segment } from "@/common/types";
import { choice, randint, range, shuffle } from "@/common/utils";

export const collageModeType = [
    "clean_grid", "chaos_grid",
    "row", "irow",
    "col", "icol",
    "concentric_factor", "concentric_spaced",
    "blend"
] as const;
export type CollageModeType = typeof collageModeType[number];

export abstract class CollageModes<C extends CollageContext, I extends CollageImage> {
    readonly modes: { [key in CollageModeType]: CollageMode<C, I> } = {
        "clean_grid": {
            name: "Clean Grid",
            minImages: 4,
            forceConfig: {
                numImages: 4
            },
            getSegments: this.getGridSegments,
            place: this.cleanPlace.bind(this)
        },
        "chaos_grid": {
            name: "Irregular Grid",
            minImages: 4,
            forceConfig: {
                numImages: 4
            },
            getSegments: this.getGridSegments,
            place: (ctx, images, segments) => {
                const shuffledImages = shuffle(images);
                shuffle(segments.map((segment, idx) => [segment, idx] as [Segment, number]))
                    .forEach(([segment, idx]) => {
                        const image = shuffledImages[idx];
                        const scaleRatio = Math.max(segment.w / image.width, segment.h / image.height);
                        this.drawImage(ctx, image, 0, 0, image.width, image.height,
                            segment.x - (image.width * scaleRatio / 2), segment.y - (image.height * scaleRatio / 2),
                            image.width * scaleRatio, image.height * scaleRatio);
                    });
            },
        },
        "row": {
            name: "Regular Row",
            minImages: 2,
            getSegments: (ctx, config, images) => {
                const numImages = Math.min(images?.length || Infinity, config?.numImages || randint(4) + 2);
                const segmentSize = [ctx.canvas.width / numImages, ctx.canvas.height];
                return range(numImages).map((idx) => {
                    return {
                        x: idx * segmentSize[0] + segmentSize[0] / 2,
                        y: segmentSize[1] / 2,
                        w: segmentSize[0],
                        h: segmentSize[1]
                    };
                });
            },
            place: this.cleanPlace.bind(this)
        },
        "irow": {
            name: "Irregular Row",
            minImages: 2,
            getSegments: (ctx, config, images) => {
                const numImages = Math.min(images?.length || Infinity, config?.numImages || randint(4) + 2);
                const segmentSize = [ctx.canvas.width / numImages, ctx.canvas.height];
                return range(numImages).map((idx) => {
                    const irregularWidth = images ?
                        segmentSize[0] + Math.random() * ((segmentSize[1] / images[idx].height * images[idx].width) - segmentSize[0]) :
                        segmentSize[0] + Math.random() * segmentSize[0] * .5;
                    return {
                        x: idx * segmentSize[0] + segmentSize[0] / 2,
                        y: segmentSize[1] / 2,
                        w: Math.min(ctx.canvas.width / 2, irregularWidth),
                        h: segmentSize[1],
                    };
                });
            },
            place: this.cleanPlace.bind(this)
        },
        "col": {
            name: "Regular Column",
            minImages: 2,
            getSegments: (ctx, config, images) => {
                const numImages = Math.min(images?.length || Infinity, config?.numImages || randint(4) + 2);
                const segmentSize = [ctx.canvas.width, ctx.canvas.height / numImages];
                return range(numImages).map((idx) => {
                    return {
                        x: segmentSize[0] / 2,
                        y: idx * segmentSize[1] + segmentSize[1] / 2,
                        w: segmentSize[0],
                        h: segmentSize[1]
                    };
                });
            },
            place: this.cleanPlace.bind(this)
        },
        "icol": {
            name: "Irregular Column",
            minImages: 2,
            getSegments: (ctx, config, images) => {
                const numImages = Math.min(images?.length || Infinity, config?.numImages || randint(4) + 2);
                const segmentSize = [ctx.canvas.width, ctx.canvas.height / numImages];
                return range(numImages).map((idx) => {
                    const irregularHeight = images ?
                        segmentSize[1] + Math.random() * ((segmentSize[0] / images[idx].width * images[idx].height) - segmentSize[1]) :
                        segmentSize[1] + Math.random() * segmentSize[1] * .5;
                    return {
                        x: segmentSize[0] / 2,
                        y: idx * segmentSize[1] + segmentSize[1] / 2,
                        w: segmentSize[0],
                        h: Math.min(ctx.canvas.height / 2, irregularHeight),
                    };
                });
            },
            place: this.cleanPlace.bind(this)
        },
        "concentric_factor": {
            name: "Constant factor concentric",
            minImages: 2,
            getSegments: (ctx, config, images) => {
                const numImages = Math.min(images?.length || Infinity, config?.numImages || randint(4) + 2);
                let factor: number;
                if (Math.random() > .5) {
                    factor = choice([1 / Math.sqrt(2), .5, .88]);
                } else {
                    factor = 1 - (1 / numImages);
                }
                return range(numImages).map((idx) => {
                    const ratio = Math.pow(factor, idx);
                    return {
                        x: ctx.canvas.width / 2,
                        y: ctx.canvas.height / 2,
                        w: ctx.canvas.width * ratio,
                        h: ctx.canvas.height * ratio
                    };
                });
            },
            place: this.cleanPlace.bind(this)
        },
        "concentric_spaced": {
            name: "Equally spaced concentric",
            minImages: 2,
            getSegments: (ctx, config, images) => {
                const numImages = Math.min(images?.length || Infinity, config?.numImages || randint(2) + 2);
                return range(numImages).map((idx) => {
                    return {
                        x: ctx.canvas.width / 2,
                        y: ctx.canvas.height / 2,
                        w: ctx.canvas.width - (ctx.canvas.width / numImages * idx),
                        h: ctx.canvas.height - (ctx.canvas.height / numImages * idx),
                    };
                });
            },
            place: this.cleanPlace.bind(this)
        },
        "blend": {
            name: "Blending",
            minImages: 2,
            getSegments: (ctx, config, images) => {
                const numImages = Math.min(images?.length || Infinity, config?.numImages || randint(2) + 2);
                return range(numImages).map((_) => {
                    return {
                        x: ctx.canvas.width / 2,
                        y: ctx.canvas.height / 2,
                        w: ctx.canvas.width,
                        h: ctx.canvas.height
                    };
                });
            },
            place: (ctx, images, segments) => {
                ctx.globalCompositeOperation = choice(["difference", "saturation", "soft-light", "overlay"]);
                this.cleanPlace(ctx, images, segments);
            }
        }
    };

    private cleanDraw(ctx: C, image: I,
        x: number, y: number, w: number, h: number) {
        const scaleRatio = Math.max(w / image.width, h / image.height);
        this.drawImage(ctx, image, image.width / 2 - w / scaleRatio / 2, image.height / 2 - h / scaleRatio / 2,
            w / scaleRatio, h / scaleRatio,
            x - w / 2, y - h / 2,
            w, h);
    }

    private cleanPlace(ctx: C, images: I[], segments: Segment[]) {
        segments.forEach((segment, idx) => {
            this.cleanDraw(ctx, images[idx], segment.x, segment.y, segment.w, segment.h);
        });
    }

    private getGridSegments(ctx: C, config?: CollageConfig) {
        return [0, 1, 2, 3].map((idx) => {
            let x!: number, y!: number;
            switch (idx) {
                case 0:
                    x = ctx.canvas.width * .25;
                    y = ctx.canvas.height * .25;
                    break;
                case 1:
                    x = ctx.canvas.width * .75;
                    y = ctx.canvas.height * .25;
                    break;
                case 2:
                    x = ctx.canvas.width * .25;
                    y = ctx.canvas.height * .75;
                    break;
                case 3:
                    x = ctx.canvas.width * .75;
                    y = ctx.canvas.height * .75;
                    break;
            }
            return {
                x, y,
                w: ctx.canvas.width / 2,
                h: ctx.canvas.height / 2
            };
        });
    }

    abstract drawImage(ctx: C, image: I, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void;
}