const fs = require('fs-extra');
const Sharp = require('sharp');
const debug = require('debug')('server-connect:image');
const { basename, extname, join } = require('path');
const { toAppPath, toSystemPath, parseTemplate, getUniqFile } = require('../core/path');

const positions = {
    'center': 0,
    'centre': 0,
    'top': 1,
    'north': 1,
    'right': 2,
    'east': 2,
    'bottom': 3,
    'south': 3,
    'left': 4,
    'west': 4,
    'top right': 5,
    'right top': 5,
    'northeast': 5,
    'bottom right': 6,
    'right bottom': 6,
    'southeast': 6,
    'bottom left': 7,
    'left bottom': 7,
    'southwest': 7,
    'top left': 8,
    'left top': 8,
    'northwest': 8,
    'entropy': 16,
    'attention': 17
};

function cw(w, meta) {
    if (typeof w == 'string') {
        if (/%$/.test(w)) {
            w = meta.width * parseFloat(w) / 100;
        }
    }

    if (w < 0) {
        w = meta.width + w;
    }

    return parseInt(w);
}

function ch(h, meta) {
    if (typeof h == 'string') {
        if (/%$/.test(h)) {
            h = meta.height * parseFloat(h) / 100;
        }
    }

    if (h < 0) {
        h = meta.height + h;
    }

    return parseInt(h);
}

function cx(x, w, meta) {
    if (typeof x == 'string') {
        switch (x) {
            case 'left':
                x = 0;
                break;
            case 'center':
                x = (meta.width - w) / 2;
                break;
            case 'right':
                x = meta.width - w;
                break;
            default:
                if (/%$/.test(x)) {
                    x = (meta.width - w) * parseFloat(x) / 100;
                }
        }
    }

    if (x < 0) {
        x = meta.width - w + x;
    }

    return parseInt(x);
}

function cy(y, h, meta) {
    if (typeof y == 'string') {
        switch (y) {
            case 'top':
                y = 0;
                break;
            case 'middle':
                y = (meta.height - h) / 2;
                break;
            case 'bottom':
                y = meta.height - h;
                break;
            default:
                if (/%$/.test(y)) {
                    y = (meta.height - h) * parseFloat(y) / 100;
                }
        }
    }

    if (y < 0) {
        y = meta.height - h + y;
    }

    return parseInt(y);
}

async function updateImage(sharp) {
    sharp.image = Sharp(await sharp.image.toBuffer());
    sharp.metadata = await sharp.image.metadata();
}

module.exports = {

    getImageSize: async function (options) {
        let path = toSystemPath(this.parseRequired(options.path, 'string', 'image.getImageSize: path is required.'));

        const image = Sharp(path);
        const metadata = await image.metadata();

        return {
            width: metadata.width,
            height: metadata.height
        };
    },

    load: async function (options, name) {
        let path = toSystemPath(this.parseRequired(options.path, 'string', 'image.load: path is required.'));
        let orient = this.parseOptional(options.autoOrient, 'boolean', false);

        this.req.image = this.req.image || {};
        this.req.image[name] = { name: basename(path), image: Sharp(path), metadata: null };

        const sharp = this.req.image[name];
        if (orient) sharp.image.rotate();

        await updateImage(sharp);

        return {
            name: basename(path),
            width: sharp.metadata.width,
            height: sharp.metadata.height
        };
    },

    save: async function (options) {
        const sharp = this.req.image[options.instance];
        if (!sharp) throw new Error(`image.save: instance "${options.instance} doesn't exist.`);

        let path = toSystemPath(this.parseRequired(options.path, 'string', 'image.save: path is required.'));
        let format = this.parseOptional(options.format, 'string', 'jpeg').toLowerCase();
        let template = this.parseOptional(options.template, 'string', '{name}{ext}');
        let overwrite = this.parseOptional(options.overwrite, 'boolean', false);
        let createPath = this.parseOptional(options.createPath, 'boolean', true);
        let background = this.parseOptional(options.background, 'string', '#FFFFFF');
        let quality = this.parseOptional(options.quality, 'number', 75);

        if (!fs.existsSync(path)) {
            if (createPath) {
                await fs.ensureDir(path);
            } else {
                throw new Error(`image.save: path "${path}" doesn't exist.`);
            }
        }

        let file = join(path, sharp.name);

        if (template) {
            file = parseTemplate(file, template);
        }

        if (format == 'auto') {
            switch (extname(file).toLowerCase()) {
                case '.png': format = 'png'; break;
                case '.gif': format = 'gif'; break;
                case '.webp': format = 'webp'; break;
                default: format = 'jpeg';
            }
        }
        
        if (format == 'jpeg') {
            sharp.image.flatten({ background });
            sharp.image.toFormat(format, { quality });
        } else if (format == 'webp') {
            sharp.image.toFormat(format, { quality });
        } else {
            sharp.image.toFormat(format);
        }

        const data = await sharp.image.toBuffer();
        
        file = file.replace(extname(file), '.' + format.replace('jpeg', 'jpg'));
        
        if (fs.existsSync(file)) {
            if (overwrite) {
                await fs.unlink(file);
            } else {
                file = getUniqFile(file);
            }
        }

        await fs.writeFile(file, data) //.toFile(file);

        return toAppPath(file);
    },

    resize: async function (options) {
        const sharp = this.req.image[options.instance];
        if (!sharp) throw new Error(`image.resize: instance "${options.instance} doesn't exist.`);

        let width = this.parseOptional(cw(this.parse(options.width), sharp.metadata), 'number', null);
        let height = this.parseOptional(ch(this.parse(options.height), sharp.metadata), 'number', null);
        let upscale = this.parseOptional(options.upscale, 'boolean', false);

        if (isNaN(width)) width = null;
        if (isNaN(height)) height = null;

        sharp.image.resize(width, height, { fit: width && height ? 'fill' : 'cover', withoutEnlargement: !upscale });

        await updateImage(sharp);
    },

    crop: async function (options) {
        const sharp = this.req.image[options.instance];
        if (!sharp) throw new Error(`image.crop: instance "${options.instance} doesn't exist.`);

        let width = this.parseRequired(cw(this.parse(options.width)), 'number', 'image.crop: width is required.');
        let height = this.parseRequired(ch(this.parse(options.height)), 'number', 'image.crop: height is required.');
        if (width > sharp.metadata.width) width = sharp.metadata.width;
        if (height > sharp.metadata.height) height = sharp.metadata.height;
        let left = this.parseRequired(cx(this.parse(options.x), width, sharp.metadata), 'number', 'image.crop: x is required.');
        let top = this.parseRequired(cy(this.parse(options.y), height, sharp.metadata), 'number', 'image.crop: y is required.');

        sharp.image.extract({ left, top, width, height });
        
        await updateImage(sharp);
    },

    cover: async function (options) {
        const sharp = this.req.image[options.instance];
        if (!sharp) throw new Error(`image.cover: instance "${options.instance}" doesn't exist.`);

        let width = this.parseRequired(options.width, 'number', 'image.cover: width is required.');
        let height = this.parseRequired(options.height, 'number', 'image.cover: height is required.');
        // position: see positions object for options
        let position = this.parseOptional(options.position, 'string', 'center');
        // kernel: 'nearest', 'cubic', 'mitchell', 'lanczos2', 'lanczos3'
        let kernel = this.parseOptional(options.kernel, 'string', 'lanczos3');

        position = positions[position] || 0;

        sharp.image.resize({ width, height, position, kernel });
        
        await updateImage(sharp);
    },

    watermark: async function (options) {
        const sharp = this.req.image[options.instance];
        if (!sharp) throw new Error(`image.watermark: instance "${options.instance} doesn't exist.`);

        let path = toSystemPath(this.parseRequired(options.path, 'string', 'image.watermark: path is required.'));
        let image = Sharp(path);
        let metadata = await image.metadata();
        let input = await image.toBuffer();
        let left = this.parseRequired(cx(this.parse(options.x), metadata.width, sharp.metadata), 'number', 'image.watermark: x is required.');
        let top = this.parseRequired(cy(this.parse(options.y), metadata.height, sharp.metadata), 'number', 'image.watermark: y is required.');

        sharp.image.composite([{ input, left, top }]);
    },

    text: async function (options) {
        const sharp = this.req.image[options.instance];
        if (!sharp) throw new Error(`image.text: instance "${options.instance} doesn't exist.`);

        let x = this.parse(options.x);
        let y = this.parse(options.y);
        let text = this.parseRequired(options.text, 'string', 'image.text: text is required.');
        let font = this.parseOptional(options.font, 'string', 'Verdana');
        let size = this.parseOptional(options.size, 'number', 24);
        let color = this.parseOptional(options.color, 'string', '#ffffff');

        let width = sharp.metadata.width;
        let height = sharp.metadata.height;
        let anchor = 'start';

        switch (x) {
            case 'left':
                x = '0%';
                anchor = 'start';
                break;
            case 'center':
                x = '50%';
                anchor = 'middle';
                break;
            case 'right':
                x = '100%';
                anchor = 'end';
                break;
            default:
                if (x < 0) {
                    x = width - x;
                    anchor = 'end';
                }
        }

        switch (y) {
            case 'top':
                y = size;
                break;
            case 'middle':
                y = (height / 2) - (size / 2);
                break;
            case 'bottom':
                y = height;
                break;
            default:
                if (y < 0) {
                    y = height - size - y;
                }
        }

        let svg = `
            <svg width="${width}" height="${height}">
                <style>
                    .text {
                        fill: ${color};
                        font-family: "${font}";
                        font-size: ${size}px;
                        line-height: 1;
                    }
                </style>
                <text x="${x}" y="${y}" text-anchor="${anchor}" class="text">${text}</text>
            </svg>
        `;

        const input = await Sharp(Buffer.from(svg)).toBuffer();

        sharp.image.composite([{ input, left: 0, top: 0 }]);
    },

    tiled: async function (options) {
        const sharp = this.req.image[options.instance];
        if (!sharp) throw new Error(`image.tiled: instance "${options.instance} doesn't exist.`);

        let input = toSystemPath(this.parseRequired(options.path, 'string', 'image.tiled: path is required.'));
        let padding = this.parseOptional(options.padding, 'number', 0);

        if (padding) {
            input = await Sharp(input).extend({
                top: padding, left: padding, bottom: 0, right: 0, background: { r: 0, g: 0, b: 0, alpha: 0 }
            }).toBuffer();
        }

        sharp.image.composite([{ input, left: 0, top: 0, tile: true }]);
    },

    flip: async function (options) {
        const sharp = this.req.image[options.instance];
        if (!sharp) throw new Error(`image.flip: instance "${options.instance} doesn't exist.`);

        let horizontal = this.parseOptional(options.horizontal, 'boolean', false);
        let vertical = this.parseOptional(options.vertical, 'boolean', false);

        if (horizontal) sharp.image.flop();
        if (vertical) sharp.image.flip();
    },

    rotateLeft: async function (options) {
        const sharp = this.req.image[options.instance];
        if (!sharp) throw new Error(`image.rotateLeft: instance "${options.instance} doesn't exist.`);

        sharp.image.rotate(-90);

        await updateImage(sharp);
    },

    rotateRight: async function (options) {
        const sharp = this.req.image[options.instance];
        if (!sharp) throw new Error(`image.rotateRight: instance "${options.instance} doesn't exist.`);

        sharp.image.rotate(90);

        await updateImage(sharp);
    },

    smooth: async function (options) {
        const sharp = this.req.image[options.instance];
        if (!sharp) throw new Error(`image.smooth: instance "${options.instance} doesn't exist.`);

        sharp.image.convolve({
            width: 3,
            height: 3,
            kernel: [
                1, 1, 1,
                1, 1, 1,
                1, 1, 1
            ]
        });
    },

    blur: async function (options) {
        const sharp = this.req.image[options.instance];
        if (!sharp) throw new Error(`image.blur: instance "${options.instance} doesn't exist.`);

        sharp.image.convolve({
            width: 3,
            height: 3,
            kernel: [
                1, 2, 1,
                2, 4, 2,
                1, 2, 1
            ]
        });
    },

    sharpen: async function (options) {
        const sharp = this.req.image[options.instance];
        if (!sharp) throw new Error(`image.sharpen: instance "${options.instance} doesn't exist.`);

        sharp.image.convolve({
            width: 3,
            height: 3,
            kernel: [
                0, -2, 0,
                -2, 15, -2,
                0, -2, 0
            ]
        });
    },

    meanRemoval: async function (options) {
        const sharp = this.req.image[options.instance];
        if (!sharp) throw new Error(`image.meanRemoval: instance "${options.instance} doesn't exist.`);

        sharp.image.convolve({
            width: 3,
            height: 3,
            kernel: [
                -1, -1, -1,
                -1, 9, -1,
                -1, -1, -1
            ]
        });
    },

    emboss: async function (options) {
        const sharp = this.req.image[options.instance];
        if (!sharp) throw new Error(`image.emboss: instance "${options.instance} doesn't exist.`);

        sharp.image.convolve({
            width: 3,
            height: 3,
            kernel: [
                -1, 0, -1,
                0, 4, 0,
                -1, 0, -1
            ],
            offset: 127
        });
    },

    edgeDetect: async function (options) {
        const sharp = this.req.image[options.instance];
        if (!sharp) throw new Error(`image.edgeDetect: instance "${options.instance} doesn't exist.`);

        sharp.image.convolve({
            width: 3,
            height: 3,
            kernel: [
                -1, -1, -1,
                0, 0, 0,
                1, 1, 1
            ],
            offset: 127
        });
    },

    grayscale: async function (options) {
        const sharp = this.req.image[options.instance];
        if (!sharp) throw new Error(`image.grayscale: instance "${options.instance} doesn't exist.`);

        sharp.image.grayscale();
    },

    sepia: async function (options) {
        const sharp = this.req.image[options.instance];
        if (!sharp) throw new Error(`image.sepia: instance "${options.instance} doesn't exist.`);

        sharp.image.tint({ r: 112, g: 66, b: 20 });
    },

    invert: async function (options) {
        const sharp = this.req.image[options.instance];
        if (!sharp) throw new Error(`image.invert: instance "${options.instance} doesn't exist.`);

        sharp.image.negate();
    },

};