import { Box } from './box';
import { String } from 'string';

export function arrayBufferToArray(data: ArrayBuffer): Array<u8> {
    const result = new Array<u8>(data.byteLength);
    store<usize>(changetype<usize>(result), changetype<usize>(data));
    store<usize>(
        changetype<usize>(result) + sizeof<usize>(),
        changetype<usize>(data),
    );
    return result;
}

const ENCODING_CONST_BECH32: u32 = 1;
const ENCODING_CONST_BECH32M: u32 = 0x2bc830a3;

// const limit: i32 = 90;
const ONE = String.UTF8.encode('1');
const ALPHABET_MAP: StaticArray<u8> = [
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 10,
    17, 21, 20, 26, 30, 7, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 24,
    13, 25, 9, 8, 23, 0, 18, 22, 31, 27, 19, 0, 1, 0, 3, 16, 11, 28, 12, 14, 6, 4,
    2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0,
];

const ALPHABET: StaticArray<u8> = [
    113, 112, 122, 114, 121, 57, 120, 56, 103, 102, 50, 116, 118, 100, 119, 48,
    115, 51, 106, 110, 53, 52, 107, 104, 99, 101, 54, 109, 117, 97, 55, 108,
];


@inline
function polymodStep(pre: u32): u32 {
    const b = pre >> 25;
    return (
        ((pre & 0x1ffffff) << 5) ^
        (-((b >> 0) & 1) & 0x3b6a57b2) ^
        (-((b >> 1) & 1) & 0x26508e6d) ^
        (-((b >> 2) & 1) & 0x1ea119fa) ^
        (-((b >> 3) & 1) & 0x3d4233dd) ^
        (-((b >> 4) & 1) & 0x2a1462b3)
    );
}


@inline
function lookupByte(n: u8): u8 {
    return load<u8>(changetype<usize>(ALPHABET_MAP) + <usize>n);
}


@inline
function encodeByte(n: u8): u8 {
    return load<u8>(changetype<usize>(ALPHABET) + <usize>n);
}

function prefixChk(prefix: ArrayBuffer): u32 {
    let chk: u32 = 1;
    for (let i: i32 = 0; i < prefix.byteLength; ++i) {
        const c = load<u8>(changetype<usize>(prefix) + <usize>i);
        if (c < <u8>33 || c > <u8>126) {
            throw new Error('Invalid prefix (' + String.UTF8.decode(prefix) + ')');
        }

        chk = polymodStep(chk) ^ (c >> 5);
    }
    chk = polymodStep(chk);

    for (let i: i32 = 0; i < prefix.byteLength; ++i) {
        const v = load<u8>(changetype<usize>(prefix) + <usize>i);
        chk = polymodStep(chk) ^ (<u32>(v & (<u8>0x1f)));
    }
    return chk;
}

// convert function uses Uint8Array instead of Array<u8>
function convert(
    data: Uint8Array,
    inBits: u32,
    outBits: u32,
    pad: boolean,
): Uint8Array {
    let value = 0;
    let bits: u32 = 0;
    const maxV = (1 << outBits) - 1;

    const result: Array<u8> = new Array(0);

    for (let i = 0; i < data.length; i++) {
        value = (value << inBits) | data[i];
        bits += inBits;

        while (bits >= outBits) {
            bits -= outBits;
            result.push(<u8>((value >> bits) & maxV));
        }
    }

    if (pad) {
        if (bits > 0) {
            result.push(<u8>((value << (outBits - bits)) & maxV));
        }
    } else {
        if (bits >= inBits) throw new Error('Excess padding');
        if ((value << (outBits - bits)) & maxV) throw new Error('Non-zero padding');
    }

    const buffer = new Uint8Array(result.length);
    buffer.set(result);

    return buffer;
}

export function toWords(bytes: ArrayBuffer): ArrayBuffer {
    let data = Uint8Array.wrap(bytes);
    return fromDataStart(convert(data, 8, 5, true));
}

export function encode(
    prefix: ArrayBuffer,
    words: Array<u8>,
    encoding: u32,
    limit: i32 = 90,
): ArrayBuffer {
    if (prefix.byteLength + 7 + words.length > limit)
        throw new Error('Exceeds length limit');

    let chk = prefixChk(prefix);

    const result = new ArrayBuffer(prefix.byteLength + 7 + words.length);
    memory.copy(
        changetype<usize>(result),
        changetype<usize>(prefix),
        <usize>prefix.byteLength,
    );
    let ptr: usize = changetype<usize>(result) + prefix.byteLength;
    store<u8>(ptr, load<u8>(changetype<usize>(ONE)));
    ptr++;
    for (let i = 0; i < words.length; ++i) {
        const x = words[i];
        if (x >> 5 !== 0) throw new Error('Non 5-bit word');
        chk = polymodStep(chk) ^ x;
        store<u8>(ptr + i, encodeByte(x));
    }
    ptr += <usize>words.length;

    for (let i: u32 = 0; i < 6; ++i) {
        chk = polymodStep(chk);
    }
    chk ^= encoding;

    for (let i = 0; i < 6; ++i) {
        const v = (chk >> ((5 - i) * 5)) & 0x1f;
        store<u8>(ptr + i, encodeByte(<u8>v));
    }

    return result;
}

export class Decoded {
    public prefix: ArrayBuffer;
    public words: ArrayBuffer;

    constructor(prefix: ArrayBuffer, words: ArrayBuffer) {
        this.prefix = prefix;
        this.words = words;
    }

    static from(prefix: ArrayBuffer, words: ArrayBuffer): Decoded {
        return new Decoded(prefix, words);
    }
}

const DEFAULT_LIMIT: u32 = 90;

function __decodeDefault(str: string): Decoded {
    return __decode(str, DEFAULT_LIMIT);
}

function __decode(str: string, limit: u32): Decoded {
    if (str.length < 8) return changetype<Decoded>(0);
    if (str.length > <i32>limit) return changetype<Decoded>(0);

    // don't allow mixed case
    const lowered = str.toLowerCase();
    const uppered = str.toUpperCase();
    if (str !== lowered && str !== uppered) return changetype<Decoded>(0);
    str = lowered;

    const split = str.lastIndexOf('1');
    if (split === -1) return changetype<Decoded>(0);
    if (split === 0) return changetype<Decoded>(0);

    const buffer = String.UTF8.encode(str);
    const prefix = Box.from(buffer).setLength(split).toArrayBuffer();
    const wordChars = Box.from(buffer).shrinkFront(split + 1).toArrayBuffer();
    if (wordChars.byteLength < 6) return changetype<Decoded>(0);

    let chk = prefixChk(prefix);

    const words = new Uint8Array(wordChars.byteLength);
    for (let i = 0; i < wordChars.byteLength; ++i) {
        const c = load<u8>(changetype<usize>(wordChars) + <usize>i);
        const v = lookupByte(c);
        chk = polymodStep(chk) ^ v;

        // not in the checksum?
        if (i + 6 >= wordChars.byteLength) continue;
        words[i] = v;
    }

    // if (chk !== ENCODING_CONST) return changetype<Decoded>(0);
    return Decoded.from(prefix, fromDataStart(words));
}

export function fromDataStart<T extends Uint8Array>(v: T): ArrayBuffer {
    return Box.from(changetype<ArrayBuffer>(v.dataStart)).setLength(v.length).toArrayBuffer();
}

export function fromWords(words: ArrayBuffer): ArrayBuffer {
    const converted = convert(Uint8Array.wrap(words), 5, 8, false);
    return converted.buffer;
}

export function b32decode(str: string): Decoded {
    return __decodeDefault(str);
}


export function bech32m(prefix: ArrayBuffer, words: ArrayBuffer): ArrayBuffer {
    return encode(prefix, arrayBufferToArray(words), ENCODING_CONST_BECH32M);
}

export function bech32(prefix: ArrayBuffer, words: ArrayBuffer): ArrayBuffer {
    return encode(prefix, arrayBufferToArray(words), ENCODING_CONST_BECH32);
}
