/* jslint node: true */
'use strict';

//  ENiGMA½
const Config = require('./config.js').get;
const miscUtil = require('./misc_util.js');
const ansi = require('./ansi_term.js');
const aep = require('./ansi_escape_parser.js');
const sauce = require('./sauce.js');
const { Errors } = require('./enig_error.js');

//  deps
const fs = require('graceful-fs');
const paths = require('path');
const assert = require('assert');
const iconv = require('iconv-lite');
const _ = require('lodash');

exports.getArt = getArt;
exports.getArtFromPath = getArtFromPath;
exports.display = display;
exports.defaultEncodingFromExtension = defaultEncodingFromExtension;

//  :TODO: Return MCI code information
//  :TODO: process SAUCE comments
//  :TODO: return font + font mapped information from SAUCE

const SUPPORTED_ART_TYPES = {
    //  :TODO: the defualt encoding are really useless if they are all the same ...
    //  perhaps .ansamiga and .ascamiga could be supported as well as overrides via conf
    '.ans': { name: 'ANSI', defaultEncoding: 'cp437', eof: 0x1a },
    '.asc': { name: 'ASCII', defaultEncoding: 'cp437', eof: 0x1a },
    '.pcb': { name: 'PCBoard', defaultEncoding: 'cp437', eof: 0x1a },
    '.bbs': { name: 'Wildcat', defaultEncoding: 'cp437', eof: 0x1a },

    '.amiga': { name: 'Amiga', defaultEncoding: 'amiga', eof: 0x1a },
    '.txt': { name: 'Amiga Text', defaultEncoding: 'cp437', eof: 0x1a },
    //  :TODO: extentions for wwiv, renegade, celerity, syncronet, ...
    //  :TODO: extension for atari
    //  :TODO: extension for topaz ansi/ascii.
};

function getFontNameFromSAUCE(sauce) {
    if (sauce && sauce.Character) {
        return sauce.Character.fontName;
    }
}

function getWidthFromSAUCE(sauce) {
    if (sauce && sauce.Character) {
        let sauceWidth = _.toNumber(sauce.Character.characterWidth);
        if(!(_.isNaN(sauceWidth)) && sauceWidth > 0) {
            return sauceWidth;
        }
    }
    return null;
}

function sliceAtEOF(data, eofMarker) {
    let eof = data.length;
    const stopPos = Math.max(data.length - 256, 0); //  256 = 2 * sizeof(SAUCE)

    for (let i = eof - 1; i > stopPos; i--) {
        if (eofMarker === data[i]) {
            eof = i;
            break;
        }
    }

    if (eof === data.length) {
        return data; //  nothing to do
    }

    //  try to prevent goofs
    if (eof < 128 && 'SAUCE00' !== data.slice(eof + 1, eof + 8).toString()) {
        return data;
    }

    return data.slice(0, eof);
}

function getArtFromPath(path, options, cb) {
    fs.readFile(path, (err, data) => {
        if (err) {
            return cb(err);
        }

        //
        //  Convert from encodedAs -> j
        //
        const ext = paths.extname(path).toLowerCase();
        const encoding = options.encodedAs || defaultEncodingFromExtension(ext);

        //  :TODO: how are BOM's currently handled if present? Are they removed? Do we need to?

        function sliceOfData() {
            if (options.fullFile === true) {
                return iconv.decode(data, encoding);
            } else {
                const eofMarker = defaultEofFromExtension(ext);
                return iconv.decode(
                    eofMarker ? sliceAtEOF(data, eofMarker) : data,
                    encoding
                );
            }
        }

        function getResult(sauce) {
            const result = {
                data: sliceOfData(),
                fromPath: path,
            };

            if (sauce) {
                result.sauce = sauce;
            }

            return result;
        }

        if (options.readSauce === true) {
            sauce.readSAUCE(data, (err, sauce) => {
                if (err) {
                    return cb(null, getResult());
                }

                //
                //  If a encoding was not provided & we have a mapping from
                //  the information provided by SAUCE, use that.
                //
                if (!options.encodedAs) {
                    /*
                    if(sauce.Character && sauce.Character.fontName) {
                        var enc = SAUCE_FONT_TO_ENCODING_HINT[sauce.Character.fontName];
                        if(enc) {
                            encoding = enc;
                        }
                    }
                    */
                }
                return cb(null, getResult(sauce));
            });
        } else {
            return cb(null, getResult());
        }
    });
}

function getArt(name, options, cb) {
    const ext = paths.extname(name);

    options.basePath = miscUtil.valueWithDefault(options.basePath, Config().paths.art);
    options.asAnsi = miscUtil.valueWithDefault(options.asAnsi, true);

    //  :TODO: make use of asAnsi option and convert from supported -> ansi

    if ('' !== ext) {
        options.types = [ext.toLowerCase()];
    } else {
        if (_.isUndefined(options.types)) {
            options.types = Object.keys(SUPPORTED_ART_TYPES);
        } else if (_.isString(options.types)) {
            options.types = [options.types.toLowerCase()];
        }
    }

    //  If an extension is provided, just read the file now
    if ('' !== ext) {
        const directPath = paths.isAbsolute(name)
            ? name
            : paths.join(options.basePath, name);
        return getArtFromPath(directPath, options, cb);
    }

    fs.readdir(options.basePath, (err, files) => {
        if (err) {
            return cb(err);
        }

        const filtered = files.filter(file => {
            //
            //  Ignore anything not allowed in |options.types|
            //
            const fext = paths.extname(file);
            if (!options.types.includes(fext.toLowerCase())) {
                return false;
            }

            const bn = paths.basename(file, fext).toLowerCase();
            if (options.random) {
                const suppliedBn = paths.basename(name, fext).toLowerCase();

                //
                //  Random selection enabled. We'll allow for
                //  basename1.ext, basename2.ext, ...
                //
                if (!bn.startsWith(suppliedBn)) {
                    return false;
                }

                const num = bn.substr(suppliedBn.length);
                if (num.length > 0) {
                    if (isNaN(parseInt(num, 10))) {
                        return false;
                    }
                }
            } else {
                //
                //  We've already validated the extension (above). Must be an exact
                //  match to basename here
                //
                if (bn != paths.basename(name, fext).toLowerCase()) {
                    return false;
                }
            }

            return true;
        });

        if (filtered.length > 0) {
            //
            //  We should now have:
            //  - Exactly (1) item in |filtered| if non-random
            //  - 1:n items in |filtered| to choose from if random
            //
            let readPath;
            if (options.random) {
                readPath = paths.join(
                    options.basePath,
                    filtered[Math.floor(Math.random() * filtered.length)]
                );
            } else {
                assert(1 === filtered.length);
                readPath = paths.join(options.basePath, filtered[0]);
            }

            return getArtFromPath(readPath, options, cb);
        }

        return cb(Errors.DoesNotExist(`No matching art for supplied criteria: ${name}`));
    });
}

function defaultEncodingFromExtension(ext) {
    const artType = SUPPORTED_ART_TYPES[ext.toLowerCase()];
    return artType ? artType.defaultEncoding : 'utf8';
}

function defaultEofFromExtension(ext) {
    const artType = SUPPORTED_ART_TYPES[ext.toLowerCase()];
    if (artType) {
        return artType.eof;
    }
}

//  :TODO: Implement the following
//  * Pause (disabled | termHeight | keyPress )
//  * Cancel (disabled | <keys> )
//  * Resume from pause -> continous (disabled | <keys>)
function display(client, art, options, cb) {
    if (_.isFunction(options) && !cb) {
        cb = options;
        options = {};
    }

    if (!art || !art.length) {
        return cb(Errors.Invalid('No art supplied!'));
    }

    options.mciReplaceChar = options.mciReplaceChar || ' ';

    //  :TODO: this is going to be broken into two approaches controlled via options:
    //  1) Standard - use internal tracking of locations for MCI -- no CPR's/etc.
    //  2) CPR driven

    if (!_.isBoolean(options.iceColors)) {
        //  try to detect from SAUCE
        if (_.has(options, 'sauce.ansiFlags') && options.sauce.ansiFlags & (1 << 0)) {
            options.iceColors = true;
        }
    }

    const ansiParser = new aep.ANSIEscapeParser({
        mciReplaceChar: options.mciReplaceChar,
        termHeight: client.term.termHeight,
        termWidth: client.term.termWidth,
        artWidth: getWidthFromSAUCE(options.sauce),
        trailingLF: options.trailingLF,
        startRow: options.startRow,
    });

    const mciMap = {};
    let generatedId = 100;

    ansiParser.on('mci', mciInfo => {
        //  :TODO: ensure generatedId's do not conflict with any existing |id|
        const id = _.isNumber(mciInfo.id) ? mciInfo.id : generatedId;
        const mapKey = `${mciInfo.mci}${id}`;
        const mapEntry = mciMap[mapKey];

        if (mapEntry) {
            mapEntry.focusSGR = mciInfo.SGR;
            mapEntry.focusArgs = mciInfo.args;
        } else {
            mciMap[mapKey] = {
                position: mciInfo.position,
                args: mciInfo.args,
                SGR: mciInfo.SGR,
                code: mciInfo.mci,
                id: id,
            };

            if (!mciInfo.id) {
                ++generatedId;
            }
        }
    });

    // Remove any MCI's that are in erased rows
    ansiParser.on('erase row', (startRow, endRow) => {
        _.forEach(mciMap, (mciInfo, mapKey) => {
            if (mciInfo.position[0] >= startRow && mciInfo.position[0] <= endRow) {
                delete mciMap[mapKey];
            }
        });
    });

    // Remove any MCI's that are in erased columns
    ansiParser.on('erase columns', (row, startCol, endCol) => {
        _.forEach(mciMap, (mciInfo, mapKey) => {
            if (
                mciInfo.position[0] === row &&
                mciInfo.position[1] >= startCol &&
                mciInfo.position[1] <= endCol
            ) {
                delete mciMap[mapKey];
            }
        });
    });

    ansiParser.on('insert columns', (row, startCol, numCols) => {
        _.forEach(mciMap, (mciInfo, mapKey) => {
            if (mciInfo.position[0] === row && mciInfo.position[1] >= startCol) {
                mciInfo.position[1] += numCols;
                if(mciInfo.position[1] > client.term.termWidth) {
                    delete mciMap[mapKey];
                }
            }
        });
    });

    // Clear the screen, removing any MCI's
    ansiParser.on('clear screen', () => {
        _.forEach(mciMap, (mciInfo, mapKey) => {
            delete mciMap[mapKey];
        });
    });

    ansiParser.on('scroll', (scrollY) => {
        _.forEach(mciMap, (mciInfo) => {
            mciInfo.position[0] -= scrollY;
        });
    });

    ansiParser.on('insert line', (row, numLines) => {
        _.forEach(mciMap, (mciInfo) => {
            if (mciInfo.position[0] >= row) {
                mciInfo.position[0] += numLines;
            }
        });
    });

    ansiParser.on('delete line', (row, numLines) => {
        _.forEach(mciMap, (mciInfo, mapKey) => {
            if (mciInfo.position[0] >= row) {
                if(mciInfo.position[0] < row + numLines) {
                    // unlike scrolling, the rows are actually gone,
                    // so we need to delete any MCI's that are in them
                    delete mciMap[mapKey];
                }
                else {
                    mciInfo.position[0] -= numLines;
                }
            }
        });
    });

    ansiParser.on('literal', literal => client.term.write(literal, false));
    ansiParser.on('control', control => client.term.rawWrite(control));

    ansiParser.on('complete', () => {
        ansiParser.removeAllListeners();

        const extraInfo = {
            height: ansiParser.row - 1,
        };

        return cb(null, mciMap, extraInfo);
    });

    let initSeq = '';
    if (client.term.syncTermFontsEnabled) {
        if (options.font) {
            initSeq = ansi.setSyncTermFontWithAlias(options.font);
        } else if (options.sauce) {
            let fontName = getFontNameFromSAUCE(options.sauce);
            if (fontName) {
                fontName = ansi.getSyncTermFontFromAlias(fontName);
            }

            //
            //  Set SyncTERM font if we're switching only. Most terminals
            //  that support this ESC sequence can only show *one* font
            //  at a time. This applies to detection only (e.g. SAUCE).
            //  If explicit, we'll set it no matter what (above)
            //
            if (fontName && client.term.currentSyncFont != fontName) {
                client.term.currentSyncFont = fontName;
                initSeq = ansi.setSyncTermFont(fontName);
            }
        }
    }

    if (options.iceColors) {
        initSeq += ansi.blinkToBrightIntensity();
    }

    if (initSeq) {
        client.term.rawWrite(initSeq);
    }

    ansiParser.reset(art);
    return ansiParser.parse();
}
