/**
 * Copyright 2016 the WHATAP project authors. All rights reserved.
 * Use of this source code is governed by a license that
 * can be found in the LICENSE file.
 */

var util = require('../util/utils'),
    Logger = require('../logger');

const BYTE_MIN_VALUE = -128;
const BYTE_MAX_VALUE = 127;
const SHORT_MIN_VALUE = -32768;
const SHORT_MAX_VALUE = 32767;
const INT3_MIN_VALUE = -0x800000;
const INT3_MAX_VALUE = 0x007fffff;
const INT_MIN_VALUE = -0x80000000;
const INT_MAX_VALUE = 0x7fffffff;
const LONG5_MIN_VALUE = -0x8000000000;
const LONG5_MAX_VALUE = 0x0000007fffffffff;
const LONG_MIN_VALUE = -0x8000000000000000;
const LONG_MAX_VALUE = 0x7fffffffffffffff;

function isNumOk(n) {
    return n != null && n !== undefined && isNaN(n) == false;
}

function isNotNull(n) {
    return n != null && n !== undefined;
}

function DataOutputX(size) {
    this.buf = undefined;
    if (size == undefined) {
        this.buf = Buffer.alloc(32, 0);
    } else {
        this.buf = Buffer.alloc(size, 0);
    }
    this.offset = 0;
}

DataOutputX.prototype.ensureCapacity = function(minCapacity) {
    if (minCapacity - this.buf.length > 0) {
        this.grow(minCapacity);
    }
};

DataOutputX.prototype.grow = function(minCapacity) {
    var oldCapacity = this.buf.length;
    var newCapacity = oldCapacity << 1;
    if (newCapacity - minCapacity < 0) {
        newCapacity = minCapacity;
    }
    if (newCapacity < 0) {
        if (minCapacity < 0) {
            throw 'out of memory';
        }
        newCapacity = 2147483647;
    }
    var oldBuffer = this.buf;
    this.buf = Buffer.alloc(newCapacity, 0);
    oldBuffer.copy(this.buf);
};

DataOutputX.prototype.reset = function() {
    this.offset = 0;
};

DataOutputX.prototype.size = function() {
    return this.offset;
};

DataOutputX.prototype.writeIntBytes = function(b) {
    if (!b || b.length == 0) {
        this.writeInt(0);
    } else {
        this.writeInt(b.length);
        this.writeBytes(b);
    }
    return this;
};

DataOutputX.prototype.writeShortBytes = function(b) {
    this.writeShort(b.length);
    this.writeBytes(b);
    return this;
};

DataOutputX.prototype.writeBlob = function(value) {
    if (isNotNull(value) == false) {
        this.writeByte(0);
    } else {
        if ((value instanceof Buffer) == false) {
            throw new Error('Write Blob parameter must be buffer instance');
        }
        var len = value.length;
        if (len <= 253) {
            this.writeUInt8(len);
            this.copy(value);
        } else if (len <= 65535) {
            this.writeUInt8(255);
            this.writeUInt16BE(len);
            this.copy(value);
        } else {
            this.writeUInt8(254);
            this.writeUInt32BE(len);
            this.copy(value);
        }
    }
    return this;
};

DataOutputX.prototype.copy = function(inputBuff) {
    this.ensureCapacity(this.offset + inputBuff.length);
    inputBuff.copy(this.buf, this.offset, 0, inputBuff.length);
    this.offset += inputBuff.length;
};

DataOutputX.prototype.toByteArray = function() {
    var returnBuffer = Buffer.alloc(this.offset);
    this.buf.copy(returnBuffer, 0, 0, this.offset);
    return returnBuffer;
};

DataOutputX.prototype.writeByte = function(b) {
    b = b & 0xFF;
    this.ensureCapacity(this.offset + 1);
    this.buf[this.offset] = b;
    this.offset += 1;
    return this;
};

DataOutputX.prototype.writeBytes = function(bytes) {
    if (isNotNull(bytes) == false) {
        bytes = [];
    }

    this.ensureCapacity(this.offset + bytes.length);
    for (var i = 0; i < bytes.length; i++) {
        this.buf[this.offset] = bytes[i];
        this.offset += 1;
    }

    return this;
};

DataOutputX.prototype.writeDecimal = function(value) {
    if (isNumOk(value) == false) {
        this.writeByte(0);
    } else if (BYTE_MIN_VALUE <= value && value <= BYTE_MAX_VALUE) {
        this.writeByte(1);
        this.writeByte(value);
    } else if (SHORT_MIN_VALUE <= value && value <= SHORT_MAX_VALUE) {
        this.writeByte(2);
        this.writeShort(value);
    } else if (INT3_MIN_VALUE <= value && value <= INT3_MAX_VALUE) {
        this.writeByte(3);
        this.writeInt3(value);
    } else if (INT_MIN_VALUE <= value && value <= INT_MAX_VALUE) {
        this.writeByte(4);
        this.writeInt(value);
    } else if (LONG5_MIN_VALUE <= value && value <= LONG5_MAX_VALUE) {
        this.writeByte(5);
        this.writeLong5(value);
    } else if (LONG_MIN_VALUE <= value && value <= LONG_MAX_VALUE) {
        this.writeByte(8);
        this.writeLong(value);
    }
    return this;
};

DataOutputX.prototype.write = function(value) {
    if (Buffer.isBuffer(value)) {
        this.ensureCapacity(this.offset + value.length);
        value.copy(this.buf, this.offset);
        this.offset += value.length;
    } else if (Array.isArray(value)) {
        this.writeBytes(value);
    }
    return this;
};

DataOutputX.prototype.writeInt = function(value) {
    this.writeInt32BE(value);
    return this;
};

DataOutputX.prototype.writeInt8 = function(value) {
    if (isNumOk(value) == false) {
        value = 0;
    }
    value = value & 0xFF;
    this.ensureCapacity(this.offset + 1);
    this.buf[this.offset] = value;
    this.offset += 1;
    return this;
};

DataOutputX.prototype.writeUInt8 = function(value) {
    if (isNumOk(value) == false) {
        value = 0;
    }
    value = value & 0xFF;
    this.ensureCapacity(this.offset + 1);
    this.buf.writeUInt8(value, this.offset);
    this.offset += 1;
    return this;
};

DataOutputX.prototype.writeShort = function(value) {
    if (isNumOk(value) == false) {
        value = 0;
    }
    value = value & 0xFFFF;
    this.ensureCapacity(this.offset + 2);
    this.buf.writeUInt16BE(value, this.offset);
    this.offset += 2;
    return this;
};

DataOutputX.prototype.writeInt16BE = function(value) {
    return this.writeShort(value);
};

DataOutputX.prototype.writeUInt16BE = function(value) {
    return this.writeShort(value);
};

DataOutputX.prototype.writeInt3 = function(value) {
    if (isNumOk(value) == false) {
        value = 0;
    }
    this.ensureCapacity(this.offset + 3);
    this.buf[this.offset] = (value >>> 16) & 0xff;
    this.buf[this.offset + 1] = (value >>> 8) & 0xff;
    this.buf[this.offset + 2] = (value >>> 0) & 0xff;
    this.offset += 3;
    return this;
};

DataOutputX.prototype.writeInt24BE = function(value) {
    return this.writeInt3(value);
};

DataOutputX.prototype.writeInt32BE = function(value) {
    if (isNumOk(value) == false) {
        value = 0;
    }
    // Convert to unsigned 32-bit integer to handle negative values
    value = (value & 0xFFFFFFFF) >>> 0;
    this.ensureCapacity(this.offset + 4);
    this.buf.writeUInt32BE(value, this.offset);
    this.offset += 4;
    return this;
};

DataOutputX.prototype.writeUInt32BE = function(value) {
    return this.writeInt32BE(value);
};

DataOutputX.prototype.writeLong5 = function(value) {
    if (isNumOk(value) == false) {
        value = 0;
    }
    this.ensureCapacity(this.offset + 5);
    this.buf[this.offset] = ((value >> 32) & 0xFF);
    this.buf[this.offset + 1] = ((value >> 24) & 0xFF);
    this.buf[this.offset + 2] = ((value >> 16) & 0xFF);
    this.buf[this.offset + 3] = ((value >> 8) & 0xFF);
    this.buf[this.offset + 4] = ((value >> 0) & 0xFF);
    this.offset += 5;
    return this;
};

DataOutputX.prototype.writeInt40BE = function(value) {
    return this.writeLong5(value);
};

// 새롭게 구현된 writeLong
DataOutputX.prototype.writeLong = function(value) {
    if (isNumOk(value) == false) {
        value = 0;
    }

    this.ensureCapacity(this.offset + 8);

    // BigInt를 사용하여 64비트 연산을 정확히 처리
    try {
        // 음수든 양수든 BigInt로 변환하면 64비트 표현으로 보존됨
        const bigValue = BigInt(value);

        // 64비트 마스킹
        const maskedValue = bigValue & 0xFFFFFFFFFFFFFFFFn;

        // BigInt 값을 버퍼에 씀
        this.buf.writeBigUInt64BE(maskedValue, this.offset);
        this.offset += 8;
    } catch (e) {
        // BigInt를 지원하지 않는 환경이나 에러 발생 시 대체 처리
        // 하위/상위 32비트 별도 처리
        let hi, lo;
        if (value < 0) {
            // 2의 보수 로직으로 음수 처리
            const absValue = Math.abs(value + 1);
            hi = ~Math.floor(absValue / 0x100000000) & 0xFFFFFFFF;
            lo = ~(absValue % 0x100000000) & 0xFFFFFFFF;
        } else {
            hi = Math.floor(value / 0x100000000) & 0xFFFFFFFF;
            lo = value % 0x100000000;
        }

        this.buf.writeUInt32BE(hi, this.offset);
        this.buf.writeUInt32BE(lo, this.offset + 4);
        this.offset += 8;
    }

    return this;
};

DataOutputX.prototype.writeFloat = function(value) {
    if (isNumOk(value) == false) {
        value = 0.0;
    }
    this.ensureCapacity(this.offset + 4);
    this.buf.writeFloatBE(value, this.offset);
    this.offset += 4;
    return this;
};

DataOutputX.prototype.writeDouble = function(value) {
    if (isNumOk(value) == false) {
        value = 0.0;
    }
    this.ensureCapacity(this.offset + 8);
    this.buf.writeDoubleBE(value, this.offset);
    this.offset += 8;
    return this;
};

DataOutputX.prototype.writeText = function(value) {
    if (isNotNull(value) == false) {
        this.writeByte(0);
    } else {
        const bytes = Buffer.from(value, 'utf-8');
        const len = bytes.length;

        if (len <= 253) {
            this.writeUInt8(len);
            this.write(bytes);
        } else if (len <= 65535) {
            this.writeUInt8(255);
            this.writeUInt16BE(len);
            this.write(bytes);
        } else {
            this.writeUInt8(254);
            this.writeUInt32BE(len);
            this.write(bytes);
        }
    }
    return this;
};

DataOutputX.prototype.writeUTF = function(value) {
    if (isNotNull(value) == false) {
        value = "";
    }

    const bytes = Buffer.from(value, 'utf-8');
    const truncatedBytes = bytes.length > 65535 ? bytes.slice(0, 65535) : bytes;

    this.writeUInt16BE(truncatedBytes.length);
    this.write(truncatedBytes);

    return this;
};

DataOutputX.prototype.writeBoolean = function(value) {
    this.writeByte(value ? 1 : 0);
    return this;
};

DataOutputX.prototype.writeFloatArray = function(values) {
    if (!values || !Array.isArray(values)) {
        this.writeShort(0);
    } else {
        this.writeShort(values.length);
        for (let i = 0; i < values.length; i++) {
            this.writeFloat(values[i]);
        }
    }
    return this;
};

DataOutputX.prototype.writeIntArray = function(values) {
    if (!values || !Array.isArray(values)) {
        this.writeShort(0);
    } else {
        this.writeShort(values.length);
        for (let i = 0; i < values.length; i++) {
            this.writeInt(values[i]);
        }
    }
    return this;
};

DataOutputX.prototype.writeLongArray = function(values) {
    if (!values || !Array.isArray(values)) {
        this.writeShort(0);
    } else {
        this.writeShort(values.length);
        for (let i = 0; i < values.length; i++) {
            this.writeLong(values[i]);
        }
    }
    return this;
};

DataOutputX.prototype.writeDecimalArray = function(values) {
    if (!values || !Array.isArray(values)) {
        this.writeShort(0);
    } else {
        this.writeShort(values.length);
        for (let i = 0; i < values.length; i++) {
            this.writeDecimal(values[i]);
        }
    }
    return this;
};

DataOutputX.prototype.writeToPos = function(pos, value) {
    if (isNumOk(value) == false) {
        value = 0;
    }
    this.buf.writeUInt32BE(value, pos);
    return this;
};

DataOutputX.prototype.writePack = function(pack, ln_fmt) {
    this.writeShort(pack.getPackType());
    pack.write(this);

    if (ln_fmt) {
        const remainder = this.offset % ln_fmt;
        if (remainder !== 0) {
            this.write(Buffer.alloc(ln_fmt - remainder));
        }
    }

    return this;
};

DataOutputX.prototype.writeValue = function(value) {
    if (value == null) {
        // NullValue 처리
        value = new NullValue();
    }

    try {
        this.writeByte(value.getValueType());
        value.write(this);
    } catch (e) {
        Logger.printError('WHATAP-503', 'DataOutputX writeValue Error', e);
    }

    return this;
};

DataOutputX.toBytes = function(v) {
    const buf = Buffer.alloc(4);
    buf.writeUInt32BE(v & 0xFFFFFFFF, 0);
    return buf;
};

DataOutputX.toBytesLong = function(v) {
    const buf = Buffer.alloc(8);
    const hi = Math.floor(v / 0x100000000);
    const lo = v % 0x100000000;

    buf.writeUInt32BE(hi, 0);
    buf.writeUInt32BE(lo, 4);
    return buf;
};

DataOutputX.toBytesPack = function(pack, ln_fmt) {
    const out = new DataOutputX().writePack(pack, ln_fmt);
    return out.toByteArray();
};

module.exports = DataOutputX;