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

//  ENiGMA½
const ansi = require('./ansi_term.js');
const Events = require('./events.js');
const Config = require('./config.js').get;
const { Errors } = require('./enig_error.js');

//  deps
const async = require('async');

exports.connectEntry = connectEntry;

const withCursorPositionReport = (client, cprHandler, failMessage, cb) => {
    let giveUpTimer;

    const done = function (err) {
        client.removeListener('cursor position report', cprListener);
        clearTimeout(giveUpTimer);
        return cb(err);
    };

    const cprListener = pos => {
        cprHandler(pos);
        return done(null);
    };

    client.once('cursor position report', cprListener);

    //  give up after 2s
    giveUpTimer = setTimeout(() => {
        return done(Errors.General(failMessage));
    }, 2000);
};

function ansiDiscoverHomePosition(client, cb) {
    //
    //  We want to find the home position. ANSI-BBS and most terminals
    //  utilize 1,1 as home. However, some terminals such as ConnectBot
    //  think of home as 0,0. If this is the case, we need to offset
    //  our positioning to accommodate for such.
    //
    if (!Config().term.checkAnsiHomePosition) {
        // Skip (and assume 1,1) if the home position check is disabled.
        return cb(null);
    }

    withCursorPositionReport(
        client,
        pos => {
            const [h, w] = pos;

            //
            //  We expect either 0,0, or 1,1. Anything else will be filed as bad data
            //
            if (h > 1 || w > 1) {
                return client.log.warn(
                    { height: h, width: w },
                    'Ignoring ANSI home position CPR due to unexpected values'
                );
            }

            if ((0 === h) & (0 === w)) {
                //
                //  Store a CPR offset in the client. All CPR's from this point on will offset by this amount
                //
                client.log.info('Setting CPR offset to 1');
                client.cprOffset = 1;
            }
        },
        'ANSI query home position timed out',
        cb
    );

    client.term.write(`${ansi.goHome()}${ansi.queryPos()}`); //  go home, query pos
}

function ansiAttemptDetectUTF8(client, cb) {
    //
    //  Trick to attempt and detect UTF-8. While there is a lot more than
    //  just UTF-8 and CP437, many those are the main concerns, when it comes
    //  terminals that for example tell us they are "xterm" but still want CP437.
    //
    //  Try to detect UTF-8 by discovering the cursor position, writing some
    //  multi-byte UTF-8, and checking the position again. If the term is really
    //  UTF-8, we should get a proper position, otherwise we'll be further out.
    //
    //  We currently only do this if the term hasn't already been ID'd as a
    //  "*nix" terminal -- that is, xterm, etc.
    //  Also skip this check if checkUtf8Encoding is disabled in the config

    if (!client.term.isNixTerm() || !Config().term.checkUtf8Encoding) {
        return cb(null);
    }

    let initialPosition;
    const ASCIIPortion = ' Character encoding detection ';

    withCursorPositionReport(
        client,
        pos => {
            initialPosition = pos;

            withCursorPositionReport(
                client,
                pos => {
                    const [_, w] = pos;
                    const len = w - initialPosition[1];
                    if (!isNaN(len) && len >= ASCIIPortion.length + 6) {
                        //  CP437 displays 3 chars each Unicode skull
                        client.log.info(
                            'Terminal identified as UTF-8 but does not appear to be. Overriding to "ansi".'
                        );
                        client.setTermType('ansi');
                    }
                },
                'Detect UTF-8 stage 2 timed out',
                cb
            );

            client.term.rawWrite(`\u9760${ASCIIPortion}\u9760`); //  Unicode skulls on each side
            client.term.rawWrite(ansi.queryPos());
        },
        'Detect UTF-8 stage 1 timed out',
        err => {
            if (err) {
                //  bailing early
                return cb(err);
            }
        }
    );

    client.term.rawWrite(`${ansi.goHome()}${ansi.queryPos()}`);
}

const ansiQuerySyncTermFontSupport = (client, cb) => {
    // If we know it's SyncTERM, then good to go
    if (client.term.termClient === 'cterm') {
        client.term.syncTermFontsEnabled = true;
        return cb(null);
    }

    //  Some terminals do not properly ignore an unknown ESC sequence for
    //  setting SyncTERM style fonts. Some draw only a 'D' while others
    //  show more of the sequence
    //
    //  If the cursor remains at 1,1, they are at least ignoring it, if not
    //  supporting it.
    //
    //  We also need CPR support. Since all known terminals that support
    //  SyncTERM fonts also support CPR, this works out.
    //
    withCursorPositionReport(
        client,
        pos => {
            const [_, w] = pos;
            if (w === 1) {
                // cursor didn't move
                client.log.info(`SyncTERM font support enabled on node ${client.node}`);
                client.term.syncTermFontsEnabled = true;
            }
        },
        'SyncTERM font detection timed out',
        cb
    );

    client.term.rawWrite(
        `${ansi.goto(1, 1)}${ansi.setSyncTermFont('cp437')}${ansi.queryPos()}`
    );
};

function ansiQueryTermSizeIfNeeded(client, cb) {
    if (client.term.termHeight > 0 || client.term.termWidth > 0) {
        return cb(null);
    }

    withCursorPositionReport(
        client,
        pos => {
            //
            //  If we've already found out, disregard
            //
            if (client.term.termHeight > 0 || client.term.termWidth > 0) {
                return;
            }

            //
            //  NetRunner for example gives us 1x1 here. Not really useful. Ignore
            //  values that seem obviously bad. Included in the set is the explicit
            //  999x999 values we asked to move to.
            //
            const [h, w] = pos;
            if (h < 10 || h === 999 || w < 10 || w === 999) {
                return client.log.warn(
                    { height: h, width: w },
                    'Ignoring ANSI CPR screen size query response due to non-sane values'
                );
            }

            client.term.termHeight = h;
            client.term.termWidth = w;

            client.log.debug(
                {
                    termWidth: client.term.termWidth,
                    termHeight: client.term.termHeight,
                    source: 'ANSI CPR',
                },
                'Window size updated'
            );
        },
        'No term size established by CPR within timeout',
        cb
    );

    //  Start the process:
    //  1 - Ask to goto 999,999 -- a very much "bottom right" (generally 80x25 for example
    //      is the real size)
    //  2 - Query for screen size with bansi.txt style specialized Device Status Report (DSR)
    //      request. We expect a CPR of:
    //      a - Terms that support bansi.txt style: Screen size
    //      b - Terms that do not support bansi.txt style: Since we moved to the bottom right
    //          we should still be able to determine a screen size.
    //
    client.term.rawWrite(`${ansi.goto(999, 999)}${ansi.queryScreenSize()}`);
}

function prepareTerminal(term) {
    term.rawWrite(`${ansi.normal()}${ansi.clearScreen()}`);
}

function displayBanner(term) {
    //  note: intentional formatting:
    term.pipeWrite(`
|06Connected to |02EN|10i|02GMA|10½ |06BBS version |12|VN
|06Copyright (c) 2014-2022 Bryan Ashby |14- |12http://l33t.codes/
|06Updates & source |14- |12https://github.com/NuSkooler/enigma-bbs/
|00`);
}

function connectEntry(client, nextMenu) {
    const term = client.term;

    async.series(
        [
            function basicPrepWork(callback) {
                term.rawWrite(ansi.queryDeviceAttributes(0));
                term.write('\nDetecting client features...');
                return callback(null);
            },
            function discoverHomePosition(callback) {
                return ansiDiscoverHomePosition(client, callback);
            },
            function queryTermSizeByNonStandardAnsi(callback) {
                ansiQueryTermSizeIfNeeded(client, err => {
                    if (err) {
                        //
                        //  Check again; We may have got via NAWS/similar before CPR completed.
                        //
                        if (0 === term.termHeight || 0 === term.termWidth) {
                            //
                            //  We still don't have something good for term height/width.
                            //  Default to DOS size 80x25.
                            //
                            //  :TODO: Netrunner is currently hitting this and it feels wrong. Why is NAWS/ENV/CPR all failing???
                            client.log.warn(
                                { reason: err.message },
                                'Failed to negotiate term size; Defaulting to 80x25!'
                            );

                            term.termHeight = 25;
                            term.termWidth = 80;
                        }
                    }

                    return callback(null);
                });
            },
            function checkUtf8IfNeeded(callback) {
                return ansiAttemptDetectUTF8(client, callback);
            },
            function querySyncTERMFontSupport(callback) {
                return ansiQuerySyncTermFontSupport(client, callback);
            },
        ],
        () => {
            prepareTerminal(term);

            //
            //  Always show an ENiGMA½ banner
            //
            displayBanner(term);

            // fire event
            Events.emit(Events.getSystemEvents().TermDetected, { client: client });

            setTimeout(() => {
                return client.menuStack.goto(nextMenu);
            }, 500);
        }
    );
}
