/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 */

'use strict';

const Long = require('long');
const BinaryUtils = require('./BinaryUtils');
const Errors = require('../Errors');

const BUFFER_CAPACITY_DEFAULT = 256;
const BYTE_ZERO = 0;
const BYTE_ONE = 1;

class MessageBuffer {
    constructor(capacity = BUFFER_CAPACITY_DEFAULT) {
        this._buffer = Buffer.allocUnsafe(capacity);
        this._capacity = capacity;
        this._length = 0;
        this._position = 0;
    }

    static from(source, position) {
        const buf = new MessageBuffer();
        buf._buffer = Buffer.from(source);
        buf._position = position;
        buf._length = buf._buffer.length;
        buf._capacity = buf._length;
        return buf;
    }

    concat(source) {
        this._buffer = Buffer.concat([this._buffer, source]);
        this._length = this._buffer.length;
        this._capacity = this._length;
    }

    get position() {
        return this._position;
    }

    set position(position) {
        this._position = position;
    }

    get length() {
        return this._length;
    }

    get data() {
        return this.getSlice(0, this.length);
    }

    get buffer() {
        return this._buffer;
    }

    getSlice(start, end) {
        return this._buffer.slice(start, end);
    }

    writeByte(value) {
        this.writeNumber(value, BinaryUtils.TYPE_CODE.BYTE);
    }

    writeShort(value) {
        this.writeNumber(value, BinaryUtils.TYPE_CODE.SHORT);
    }

    writeInteger(value) {
        this.writeNumber(value, BinaryUtils.TYPE_CODE.INTEGER);
    }

    writeLong(value) {
        try {
            if (!Long.isLong(value)) {
                value = Long.fromValue(value);
            }
        }
        catch (err) {
            throw Errors.IgniteClientError.valueCastError(value, BinaryUtils.TYPE_CODE.LONG);
        }
        const buffer = Buffer.from(value.toBytesLE());
        this.writeBuffer(buffer);
    }

    writeFloat(value) {
        this.writeNumber(value, BinaryUtils.TYPE_CODE.FLOAT);
    }

    writeDouble(value) {
        this.writeNumber(value, BinaryUtils.TYPE_CODE.DOUBLE);
    }

    writeNumber(value, type, signed = true) {
        const size = BinaryUtils.getSize(type);
        this._ensureCapacity(size);
        try {
            switch (type) {
                case BinaryUtils.TYPE_CODE.BYTE:
                    if (signed) {
                        this._buffer.writeInt8(value, this._position);
                    }
                    else {
                        this._buffer.writeUInt8(value, this._position);
                    }
                    break;
                case BinaryUtils.TYPE_CODE.SHORT:
                    if (signed) {
                        this._buffer.writeInt16LE(value, this._position);
                    }
                    else {
                        this._buffer.writeUInt16LE(value, this._position);
                    }
                    break;
                case BinaryUtils.TYPE_CODE.INTEGER:
                    if (signed) {
                        this._buffer.writeInt32LE(value, this._position);
                    }
                    else {
                        this._buffer.writeUInt32LE(value, this._position);
                    }
                    break;
                case BinaryUtils.TYPE_CODE.FLOAT:
                    this._buffer.writeFloatLE(value, this._position);
                    break;
                case BinaryUtils.TYPE_CODE.DOUBLE:
                    this._buffer.writeDoubleLE(value, this._position);
                    break;
                default:
                    throw Errors.IgniteClientError.internalError();
            }
        }
        catch (err) {
            throw Errors.IgniteClientError.valueCastError(value, type);
        }
        this._position += size;
    }

    writeBoolean(value) {
        this.writeByte(value ? BYTE_ONE : BYTE_ZERO);
    }

    writeChar(value) {
        this.writeShort(value.charCodeAt(0));
    }

    writeString(value) {
        const buffer = Buffer.from(value, BinaryUtils.ENCODING);
        const length = buffer.length;
        this.writeInteger(length);
        if (length > 0) {
            this.writeBuffer(buffer);
        }
    }

    writeDate(value) {
        this.writeLong(value.getTime());
    }

    readByte() {
        return this.readNumber(BinaryUtils.TYPE_CODE.BYTE);
    }

    readShort() {
        return this.readNumber(BinaryUtils.TYPE_CODE.SHORT);
    }

    readInteger() {
        return this.readNumber(BinaryUtils.TYPE_CODE.INTEGER);
    }

    readLong() {
        const size = BinaryUtils.getSize(BinaryUtils.TYPE_CODE.LONG)
        this._ensureSize(size);
        const value = Long.fromBytesLE([...this._buffer.slice(this._position, this._position + size)]);
        this._position += size;
        return value;
    }

    readFloat() {
        return this.readNumber(BinaryUtils.TYPE_CODE.FLOAT);
    }

    readDouble() {
        return this.readNumber(BinaryUtils.TYPE_CODE.DOUBLE);
    }

    readNumber(type, signed = true) {
        const size = BinaryUtils.getSize(type);
        this._ensureSize(size);
        let value;
        switch (type) {
            case BinaryUtils.TYPE_CODE.BYTE:
                value = signed ? this._buffer.readInt8(this._position) : this._buffer.readUInt8(this._position);
                break;
            case BinaryUtils.TYPE_CODE.SHORT:
                value = signed ? this._buffer.readInt16LE(this._position) : this._buffer.readUInt16LE(this._position);
                break;
            case BinaryUtils.TYPE_CODE.INTEGER:
                value = signed ? this._buffer.readInt32LE(this._position) : this._buffer.readUInt32LE(this._position);
                break;
            case BinaryUtils.TYPE_CODE.FLOAT:
                value = this._buffer.readFloatLE(this._position);
                break;
            case BinaryUtils.TYPE_CODE.DOUBLE:
                value = this._buffer.readDoubleLE(this._position);
                break;
            default:
                throw Errors.IgniteClientError.internalError();
        }
        this._position += size;
        return value;
    }

    readBoolean() {
        return this.readByte() === BYTE_ONE;
    }

    readChar() {
        return String.fromCharCode(this.readShort());
    }

    readString() {
        const bytesCount = this.readInteger();
        this._ensureSize(bytesCount);
        const result = this._buffer.toString(BinaryUtils.ENCODING, this._position, this._position + bytesCount);
        this._position += bytesCount;
        return result;
    }

    readBuffer(length) {
        this._ensureSize(length);
        const result = this._buffer.slice(this._position, this._position + length);
        this._position += length;
        return result;
    }

    readDate() {
        return new Date(this.readLong().toNumber());
    }

    writeBuffer(buffer, start = undefined, end = undefined) {
        if (start === undefined) {
            start = 0;
        }
        if (end === undefined) {
            end = buffer.length;
        }
        const size = end - start;
        this._ensureCapacity(size);
        buffer.copy(this._buffer, this._position, start, end);
        this._position += size;
    }

    _ensureSize(size) {
        if (this._position + size > this._length) {
            throw Errors.IgniteClientError.internalError('Unexpected format of response');
        }
    }

    _ensureCapacity(valueSize) {
        if (valueSize <= 0) {
            throw Errors.IgniteClientError.internalError();
        }
        let newCapacity = this._capacity;
        while (this._position + valueSize > newCapacity) {
            newCapacity = newCapacity * 2;
        }
        if (this._capacity < newCapacity) {
            this._buffer = Buffer.concat([this._buffer, Buffer.allocUnsafe(newCapacity - this._capacity)], newCapacity);
            this._capacity = newCapacity;
        }
        if (this._position + valueSize > this._length) {
            this._length = this._position + valueSize;
        }
    }
}

module.exports = MessageBuffer;
