import {
    Entity,
    PrimaryGeneratedColumn,
    Column,
    ManyToOne,
    OneToMany
} from "typeorm";
import BaseEntity from "./base.entity";
import User from "./user.entity";
import * as Jimp from "jimp";
import { v4 } from "uuid";
const uuid = v4;
import { app } from "../app";

import * as fs from 'fs';
import { lookup } from 'mime-types';

export interface ResizeConfig {
    rotate: number;
    scaleX: number;
    scaleY: number;
    x: number;
    y: number;
    width: number;
    height: number;
}

@Entity("media")
export default class Media extends BaseEntity {
    hiddenFields = ["owner", "_children", "children", "parent"];

    @PrimaryGeneratedColumn() id: number;
    @Column() originalName: string;
    @Column({ unique: true, default: null })
    slug: string;
    @Column({ type: "smallint", default: 0 })
    private is_directory: number;

    get isDirectory(): boolean {
        return this.is_directory == 1;
    }

    set isDirectory(value: boolean) {
        if (value) {
            this.is_directory = 1;
        } else {
            this.is_directory = 0;
        }
    }

    @Column({ nullable: true }) mimetype: string;
    @Column({ nullable: true }) size: number;
    @Column({ nullable: true }) fileName: string;
    @Column({ nullable: true }) path: string;

    @ManyToOne(_ => Media, media => media._children)
    parent: Media;

    @OneToMany(_ => Media, media => media.parent)
    _children: Media[];

    @ManyToOne(_ => User, user => user.media, { eager: true })
    owner: User;

    @Column({ nullable: true })
    width: number;
    @Column({ nullable: true })
    height: number;

    get children(): Promise<Media[]> {
        return Media.find({ where: { parent: this } });
    }

    async remove(): Promise<this> {
        if (this.isDirectory) {
            for (let child of await this.children) {
                await child.remove();
            }
            return super.remove();
        }
        let k = await super.remove();
        app.config.ufs.unlink(this.path, () => { });
        return k;
    }

    /**
     * Create a cropped image starting of the current.
     * The image will be placed in the same directory as the original.
     * @param config: The configuration object containing the crop parameters
     * @return a promise with the new @type Media
     */
    async resizeToNewEntity(config: ResizeConfig): Promise<Media> {
        let cachedPath = await app.config.ufs.getToCache(
            this.fileName,
            app.config.cachePath
        );
        let img = await Jimp.read(cachedPath);
        img = await img.rotate(config.rotate);
        img = await img.resize(
            img.getWidth() * Math.round(config.scaleX),
            img.getHeight() * Math.round(config.scaleY)
        );
        img = await img.crop(Math.round(config.x), Math.round(config.y), Math.round(config.width), Math.round(config.height));
        let newFileName = uuid();
        let path = app.config.cachePath;
        await img.writeAsync(path + "/" + newFileName);
        await app.config.ufs.uploadFileFromCache(newFileName, path);
        let newMedia = new Media();
        newMedia.isDirectory = false;
        newMedia.originalName = copiedName(this.originalName);
        newMedia.mimetype = this.mimetype;
        let stat = await app.config.ufs.stat(newFileName);
        newMedia.size = stat.size;
        newMedia.fileName = newFileName;
        newMedia.path = newFileName;
        newMedia.owner = this.owner;
        newMedia.parent = this.parent;
        newMedia.width = config.width;
        newMedia.height = config.height;
        return await newMedia.save();
    }

    /**
     * Generate a new MediaEntity from an uploaded file using the Multer library
     * @param uploadMedia  the multer File
     * @param user  the user performing the request (aka the onwer of the file)
     * @param directory  the (optional) virtual parent directory
     */
    static async persist(
        uploadMedia: Express.Multer.File,
        user: User,
        directory?: Media
    ): Promise<Media> {
        let m = new Media();
        uploadMedia = await app.config.ufs.uploadFile(uploadMedia);
        m.originalName = uploadMedia.originalname;
        m.isDirectory = false;
        m.mimetype = uploadMedia.mimetype;
        m.size = uploadMedia.size;
        m.fileName = uploadMedia.filename;
        m.path = uploadMedia.path;
        m.owner = user;
        if (directory) {
            m.parent = directory;
        }
        let cachedPath = await app.config.ufs.getToCache(
            m.fileName,
            app.config.cachePath
        );
        if (m.mimetype.startsWith('image')) {
            try {
                let img = await Jimp.read(cachedPath);
                m.width = img.getWidth();
                m.height = img.getHeight();
            } catch (e) {
                console.log("Error obtaining the metadata of the image.");
                console.log("Image path: " + uploadMedia.path);
                console.error(e);
            }
        }
        return m.save();
    }

    /**
     * Generate a new MediaEntity from a local temporary file
     * @param originalName the original name of the file
     * @param path the temporary path of the file
     * @param user the user perfrming the request (aka the owner of the file)
     * @param directory the (optional) virtual parent directory
     */
    static async persistTempFile(originalName: string, path: string, user: User, directory?: Media): Promise<Media> {
        let m = new Media();
        m.originalName = originalName;
        m.isDirectory = false;
        m.mimetype = lookup(path) || '';
        m.size = (await stat(path)).size;
        let uploadedFile = await app.config.ufs.uploadTempFile(path);
        m.fileName = uploadedFile.fileName;
        m.path = uploadedFile.path;
        m.owner = user;
        if (directory) {
            m.parent = directory;
        }
        return m.save();
    }

    static async mkdir(
        name: string,
        user?: User,
        directory?: Media | null
    ): Promise<Media> {
        let names = normalizePath(name).split("/");
        let prev = directory;
        for (let i = 0; i < names.length; i++) {
            let n = names[i];
            if (!prev) {
                prev = null;
            }
            let where = {
                where: {
                    originalName: n,
                    is_directory: 1,
                    parent: prev
                }
            };
            let m = await Media.findOne(where);
            if (!m) {
                m = new Media();
                m.originalName = n;
                m.isDirectory = true;
                if (user) {
                    m.owner = user;
                }
                if (prev) {
                    m.parent = prev;
                }
                await m.save();
            }
            prev = m;
        }
        return <Media>prev;
    }

    static async getFolder(path: string): Promise<Media | undefined> {
        let names = normalizePath(path).split("/");
        let prev = null;
        for (let n of names) {
            prev = await Media.findOne({
                where: {
                    originalName: n,
                    is_directory: 1,
                    parent: prev
                }
            });
            if (!prev) {
                return prev;
            }
        }
        return <Media>prev;
    }

    static findBySlug(slug: string): Promise<Media | undefined> {
        return Media.findOne({ where: { slug: slug, is_directory: 0 } });
    }

    static findBySlugOrId(key: string): Promise<Media | undefined> {
        return Media.getRepository()
            .createQueryBuilder("m")
            .where("m.slug = :slug AND is_directory = 0", { slug: key })
            .orWhere("m.id = :id AND is_directory = 0", { id: key })
            .getOne();
    }

    static findOneWithParent(id: number): Promise<Media | undefined> {
        return Media.findOne({ where: { id: id }, relations: ["parent"] });
    }
}

function normalizePath(path: string): string {
    if (path.startsWith("/")) {
        path = path.substring(1);
    }
    if (path.endsWith("/")) {
        path = path.substring(0, path.length - 1);
    }
    return path;
}

function copiedName(name: string): string {
    let index = name.lastIndexOf(".");
    if (index == -1) {
        return name + " 1";
    }
    return name.substring(0, index) + " 1" + name.substring(index);
}


function stat(path: string): Promise<fs.Stats> {
    return new Promise((ok, fail) => {
        fs.stat(path, (err, info) => {
            if (err) {
                return fail(err);
            }
            ok(info);
        });
    });
}