'use strict';

const Indexer = require('./indexer/indexer');
const libmime = require('libmime');
const punycode = require('punycode/');
const iconv = require('iconv-lite');

module.exports.systemFlagsFormatted = ['\\Answered', '\\Flagged', '\\Draft', '\\Deleted', '\\Seen'];
module.exports.systemFlags = ['\\answered', '\\flagged', '\\draft', '\\deleted', '\\seen'];

const utf7encode = str => iconv.encode(str, 'utf-7-imap').toString();
const utf7decode = str => iconv.decode(Buffer.from(str), 'utf-7-imap').toString();

module.exports.utf7encode = utf7encode;
module.exports.utf7decode = utf7decode;

module.exports.fetchSchema = {
    body: [
        true,
        {
            type: /^(\d+\.)*(CONTENT|HEADER|HEADER\.FIELDS|HEADER\.FIELDS\.NOT|TEXT|MIME|\d+)$/i,
            headers: /^(\d+\.)*(HEADER\.FIELDS|HEADER\.FIELDS\.NOT)$/i,
            startFrom: 'optional',
            maxLength: 'optional'
        }
    ],
    bodystructure: true,
    envelope: true,
    flags: true,
    internaldate: true,
    rfc822: true,
    'rfc822.header': true,
    'rfc822.size': true,
    'rfc822.text': true,
    modseq: true,
    uid: true
};

module.exports.searchSchema = {
    charset: ['string'],
    all: true,
    answered: true,
    bcc: ['string'],
    before: ['date'],
    body: ['string'],
    cc: ['string'],
    deleted: true,
    draft: true,
    flagged: true,
    from: ['string'],
    header: ['string', 'string'],
    keyword: ['string'],
    larger: ['number'],
    modseq: [['string', 'string', 'number'], ['number']],
    new: true,
    not: ['expression'],
    old: true,
    on: ['date'],
    or: ['expression', 'expression'],
    recent: true,
    seen: true,
    sentbefore: ['date'],
    senton: ['date'],
    sentsince: ['date'],
    since: ['date'],
    smaller: ['number'],
    subject: ['string'],
    text: ['string'],
    to: ['string'],
    uid: ['sequence'],
    unanswered: true,
    undeleted: true,
    undraft: true,
    unflagged: true,
    unkeyword: ['string'],
    unseen: true
};

module.exports.searchMapping = {
    all: {
        key: 'all',
        value: [true]
    },
    answered: {
        key: 'flag',
        value: ['\\Answered', true]
    },
    bcc: {
        key: 'header',
        value: ['bcc', '$1']
    },
    before: {
        key: 'internaldate',
        value: ['<', '$1']
    },
    cc: {
        key: 'header',
        value: ['cc', '$1']
    },
    deleted: {
        key: 'flag',
        value: ['\\Deleted', true]
    },
    draft: {
        key: 'flag',
        value: ['\\Draft', true]
    },
    flagged: {
        key: 'flag',
        value: ['\\Flagged', true]
    },
    from: {
        key: 'header',
        value: ['from', '$1']
    },
    keyword: {
        key: 'flag',
        value: ['$1', true]
    },
    larger: {
        key: 'size',
        value: ['>', '$1']
    },
    new: {
        key: 'flag',
        value: ['\\Recent', true, '\\Seen', false]
    },
    old: {
        key: 'flag',
        value: ['\\Recent', false]
    },
    on: {
        key: 'internaldate',
        value: ['=', '$1']
    },
    recent: {
        key: 'flag',
        value: ['\\Recent', true]
    },
    seen: {
        key: 'flag',
        value: ['\\Seen', true]
    },
    sentbefore: {
        key: 'date',
        value: ['<', '$1']
    },
    senton: {
        key: 'date',
        value: ['=', '$1']
    },
    sentsince: {
        key: 'date',
        value: ['>=', '$1']
    },
    since: {
        key: 'internaldate',
        value: ['>=', '$1']
    },
    smaller: {
        key: 'size',
        value: ['<', '$1']
    },
    subject: {
        key: 'header',
        value: ['subject', '$1']
    },
    to: {
        key: 'header',
        value: ['to', '$1']
    },
    unanswered: {
        key: 'flag',
        value: ['\\Answered', false]
    },
    undeleted: {
        key: 'flag',
        value: ['\\Deleted', false]
    },
    undraft: {
        key: 'flag',
        value: ['\\Draft', false]
    },
    unflagged: {
        key: 'flag',
        value: ['\\Flagged', false]
    },
    unkeyword: {
        key: 'flag',
        value: ['$1', false]
    },
    unseen: {
        key: 'flag',
        value: ['\\Seen', false]
    }
};

/**
 * Checks if a sequence range string is valid or not
 *
 * @param {range} range Sequence range, eg "1,2,3:7"
 * @returns {Boolean} True if the string looks like a sequence range
 */
module.exports.validateSequnce = function (range) {
    return !!(range.length && /^(\d+|\*)(:\d+|:\*)?(,(\d+|\*)(:\d+|:\*)?)*$/.test(range));
};

module.exports.normalizeMailbox = function (mailbox, utf7Encoded) {
    if (!mailbox) {
        return '';
    }

    // trim slashes
    mailbox = mailbox.replace(/^\/|\/$/g, () => '');

    // Normalize case insensitive INBOX to always use uppercase
    let parts = mailbox.split('/');
    if (parts[0].toUpperCase() === 'INBOX') {
        parts[0] = 'INBOX';
    }

    if (utf7Encoded) {
        parts = parts.map(value => utf7decode(value));
    }

    mailbox = parts.join('/');

    return mailbox;
};

module.exports.generateFolderListing = function (folders, skipHierarchy) {
    let items = new Map();
    let parents = [];

    folders.forEach(folder => {
        let item;

        if (typeof folder === 'string') {
            folder = {
                path: folder
            };
        }

        if (!folder || typeof folder !== 'object') {
            return;
        }

        let path = module.exports.normalizeMailbox(folder.path);
        let parent, parentPath;
        if (!path) {
            return;
        }
        parent = path.split('/');
        parent.pop();

        while (parent.length) {
            parentPath = parent.join('/');
            if (parent && parents.indexOf(parentPath) < 0) {
                parents.push(parentPath);
            }
            parent.pop();
        }

        item = {
            // flags array is used to store permanentflags
            //flags: [].concat(folder.flags || []),

            flags: [],
            path
        };

        if (typeof folder.specialUse === 'string' && folder.specialUse) {
            item.specialUse = folder.specialUse;
        }

        items.set(path, item);
    });

    // ensure INBOX
    if (!items.has('INBOX')) {
        items.set('INBOX', {
            path: 'INBOX',
            flags: []
        });
    }

    // Adds \HasChildren flag for parent folders
    parents.forEach(path => {
        if (!items.has(path) && !skipHierarchy) {
            // add virtual hierarchy folders
            items.set(path, {
                flags: ['\\Noselect'],
                path
            });
        }
        let parent = items.get(path);

        if (parent && parent.flags.indexOf('\\HasChildren') < 0) {
            parent.flags.push('\\HasChildren');
        }
    });

    // converts cache Map to a response array
    let result = [];
    items.forEach(folder => {
        // Adds \HasNoChildren flag for leaf folders
        if (folder.flags.indexOf('\\HasChildren') < 0 && folder.flags.indexOf('\\HasNoChildren') < 0) {
            folder.flags.push('\\HasNoChildren');
        }
        result.push(folder);
    });

    // sorts folders
    result.sort((a, b) => {
        let aParts = a.path.split('/');
        let bParts = b.path.split('/');
        for (let i = 0; i < aParts.length; i++) {
            if (!bParts[i]) {
                return 1;
            }
            if (aParts[i] !== bParts[i]) {
                // prefer INBOX when sorting
                if (i === 0 && aParts[i] === 'INBOX') {
                    return -1;
                } else if (i === 0 && bParts[i] === 'INBOX') {
                    return 1;
                }
                return aParts[i].localeCompare(bParts[i]);
            }
        }
        return 0;
    });

    return result;
};

module.exports.filterFolders = function (folders, query) {
    query = query
        // remove excess * and %
        .replace(/\*\*+/g, '*')
        .replace(/%%+/g, '%')
        // escape special characters
        .replace(/([\\^$+?!.():=[\]|,-])/g, '\\$1')
        // setup *
        .replace(/[*]/g, '.*')
        // setup %
        .replace(/[%]/g, '[^/]*');

    let regex = new RegExp('^' + query + '$', '');

    return folders.filter(folder => !!regex.test(folder.path));
};

module.exports.getMessageRange = function (uidList, range, isUid) {
    range = (range || '').toString();

    let result = [];
    let rangeParts = range.split(',');
    let uid, i, len;
    let totalMessages = uidList.length;
    let maxUid = 0;

    let inRange = (nr, ranges, total) => {
        let range, from, to;
        for (let i = 0, len = ranges.length; i < len; i++) {
            range = ranges[i];
            to = range.split(':');
            from = to.shift();
            if (from === '*') {
                from = total;
            }
            from = Number(from) || 1;
            to = to.pop() || from;
            to = Number((to === '*' && total) || to) || from;

            if (nr >= Math.min(from, to) && nr <= Math.max(from, to)) {
                return true;
            }
        }
        return false;
    };

    for (i = 0, len = uidList.length; i < len; i++) {
        if (uidList[i] > maxUid) {
            maxUid = uidList[i];
        }
    }

    for (i = 0, len = uidList.length; i < len; i++) {
        uid = uidList[i] || 1;
        if (inRange(isUid ? uid : i + 1, rangeParts, isUid ? maxUid : totalMessages)) {
            result.push(uidList[i]);
        }
    }

    return result;
};

module.exports.packMessageRange = function (uidList) {
    if (!Array.isArray(uidList)) {
        uidList = [].concat(uidList || []);
    }

    if (!uidList.length) {
        return '';
    }

    uidList.sort((a, b) => a - b);

    let last = uidList[uidList.length - 1];
    let result = [[last]];
    for (let i = uidList.length - 2; i >= 0; i--) {
        if (uidList[i] === uidList[i + 1] - 1) {
            result[0].unshift(uidList[i]);
            continue;
        }
        result.unshift([uidList[i]]);
    }

    result = result.map(item => {
        if (item.length === 1) {
            return item[0];
        }
        return item.shift() + ':' + item.pop();
    });

    return result.join(',');
};

/**
 * Returns a date in GMT timezone
 *
 * @param {Date} date Date object to parse
 * @returns {String} Internaldate formatted date
 */
module.exports.formatInternalDate = function (date) {
    let day = date.getUTCDate(),
        month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][date.getUTCMonth()],
        year = date.getUTCFullYear(),
        hour = date.getUTCHours(),
        minute = date.getUTCMinutes(),
        second = date.getUTCSeconds(),
        tz = 0, //date.getTimezoneOffset(),
        tzHours = Math.abs(Math.floor(tz / 60)),
        tzMins = Math.abs(tz) - tzHours * 60;

    return (
        (day < 10 ? '0' : '') +
        day +
        '-' +
        month +
        '-' +
        year +
        ' ' +
        (hour < 10 ? '0' : '') +
        hour +
        ':' +
        (minute < 10 ? '0' : '') +
        minute +
        ':' +
        (second < 10 ? '0' : '') +
        second +
        ' ' +
        (tz > 0 ? '-' : '+') +
        (tzHours < 10 ? '0' : '') +
        tzHours +
        (tzMins < 10 ? '0' : '') +
        tzMins
    );
};

/**
 * Converts query data and message into an array of query responses.
 *
 * Message object must have the following properties:
 *
 *   * raw – string (binary) or buffer with the rfc822 contents of the message
 *   * uid – message UID
 *   * flags - an array with message flags
 *   * date - internaldate date object
 *
 * Additionally the message object *should* have the following  properties (if not present then generated automatically):
 *
 *   * mimeTree - message MIME tree object
 *   * envelope - message IMAP envelope object
 *   * bodystructure - message bodustructure object
 *   * bodystructureShort - bodyscructure for the BODY query
 *
 * @param {Array} query Query objects
 * @param {Object} message Message object
 * @param {Object} options Options for the indexer
 * @returns {Array} Resolved responses
 */
module.exports.getQueryResponse = function (query, message, options) {
    options = options || {};

    // for optimization purposes try to use cached mimeTree etc. if available
    // If these values are missing then generate these when first time required
    // So if the query is for (UID FLAGS) then mimeTree is never generated
    let mimeTree = message.mimeTree;
    let indexer = new Indexer(options);

    // generate response object
    let values = [];
    query.forEach(item => {
        let value = '';
        switch (item.item) {
            case 'uid':
                value = message.uid;
                break;

            case 'modseq':
                value = message.modseq;
                break;

            case 'flags':
                value = message.flags;
                break;

            case 'internaldate':
                if (!message.idate) {
                    message.idate = new Date();
                }
                value = message.idate;
                break;

            case 'bodystructure': {
                if (message.bodystructure) {
                    value = message.bodystructure;
                } else {
                    if (!mimeTree) {
                        mimeTree = indexer.parseMimeTree(message.raw);
                    }
                    value = indexer.getBodyStructure(mimeTree);
                }

                let walk = arr => {
                    arr.forEach((entry, i) => {
                        if (Array.isArray(entry)) {
                            return walk(entry);
                        }
                        if (!entry || typeof entry !== 'object') {
                            return;
                        }
                        let val = entry;
                        if (!Buffer.isBuffer(val) && val.buffer) {
                            val = val.buffer;
                        }
                        arr[i] = libmime.encodeWords(val.toString(), false, Infinity);
                    });
                };

                if (!options.acceptUTF8Enabled) {
                    walk(value);
                }

                break;
            }
            case 'envelope':
                if (message.envelope) {
                    value = message.envelope;
                } else {
                    if (!mimeTree) {
                        mimeTree = indexer.parseMimeTree(message.raw);
                    }
                    value = indexer.getEnvelope(mimeTree);
                }
                if (!options.acceptUTF8Enabled) {
                    // encode unicode values

                    // subject
                    value[1] = libmime.encodeWords(value[1], false, Infinity);

                    for (let i = 2; i < 8; i++) {
                        if (value[i] && Array.isArray(value[i])) {
                            value[i].forEach(addr => {
                                if (addr[0] && typeof addr[0] === 'object') {
                                    // name
                                    let val = addr[0];
                                    if (!Buffer.isBuffer(val) && val.buffer) {
                                        val = val.buffer;
                                    }
                                    addr[0] = libmime.encodeWords(val.toString(), false, Infinity);
                                }

                                if (addr[2] && typeof addr[2] === 'object') {
                                    // username
                                    let val = addr[2];
                                    if (!Buffer.isBuffer(val) && val.buffer) {
                                        val = val.buffer;
                                    }
                                    addr[2] = libmime.encodeWords(val.toString(), false, Infinity);
                                }

                                if (addr[3] && typeof addr[3] === 'object') {
                                    // domain
                                    let val = addr[3];
                                    if (!Buffer.isBuffer(val) && val.buffer) {
                                        val = val.buffer;
                                    }
                                    try {
                                        addr[3] = punycode.toASCII(val.toString());
                                    } catch (E) {
                                        addr[3] = val.toString();
                                    }
                                }
                            });
                        }
                    }

                    // libmime.encodeWords(value, false, Infinity)
                }
                break;

            case 'rfc822':
                if (!mimeTree) {
                    mimeTree = indexer.parseMimeTree(message.raw);
                }
                value = indexer.getContents(mimeTree);
                break;

            case 'rfc822.size':
                if (message.size) {
                    value = message.size;
                } else {
                    if (!mimeTree) {
                        mimeTree = indexer.parseMimeTree(message.raw);
                    }
                    value = indexer.getSize(mimeTree);
                }
                break;

            case 'rfc822.header':
                // Equivalent to BODY[HEADER]
                if (!mimeTree) {
                    mimeTree = indexer.parseMimeTree(message.raw);
                }
                value = [].concat(mimeTree.header || []).join('\r\n') + '\r\n\r\n';
                break;

            case 'rfc822.text':
                // Equivalent to BODY[TEXT]
                if (!mimeTree) {
                    mimeTree = indexer.parseMimeTree(message.raw);
                }
                value = indexer.getContents(mimeTree, {
                    path: '',
                    type: 'text'
                });
                break;

            case 'body':
                if (!item.hasOwnProperty('type')) {
                    // BODY
                    if (!mimeTree) {
                        mimeTree = indexer.parseMimeTree(message.raw);
                    }
                    value = indexer.getBody(mimeTree);
                } else if (item.path === '' && item.type === 'content') {
                    // BODY[]
                    if (!mimeTree) {
                        mimeTree = indexer.parseMimeTree(message.raw);
                    }
                    value = indexer.getContents(mimeTree, false, {
                        startFrom: item.partial && item.partial.startFrom,
                        maxLength: item.partial && item.partial.maxLength
                    });
                } else {
                    // BODY[SELECTOR]
                    if (!mimeTree) {
                        mimeTree = indexer.parseMimeTree(message.raw);
                    }
                    value = indexer.getContents(mimeTree, item, {
                        startFrom: item.partial && item.partial.startFrom,
                        maxLength: item.partial && item.partial.maxLength
                    });
                }

                if (item.partial) {
                    let len;

                    if (value && value.type === 'stream') {
                        value.startFrom = item.partial.startFrom;
                        value.maxLength = item.partial.maxLength;
                        len = value.expectedLength;
                    } else {
                        value = value.toString('binary').substr(item.partial.startFrom, item.partial.maxLength);
                        len = value.length;
                    }

                    // If start+length is larger than available value length, then do not return the length value
                    // Instead of BODY[]<10.20> return BODY[]<10> which means that the response is from offset 10 to the end
                    if (item.original.partial.length === 2 && item.partial.maxLength - item.partial.startFrom > len) {
                        item.original.partial.pop();
                    }
                }

                break;
        }
        values.push(value);
    });

    return values;
};

/**
 * Builds and emits an untagged CAPABILITY response depending on current state
 *
 * @param {Object} connection IMAP connection object
 */
module.exports.sendCapabilityResponse = connection => {
    let capabilities = [];

    if (!connection.secure) {
        if (!connection._server.options.disableSTARTTLS) {
            capabilities.push('STARTTLS');
            if (!connection._server.options.ignoreSTARTTLS) {
                capabilities.push('LOGINDISABLED');
            }
        }
    }

    if (connection.state === 'Not Authenticated') {
        capabilities.push('AUTH=PLAIN');
        capabilities.push('AUTH=PLAIN-CLIENTTOKEN');
        capabilities.push('SASL-IR');
        capabilities.push('ENABLE');

        capabilities.push('ID');
        capabilities.push('UNSELECT');
        capabilities.push('IDLE');
        capabilities.push('NAMESPACE');
        capabilities.push('QUOTA');
        capabilities.push('XLIST');
        capabilities.push('CHILDREN');
    } else {
        capabilities.push('ID');
        capabilities.push('UNSELECT');
        capabilities.push('IDLE');
        capabilities.push('NAMESPACE');
        capabilities.push('QUOTA');
        capabilities.push('XLIST');
        capabilities.push('CHILDREN');

        capabilities.push('SPECIAL-USE');
        capabilities.push('UIDPLUS');
        capabilities.push('UNSELECT');
        capabilities.push('ENABLE');
        capabilities.push('CONDSTORE');
        capabilities.push('UTF8=ACCEPT');

        capabilities.push('MOVE');

        if (connection._server.options.enableCompression) {
            capabilities.push('COMPRESS=DEFLATE');
        }

        if (connection._server.options.maxMessage) {
            capabilities.push('APPENDLIMIT=' + connection._server.options.maxMessage);
        }
    }

    capabilities.sort((a, b) => a.localeCompare(b));

    connection.send('* CAPABILITY ' + ['IMAP4rev1'].concat(capabilities).join(' '));
};

module.exports.validateInternalDate = internaldate => {
    if (!internaldate || typeof internaldate !== 'string') {
        return false;
    }
    return /^([ \d]?\d)-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(\d{4}) (\d{2}):(\d{2}):(\d{2}) ([-+])(\d{2})(\d{2})$/i.test(internaldate);
};

module.exports.validateSearchDate = internaldate => {
    if (!internaldate || typeof internaldate !== 'string') {
        return false;
    }
    return /^\d{1,2}-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-\d{4}$/i.test(internaldate);
};
