/**
 * Copyright (c) Whales Corp. 
 * All Rights Reserved.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import { BitReader } from "../BitReader";
import { BitString } from "../BitString";
import { Cell } from "../Cell";
import { topologicalSort } from "./utils/topologicalSort";
import { bitsForNumber } from "../../utils/bitsForNumber";
import { BitBuilder } from "../BitBuilder";
import { getBitsDescriptor, getRefsDescriptor } from "./descriptor";
import { bitsToPaddedBuffer } from "../utils/paddedBits";
import { crc32c } from "../../utils/crc32c";

function getHashesCount(levelMask: number) {
    return getHashesCountFromMask(levelMask & 7)
}

function getHashesCountFromMask(mask: number) {
    let n = 0;
    for (let i = 0; i < 3; i++) {
        n += (mask & 1);
        mask = mask >> 1;
    }
    return n+1; // 1 repr + up to 3 higher hashes
}

function readCell(reader: BitReader, sizeBytes: number) {

    // D1
    const d1 = reader.loadUint(8);
    const refsCount = d1 % 8;
    const exotic = !!(d1 & 8);

    // D2
    const d2 = reader.loadUint(8);
    const dataBytesize = Math.ceil(d2 / 2);
    const paddingAdded = !!(d2 % 2);

    const levelMask = d1 >> 5;
    const hasHashes = (d1 & 16) != 0;
    const hash_bytes = 32;

    const hashesSize = hasHashes ? getHashesCount(levelMask) * hash_bytes : 0;
    const depthSize = hasHashes ? getHashesCount(levelMask) * 2 : 0;

    reader.skip(hashesSize * 8);
    reader.skip(depthSize * 8);

    // Bits
    let bits = BitString.EMPTY;
    if (dataBytesize > 0) {
        if (paddingAdded) {
            bits = reader.loadPaddedBits(dataBytesize * 8);
        } else {
            bits = reader.loadBits(dataBytesize * 8);
        }
    }

    // Refs
    let refs: number[] = [];
    for (let i = 0; i < refsCount; i++) {
        refs.push(reader.loadUint(sizeBytes * 8));
    }

    // Result
    return {
        bits,
        refs,
        exotic
    };
}

function calcCellSize(cell: Cell, sizeBytes: number) {
    return 2 /* D1+D2 */ + Math.ceil(cell.bits.length / 8) + cell.refs.length * sizeBytes;
}

export function parseBoc(src: Buffer) {
    let reader = new BitReader(new BitString(src, 0, src.length * 8));
    let magic = reader.loadUint(32);
    if (magic === 0x68ff65f3) {
        let size = reader.loadUint(8);
        let offBytes = reader.loadUint(8);
        let cells = reader.loadUint(size * 8);
        let roots = reader.loadUint(size * 8); // Must be 1
        let absent = reader.loadUint(size * 8);
        let totalCellSize = reader.loadUint(offBytes * 8);
        let index = reader.loadBuffer(cells * offBytes);
        let cellData = reader.loadBuffer(totalCellSize);
        return {
            size,
            offBytes,
            cells,
            roots,
            absent,
            totalCellSize,
            index,
            cellData,
            root: [0]
        };
    } else if (magic === 0xacc3a728) {
        let size = reader.loadUint(8);
        let offBytes = reader.loadUint(8);
        let cells = reader.loadUint(size * 8);
        let roots = reader.loadUint(size * 8); // Must be 1
        let absent = reader.loadUint(size * 8);
        let totalCellSize = reader.loadUint(offBytes * 8);
        let index = reader.loadBuffer(cells * offBytes);
        let cellData = reader.loadBuffer(totalCellSize);
        let crc32 = reader.loadBuffer(4);
        if (!crc32c(src.subarray(0, src.length - 4)).equals(crc32)) {
            throw Error('Invalid CRC32C');
        }
        return {
            size,
            offBytes,
            cells,
            roots,
            absent,
            totalCellSize,
            index,
            cellData,
            root: [0]
        };
    } else if (magic === 0xb5ee9c72) {
        let hasIdx = reader.loadUint(1);
        let hasCrc32c = reader.loadUint(1);
        let hasCacheBits = reader.loadUint(1);
        let flags = reader.loadUint(2); // Must be 0
        let size = reader.loadUint(3);
        let offBytes = reader.loadUint(8);
        let cells = reader.loadUint(size * 8);
        let roots = reader.loadUint(size * 8);
        let absent = reader.loadUint(size * 8);
        let totalCellSize = reader.loadUint(offBytes * 8);
        let root: number[] = [];
        for (let i = 0; i < roots; i++) {
            root.push(reader.loadUint(size * 8));
        }
        let index: Buffer | null = null;
        if (hasIdx) {
            index = reader.loadBuffer(cells * offBytes);
        }
        let cellData = reader.loadBuffer(totalCellSize);
        if (hasCrc32c) {
            let crc32 = reader.loadBuffer(4);
            if (!crc32c(src.subarray(0, src.length - 4)).equals(crc32)) {
                throw Error('Invalid CRC32C');
            }
        }
        return {
            size,
            offBytes,
            cells,
            roots,
            absent,
            totalCellSize,
            index,
            cellData,
            root
        };
    } else {
        throw Error('Invalid magic');
    }
}

export function deserializeBoc(src: Buffer) {

    //
    // Parse BOC
    //

    let boc = parseBoc(src);
    let reader = new BitReader(new BitString(boc.cellData, 0, boc.cellData.length * 8));

    //
    // Load cells
    //

    let cells: { bits: BitString, refs: number[], exotic: boolean, result: Cell | null }[] = [];
    for (let i = 0; i < boc.cells; i++) {
        let cll = readCell(reader, boc.size);
        cells.push({ ...cll, result: null });
    }

    //
    // Build cells
    //

    for (let i = cells.length - 1; i >= 0; i--) {
        if (cells[i].result) {
            throw Error('Impossible');
        }
        let refs: Cell[] = [];
        for (let r of cells[i].refs) {
            if (!cells[r].result) {
                throw Error('Invalid BOC file');
            }
            refs.push(cells[r].result!);
        }
        cells[i].result = new Cell({ bits: cells[i].bits, refs, exotic: cells[i].exotic });
    }

    //
    // Load roots
    //

    let roots: Cell[] = [];
    for (let i = 0; i < boc.root.length; i++) {
        roots.push(cells[boc.root[i]].result!);
    }

    //
    // Return
    //

    return roots;
}

function writeCellToBuilder(cell: Cell, refs: number[], sizeBytes: number, to: BitBuilder) {
    let d1 = getRefsDescriptor(cell.refs, cell.mask.value, cell.type);
    let d2 = getBitsDescriptor(cell.bits);
    to.writeUint(d1, 8);
    to.writeUint(d2, 8);
    to.writeBuffer(bitsToPaddedBuffer(cell.bits));
    for (let r of refs) {
        to.writeUint(r, sizeBytes * 8);
    }
}

export function serializeBoc(root: Cell, opts: { idx: boolean, crc32: boolean }) {

    // Sort cells
    let allCells = topologicalSort(root);

    // Calculcate parameters
    let cellsNum = allCells.length;
    let has_idx = opts.idx;
    let has_crc32c = opts.crc32;
    let has_cache_bits = false;
    let flags = 0;
    let sizeBytes = Math.max(Math.ceil(bitsForNumber(cellsNum, 'uint') / 8), 1);
    let totalCellSize: number = 0;
    let index: number[] = [];
    for (let c of allCells) {
        let sz = calcCellSize(c.cell, sizeBytes);
        totalCellSize += sz;
        index.push(totalCellSize);
    }
    let offsetBytes = Math.max(Math.ceil(bitsForNumber(totalCellSize, 'uint') / 8), 1);
    let totalSize = (
        4 + // magic
        1 + // flags and s_bytes
        1 + // offset_bytes
        3 * sizeBytes + // cells_num, roots, complete
        offsetBytes + // full_size
        1 * sizeBytes + // root_idx
        (has_idx ? cellsNum * offsetBytes : 0) +
        totalCellSize +
        (has_crc32c ? 4 : 0)
    ) * 8;

    // Serialize
    let builder = new BitBuilder(totalSize);
    builder.writeUint(0xb5ee9c72, 32); // Magic
    builder.writeBit(has_idx); // Has index
    builder.writeBit(has_crc32c); // Has crc32c
    builder.writeBit(has_cache_bits); // Has cache bits
    builder.writeUint(flags, 2); // Flags
    builder.writeUint(sizeBytes, 3); // Size bytes
    builder.writeUint(offsetBytes, 8); // Offset bytes
    builder.writeUint(cellsNum, sizeBytes * 8); // Cells num
    builder.writeUint(1, sizeBytes * 8); // Roots num
    builder.writeUint(0, sizeBytes * 8); // Absent num
    builder.writeUint(totalCellSize, offsetBytes * 8); // Total cell size
    builder.writeUint(0, sizeBytes * 8); // Root id == 0
    if (has_idx) { // Index
        for (let i = 0; i < cellsNum; i++) {
            builder.writeUint(index[i], offsetBytes * 8);
        }
    }
    for (let i = 0; i < cellsNum; i++) { // Cells
        writeCellToBuilder(allCells[i].cell, allCells[i].refs, sizeBytes, builder);
    }
    if (has_crc32c) {
        let crc32 = crc32c(builder.buffer()) // builder.buffer() is fast since it doesn't allocate new memory
        builder.writeBuffer(crc32);
    }

    // Sanity Check
    let res = builder.buffer();
    if (res.length !== totalSize / 8) {
        throw Error('Internal error');
    }
    return res;
}