"use strict";
/**
 * Copyright (c) "Neo4j"
 * Neo4j Sweden AB [https://neo4j.com]
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = handshake;
var channel_1 = require("../channel");
var neo4j_driver_core_1 = require("neo4j-driver-core");
var BOLT_MAGIC_PREAMBLE = 0x6060b017;
var AVAILABLE_BOLT_PROTOCOLS = ['5.8', '5.7', '5.6', '5.4', '5.3', '5.2', '5.1', '5.0', '4.4', '4.3', '4.2', '3.0']; // bolt protocols the client will accept, ordered by preference
var DESIRED_CAPABILITES = 0;
function version(major, minor) {
    return {
        major: major,
        minor: minor
    };
}
function createHandshakeMessage(versions) {
    if (versions.length > 4) {
        throw (0, neo4j_driver_core_1.newError)('It should not have more than 4 versions of the protocol');
    }
    var handshakeBuffer = (0, channel_1.alloc)(5 * 4);
    handshakeBuffer.writeInt32(BOLT_MAGIC_PREAMBLE);
    versions.forEach(function (version) {
        if (version instanceof Array) {
            var _a = version[0], major = _a.major, minor = _a.minor;
            var minMinor = version[1].minor;
            var range = minor - minMinor;
            handshakeBuffer.writeInt32((range << 16) | (minor << 8) | major);
        }
        else {
            var major = version.major, minor = version.minor;
            handshakeBuffer.writeInt32((minor << 8) | major);
        }
    });
    handshakeBuffer.reset();
    return handshakeBuffer;
}
function parseNegotiatedResponse(buffer, log) {
    var h = [
        buffer.readUInt8(),
        buffer.readUInt8(),
        buffer.readUInt8(),
        buffer.readUInt8()
    ];
    if (h[0] === 0x48 && h[1] === 0x54 && h[2] === 0x54 && h[3] === 0x50) {
        log.error('Handshake failed since server responded with HTTP.');
        throw (0, neo4j_driver_core_1.newError)('Server responded HTTP. Make sure you are not trying to connect to the http endpoint ' +
            '(HTTP defaults to port 7474 whereas BOLT defaults to port 7687)');
    }
    return Number(h[3] + '.' + h[2]);
}
function handshakeNegotiationV2(channel, buffer, log) {
    var numVersions = buffer.readVarInt();
    var versions = [];
    for (var i = 0; i < numVersions; i++) {
        var versionRange = [
            buffer.readUInt8(),
            buffer.readUInt8(),
            buffer.readUInt8(),
            buffer.readUInt8()
        ];
        versions = versions.concat(getVersions(versionRange));
    }
    var capabilityBitMask = buffer.readVarInt();
    var capabilites = selectCapabilites(capabilityBitMask);
    var major = 0;
    var minor = 0;
    versions.sort(function (a, b) {
        if (Number(a.major) !== Number(b.major)) {
            return Number(b.major) - Number(a.major);
        }
        else {
            return Number(b.minor) - Number(a.minor);
        }
    });
    for (var i = 0; i < versions.length; i++) {
        var version_1 = versions[i];
        if (AVAILABLE_BOLT_PROTOCOLS.includes(version_1.major + '.' + version_1.minor)) {
            major = version_1.major;
            minor = version_1.minor;
            break;
        }
    }
    return new Promise(function (resolve, reject) {
        try {
            var selectionBuffer = (0, channel_1.alloc)(5);
            selectionBuffer.writeInt32((minor << 8) | major);
            selectionBuffer.writeVarInt(capabilites);
            channel.write(selectionBuffer);
            resolve({
                protocolVersion: Number(major + '.' + minor),
                capabilites: capabilites,
                consumeRemainingBuffer: function (consumer) {
                    if (buffer.hasRemaining()) {
                        consumer(buffer.readSlice(buffer.remaining()));
                    }
                }
            });
        }
        catch (e) {
            reject(e);
        }
    });
}
/**
 * @return {BaseBuffer}
 * @private
 */
function newHandshakeBuffer() {
    return createHandshakeMessage([
        version(255, 1),
        [version(5, 8), version(5, 0)],
        [version(4, 4), version(4, 2)],
        version(3, 0)
    ]);
}
/**
 * This callback is displayed as a global member.
 * @callback BufferConsumerCallback
 * @param {buffer} buffer the remaining buffer
 */
/**
 * @typedef HandshakeResult
 * @property {number} protocolVersion The protocol version negotiated in the handshake
 * @property {number} capabilites A bitmask representing the capabilities negotiated in the handshake
 * @property {function(BufferConsumerCallback)} consumeRemainingBuffer A function to consume the remaining buffer if it exists
 */
/**
 * Shake hands using the channel and return the protocol version
 *
 * @param {Channel} channel the channel use to shake hands
 * @param {Logger} log the log object
 * @returns {Promise<HandshakeResult>} Promise of protocol version and consumeRemainingBuffer
 */
function handshake(channel, log) {
    return initialHandshake(channel, log).then(function (result) {
        if (result.protocolVersion === 255.1) {
            return handshakeNegotiationV2(channel, result.buffer, log);
        }
        else {
            return result;
        }
    });
}
/**
 * Shake hands using the channel and return the protocol version, or the improved handshake protocol if communicating with a newer server.
 *
 * @param {Channel} channel the channel use to shake hands
 * @param {Logger} log the log object
 * @returns {Promise<HandshakeResult>} Promise of protocol version and consumeRemainingBuffer
 */
function initialHandshake(channel, log) {
    var _this = this;
    return new Promise(function (resolve, reject) {
        var handshakeErrorHandler = function (error) {
            reject(error);
        };
        channel.onerror = handshakeErrorHandler.bind(_this);
        if (channel._error) {
            handshakeErrorHandler(channel._error);
        }
        channel.onmessage = function (buffer) {
            try {
                // read the response buffer and initialize the protocol
                var protocolVersion = parseNegotiatedResponse(buffer, log);
                resolve({
                    protocolVersion: protocolVersion,
                    capabilites: 0,
                    buffer: buffer,
                    consumeRemainingBuffer: function (consumer) {
                        if (buffer.hasRemaining()) {
                            consumer(buffer.readSlice(buffer.remaining()));
                        }
                    }
                });
            }
            catch (e) {
                reject(e);
            }
        };
        channel.write(newHandshakeBuffer());
    });
}
function getVersions(versionArray) {
    var resultArr = [];
    var major = versionArray[3];
    var minor = versionArray[2];
    for (var i = 0; i <= versionArray[1]; i++) {
        resultArr.push({ major: major, minor: minor - i });
    }
    return resultArr;
}
function selectCapabilites(capabilityBitMask) {
    return DESIRED_CAPABILITES; // capabilites are currently unused and will always be 0.
}
