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

const imageTypes = ['PNG', 'GIF', 'BMP', 'JPEG', 'TIFF'];
const videoTypes = ['AVI', 'MP4', 'MOV', 'MKV', 'WEBM', 'OGV'];
const soundTypes = ['OGG', 'WAV', 'MP3', 'FLAC'];

const read = async (path, offset, length) => {
    const fp = await fs.open(path);
    const buff = Buffer.alloc(length);
    
    await fs.read(fd, buff, 0, length, offset);
    await fs.close();

    return buff;
};


const parser = {

    PNG: async (path, result) => {
        const buff = await read(path, 18, 6);
        result.width = buff.readUInt16BE(0);
        result.height = buff.readUInt16BE(4);
    },

    GIF: async (path, result) => {
        const buff = await read(path, 6, 4);
        result.width = buff.readUInt16LE(0);
        result.height = buff.readUInt16LE(2);
    },

    BMP: async (path, result) => {
        const buff = await read(path, 18, 8);
        result.width = buff.readUInt32LE(0);
        result.height = buff.readUInt32LE(4);
    },

    JPEG: async (path, result) => {
        const sof = [0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf, 0xde];
        const buff = await read(path, 2, 64000);
        
        let pos = 0;

        while (buff[pos++] == 0xff) {
            let marker = buff[pos++];
            let size = buff.readUInt16BE(pos);

            if (marker == 0xda) break;

            if (sof.includes(marker)) {
                result.height = buff.readUInt16BE(pos + 3);
                result.width = buff.readUInt16BE(pos + 5);
                break;
            }

            pos += size;
        }
    },

    TIFF: async (path, result) => {
        const buff = await read(path, 0, 64000);
        const le = buff.toString('ascii', 0, 2) == 'II';
        let pos = 0;
        const readUInt16 = () => { pos += 2; return buff[le ? 'readUInt16LE' : 'readUInt16BE'](pos - 2); }
        const readUInt32 = () => { pos += 4; return buff[le ? 'readUInt32LE' : 'readUInt32BE'](pos - 4); }

        let offset = readUInt32();
        
        while (pos < buff.length && offset > 0) {
            let entries = readUInt16(offset);
            let start = pos;

            for (let i = 0; i < entries; i++) {
                let tag = readUInt16();
                let type = readUInt16();
                let length = readUInt32();
                let data = (type == 3) ? readUInt16() : readUInt32();
                if (type == 3) pos += 2;

                if (tag == 256) {
                    result.width = data;
                } else if (tag == 257) {
                    result.height = data;
                }

                if (result.width > 0 && result.height > 0) {
                    return;
                }
            }

            offset = readUInt32();
            pos += offset;
        }
    },

    AVI: async (path, result) => {
        const buff = await read(path, 0, 144);
        result.width = buff.readUInt32LE(64);
        result.height = buff.readUInt32LE(68);
        result.duration = ~~(buff.readUInt32LE(128) / buff.readUInt32LE(132) * buff.readUInt32LE(140));
    },

    MP4: async (path, result) => {
        return parser.MOV(path, result);
    },

    MOV: async (path, result, pos = 0) => {
        const buff = await read(path, 0, 64000);

        while (pos < buff.length) {
            let size = buff.readUInt32BE(pos);
            let name = buff.toString('ascii', pos + 4, 4);

            if (name == 'mvhd') {
                let scale = buff.readUInt32BE(pos + 20);
                let duration = buff.readUInt32BE(pos + 24);
                result.duration = ~~(duration / scale);
            }

            if (name == 'tkhd') {
                let m0 = buff.readUInt32BE(pos + 48);
                let m4 = buff.readUInt32BE(pos + 64);
                let w = buff.readUInt32BE(pos + 84);
                let h = buff.readUInt32BE(pos + 88);
                if (w > 0 && h > 0) {
                    result.width = w / m0;
                    result.height = h / m4;
                    return;
                }
            }

            if (name == 'moov' || name == 'trak') {
                await parser.MOV(path, pos + 8);
            }

            pos += size;
        }
    },

    WEBM: async (path, result) => {
        return parser.EBML(path, result);
    },

    MKV: async (path, result) => {
        return parser.EBML(path, result);
    },

    EBML: async (path, result) => {
        const containers = ['\x1a\x45\xdf\xa3', '\x18\x53\x80\x67', '\x15\x49\xa9\x66', '\x16\x54\xae\x6b', '\xae', '\xe0'];
        const buff = await read(path, 0, 64000);

        // TODO parse EBML
    },

    OGV: async (path, result) => {
        return parser.OGG(path, result);
    },

    OGG: async (path, result) => {
        const buff = await read(apth, 0, 64000);
        let pos = 0, vorbis;

        while (buff.toString('ascii', pos, pos + 4) == 'OggS') {
            let version = buff[pos + 4];
            let b = buff[pos + 5];
            let continuation = !!(b & 0x01);
            let bos = !!(b & 0x02);
            let eos = !!(b & 0x04);
            let position = Number(buff.readBigUInt64LE(pos + 6));
            let serial = buff.readUInt32LE(pos + 14);
            let pageNumber = buff.readUInt32LE(pos + 18);
            let checksum = buff.readUInt32LE(pos + 22);
            let pageSegments = buff[path + 26];
            let lacing = buff.slice(pos + 27, pos + 27 + pageSegments);
            let pageSize = lacing.reduce((p, v) => p + v, 0);
            let start = pos + 27 + pageSegments;
            let pageHeader = buff.slice(start, start + 7);

            if (pageHeader.compare(Buffer.from([0x01, 'v', 'o', 'r', 'b', 'i', 's']))) {
                vorbis = { serial, sampleRate: buff.readUInt32LE(start + 12) };
            }

            if (pageHeader.compare(Buffer.from([0x80, 't', 'h', 'e', 'o', 'r', 'a']))) {
                let version = buff.slice(start + 7, start + 10);
                result.width = buff.readUInt16BE(start + 10) << 4;
                result.height = buff.readUInt16BE(start + 12) << 4;

                if (version >= 0x030200) {
                    let width = buff.slice(start + 14, start + 17);
                    let height = buff.slice(start + 17, start + 20);

                    if (width <= result.width && width > result.width - 16 && height <= result.height && height > result.height - 16) {
                        result.width = width;
                        result.height = height;
                    }
                }
            }

            if (eos && vorbis && serail == vorbis.serial) {
                result.duration = ~~(position / vorbis.sampleRate);
            }

            pos = start + pageSize;
        }
    },

    WAV: async (path, result) => {
        const buff = await read(path, 0, 32);
        let size = buff.readUInt32LE(4);
        let rate = buff.readUInt32LE(28);
        result.duration = ~~(size / rate);
    },

    MP3: async (path, result) => {
        const versions = [2.5, 0, 2, 1];
        const layers = [0, 3, 2, 1];
        const bitrates = [
            [ // version 2.5
                [0, 0, 0, 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0], // reserved
                [0, 8,16,24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160], // layer 3
                [0, 8,16,24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160], // layer 2
                [0,32,48,56, 64, 80, 96,112,128,144,160,176,192,224,256]  // layer 1
            ],
            [ // reserved
                [0, 0, 0, 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0], // reserved
                [0, 0, 0, 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0], // reserved
                [0, 0, 0, 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0], // reserved
                [0, 0, 0, 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0]  // reserved
            ],
            [ // version 2
                [0, 0, 0, 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0], // reserved
                [0, 8,16,24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160], // layer 3
                [0, 8,16,24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160], // layer 2
                [0,32,48,56, 64, 80, 96,112,128,144,160,176,192,224,256]  // layer 1
            ],
            [ // version 1
                [0, 0, 0, 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0], // reserved
                [0,32,40,48, 56, 64, 80, 96,112,128,160,192,224,256,320], // layer 3
                [0,32,48,56, 64, 80, 96,112,128,160,192,224,256,320,384], // layer 2
                [0,32,64,96,128,160,192,224,256,288,320,352,384,416,448]  // layer 1
            ]
        ]
        const srates = [
            [11025, 12000,  8000, 0], // mpeg 2.5
            [    0,     0,     0, 0], // reserved
            [22050, 24000, 16000, 0], // mpeg 2
            [44100, 48000, 32000, 0]  // mpeg 1
        ]
        const tsamples = [
            [0,  576, 1152, 384], // mpeg 2.5
            [0,    0,    0,   0], // reserved
            [0,  576, 1152, 384], // mpeg 2
            [0, 1152, 1152, 384]  // mpeg 1
        ];
        const slotSizes = [0, 1, 1, 4];
        const modes = ['stereo', 'joint_stereo', 'dual_channel', 'mono'];
        const buff = await read(path, 0, 64000);

        let duration = 0;
        let count = 0;
        let skip = 0;
        let pos = 0;

        while (pos < buff.length) {
            let start = pos;

            if (buff.toString('ascii', pos, 4) == 'TAG+') {
                skip += 227;
                pos += 227;
            } else if (buff.toString('ascii', pos, 3) == 'TAG') {
                skip += 128;
                pos += 128;
            } else if (buff.toString('ascii', pos, 3) == 'ID3') {
                let bytes = buff.readUInt32BE(pos + 6);
                let size = 10 + (bytes[0] << 21 | bytes[1] << 14 | bytes[2] << 7 | bytes[3]);
                skip += size;
                pos += size;
            } else {
                let hdr = buff.slice(pos, pos + 4);

                while (pos < buff.length && !(hdr[0] == 0xff && (hdr[1] & 0xe0) == 0xe0)) {
                    pos++;
                    hdr = buff.slice(pos, pos + 4);
                }

                let ver = (hdr[1] & 0x18) >> 3;
                let lyr = (hdr[1] & 0x06) >> 1;
                let pad = (hdr[2] & 0x02) >> 1;
                let brx = (hdr[2] & 0xf0) >> 4;
                let srx = (hdr[2] & 0x0c) >> 2;
                let mdx = (hdr[3] & 0xc0) >> 6;

                let version = versions[ver];
                let layer = layers[lyr];
                let bitrate = bitrates[ver][lyr][brx] * 1000;
                let samprate = srates[ver][srx];
                let samples = tsamples[ver][lyr];
                let slotSize = slotSizes[lyr];
                let mode = modes[mdx];
                let fsize = ~~(((samples / 8 * bitrate) / samprate) + (pad ? slotSize : 0));

                count++;

                if (count == 1) {
                    if (layer != 3) {
                        pos += 2;
                    } else {
                        if (mode != 'mono') {
                            if (version == 1) {
                                pos += 32;
                            } else {
                                pos += 17;
                            }
                        } else {
                            if (version == 1) {
                                pos += 17;
                            } else {
                                pos += 9;
                            }
                        }
                    }

                    if (buff.toString('ascii', pos, pos + 4) == 'Xing' && (buff.readUInt32BE(pos + 4) & 0x0001) == 0x0001) {
                        let totalFrames = buff.readUInt32BE(pos + 8);
                        duration = totalFrames * samples / samprate;
                        break;
                    }
                }

                if (fsize < 1) break;

                pos = start + fsize;

                duration += (samples / samprate);
            }
        }
        
        result.duration = ~~duration;
    },

    FLAC: async (path, result) => {
        const buff = await read(path, 18, 8);
        let rate = (buff[0] << 12) | (buff[1] << 4) | ((buff[2] & 0xf0) >> 4);
        let size = ((buff[3] & 0x0f) << 32) | (buff[4] << 24) | (buff[5] << 16) | (buff[6] << 8) | buff[7];
        result.duration = ~~(size / rate);
    },

};

async function detect(path) {
    const buff = await read(path, 0, 12);

    if (buff.slice(0, 8).compare(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) {
        return 'PNG';
    }

    if (buff.toString('ascii', 0, 3) == 'GIF') {
        return 'GIF';
    }

    if (buff.toString('ascii', 0, 2) == 'BM') {
        return 'BMP';
    }

    if (buff.slice(0, 2).compare(Buffer.from([0xff, 0xd8]))) {
        return 'JPEG';
    }

    if (buff.toString('ascii', 0, 2) == 'II' && buff.readUInt16LE(2) == 42) {
        return 'TIFF';
    }

    if (buff.toString('ascii', 0, 2) == 'MM' && buff.readUInt16BE(2) == 42) {
        return 'TIFF';
    }

    if (buff.toString('ascii', 0, 4) == 'RIFF' && buff.toString('ascii', 8, 4) == 'AVI ') {
        return 'AVI';
    }

    if (buff.toString('ascii', 4, 4) == 'ftyp') {
        return 'MP4';
    }

    if (buff.toString('ascii', 4, 4) == 'moov') {
        return 'MOV';
    }

    if (buff.slice(0, 4).compare(Buffer.from([0x1a, 0x45, 0xdf, 0xa3]))) {
        // TODO detect MKV

        return 'EBML';
    }

    if (buff.toString('ascii', 0, 4) == 'OggS') {
        // TODO detect OGV
        
        return 'OGG'
    }

    if (buff.toString('ascii', 0, 4) == 'RIFF', buff.toString('ascii', 8, 4) == 'WAVE') {
        return 'WAV';
    }

    if (buff.toString('ascii', 0, 3) == 'ID3' || (buf[0] == 0xff && (buff[1] & 0xe0))) {
        return 'MP3';
    }

    if (buff.toString('ascii', 0, 4) == 'fLaC') {
        return 'FLAC';
    }

    return null
}

module.exports = {

    detect: async function(options) {
        let path = this.parseRequired(options.path, 'string', 'metadata.detect: path is required.');

        return detect(path);
    },

    isImage: async function(options) {
        let path = this.parseRequired(options.path, 'string', 'metadata.isImage: path is required.');
        let type = await detect(path);
        let cond = imageTypes.includes(type);
        
        if (cond) {
            if (options.then) {
                await this.exec(options.then, true);
            }
        } else if (options.else) {
            await this.exec(options.else, true);
        }

        return cond;
    },

    isVideo: async function(options) {
        let path = this.parseRequired(options.path, 'string', 'metadata.isVideo: path is required.');
        let type = await detect(path);
        let cond = videoTypes.includes(type);
        
        if (cond) {
            if (options.then) {
                await this.exec(options.then, true);
            }
        } else if (options.else) {
            await this.exec(options.else, true);
        }

        return cond;
    },

    isSound: async function(options) {
        let path = this.parseRequired(options.path, 'string', 'metadata.isSound: path is required.');
        let type = await detect(path);
        let cond = soundTypes.includes(type);
        
        if (cond) {
            if (options.then) {
                await this.exec(options.then, true);
            }
        } else if (options.else) {
            await this.exec(options.else, true);
        }

        return cond;
    },

    fileinfo: async function(options) {
        let path = this.parseRequired(options.path, 'string', 'metadata.fileinfo: path is required.');
        let type = await detect(path);
        let result = { type, width: null, height: null, duration: null };

        if (parser[type]) {
            await parser[type](path, result);
        }

        return result;
    },

};