const md5 = require('md5');
const fs = require('fs-extra');
const sharp = require('sharp');

module.exports = Self => {
    Self.remoteMethod('upload', {
        description: 'Uploads an image',
        accessType: 'WRITE',
        accepts: [
            {
                arg: 'ctx',
                type: 'Object',
                http: {source: 'context'}
            }
        ],
        returns: {
            type: Self.modelName,
            description: 'The resulting file instance',
            root: true,
        },
        http: {
            path: `/upload`,
            verb: 'POST'
        }
    });

    Self.upload = async ctx => {
        let app = Self.app;
        let $ = app.models;
        let storageConnector = app.dataSources.storage.connector;

        let tx = await Self.beginTransaction({});
        let myOptions = {transaction: tx};

        async function getContainer(name) {
            let container;
            try {
                container = await $.Container.getContainer(name);
            } catch (err) {
                if (err.code === 'ENOENT') {
                    container = await $.Container.createContainer({
                        name: name
                    });
                } else throw err;
            }
    
            return container;
        }

        try {
            // Upload file to temporary path

            let tempContainer = await getContainer('temp');
            let uploaded = await $.Container.upload(tempContainer.name, ctx.req, ctx.result, {});
            let files = Object.values(uploaded.files).map(file => file[0]);
            let args = {};

            for (let key in uploaded.fields)
                args[key] = uploaded.fields[key][0];

            if (!/^[a-z0-9_]+$/.test(args.name))
                throw new Error('Bad file name');

            let collection = await $.ImageCollection.findOne({
                fields: [
                    'id',
                    'name',
                    'maxWidth',
                    'maxHeight',
                    'model',
                    'property'
                ],
                where: {name: args.collectionFk},
                include: {
                    relation: 'sizes',
                    scope: {
                        fields: ['width', 'height', 'crop']
                    }
                }
            });

            let md5Hash = md5(args.name).substring(0, 4);
            let md5Path = md5Hash.match(/(..?)/g).join('/');
            let rootPath = storageConnector.client.root;

            let file = files[0];
            let data = {
                name: args.name,
                collectionFk: args.collectionFk
            };
            let newImage = await Self.upsertWithWhere(data, {
                name: args.name,
                collectionFk: args.collectionFk,
                updated: (new Date).getTime()
            }, myOptions);

            // Resizes and saves the image

            let extension = file.name.split('.').pop().toLowerCase();
            let srcPath = `${rootPath}/${tempContainer.name}/${file.name}`;
            let collectionDir = `${rootPath}/${args.collectionFk}`;
            let dstDir = `${collectionDir}/${md5Path}/${args.name}`;
            let dstFile = `full.${extension}`;

            let resizeOpts = {
                withoutEnlargement: true,
                fit: 'inside'
            };

            await fs.mkdir(dstDir, {recursive: true});
            await sharp(srcPath)
                .resize(collection.maxWidth, collection.maxHeight, resizeOpts)
                .toFile(`${dstDir}/${dstFile}`);

            let sizes = collection.sizes();
            for (let size of sizes) {
                let dstFile = `${size.width}x${size.height}.${extension}`;
                let resizeOpts = {
                    withoutEnlargement: true,
                    fit: size.crop ? 'cover' : 'inside'
                };

                await sharp(srcPath)
                    .resize(size.width, size.height, resizeOpts)
                    .toFile(`${dstDir}/${dstFile}`);
            }

            // Updates items with matching id, when option is checked

            if (args.updateMatching === 'true') {
                if (!collection.model || !collection.property)
                    throw new Error('Matching model settings not defined');

                let model = app.models[collection.model];

                if (!model)
                    throw new Error('Matching model not found');

                let item = await model.findById(args.name, null, myOptions);

                if (item)
                    await item.updateAttribute(
                        collection.property,
                        args.name,
                        myOptions
                    );
            }

            await fs.unlink(srcPath);
            await tx.commit();
            return newImage;
        } catch (e) {
            await tx.rollback();
            throw e;
        }
    };
};
