const KIND_TAG_BITS = 2
const CHUNK_BITS = 4


const SCHEMAS = {
    '0-2': {
        name: 'item',
        fields: ['owner_identity_id', 'item_local_id'],
        interpret: ([owner_identity_id, item_local_id]) => ({kind: 'item', owner_identity_id, item_local_id})
    },
//0-1 left free for later use
    '1-2': {
        name: 'group_item',
        fields: ['owner_group_id', 'item_local_id'],
        interpret: ([owner_group_id, item_local_id]) => ({kind: 'group_item', owner_group_id, item_local_id})
    },
    '1-1': {
        name: 'group',
        fields: ['group_id'],
        interpret: ([group_id]) => ({kind: 'group', group_id})
    },
    '2-2': {
        name: 'storage_location',
        fields: ['owner_identity_id', 'storage_location_id'],
        interpret: ([owner_identity_id, storage_location_id]) =>
            ({kind: 'storage_location', owner_identity_id, storage_location_id})
    },
    '2-1': {
        name: 'file',
        fields: ['file_id'],
        interpret: ([file_id]) => ({kind: 'file', file_id})
    },
    '3-2': {
        name: 'workflow',
        fields: ['owner_identity_id', 'workflow_id'],
        interpret: ([owner_identity_id, workflow_id]) => ({kind: 'workflow', owner_identity_id, workflow_id})
    },
    '3-1': {
        name: 'category',
        fields: ['category_id'],
        interpret: ([category_id]) => ({kind: 'category', category_id})
    },
}

const BASE64URL_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
const DIRECT_KIND_COUNT = 2 ** KIND_TAG_BITS - 1

class BitWriter {
    constructor() {
        this.bits = []
    }

    writeBits(value, count) {
        value = BigInt(value)
        for (let i = count - 1; i >= 0; i--) {
            this.bits.push(Number((value >> BigInt(i)) & 1n))
        }
    }

    toBase64Url() {
        const padded = this.bits.slice()
        while (padded.length % 6 !== 0) {
            padded.push(0)
        }
        let token = ''
        for (let i = 0; i < padded.length; i += 6) {
            let n = 0
            for (let j = 0; j < 6; j++) {
                n = (n << 1) | padded[i + j]
            }
            token += BASE64URL_ALPHABET[n]
        }
        return token
    }
}

class BitReader {
    constructor(bits) {
        this.bits = bits
        this.pos = 0
    }

    readBits(count) {
        let value = 0n
        for (let i = 0; i < count; i++) {
            value = (value << 1n) | BigInt(this.bits[this.pos] ?? 0)
            this.pos++
        }
        return value
    }

    bitsLeft() {
        return this.bits.length - this.pos
    }
}

function base64UrlToBits(text) {
    const bits = []
    for (const char of text) {
        const n = BASE64URL_ALPHABET.indexOf(char)
        if (n === -1) {
            throw new Error(`invalid short id: '${char}' is not a URL-safe base64 character`)
        }
        for (let i = 5; i >= 0; i--) {
            bits.push((n >> i) & 1)
        }
    }
    return bits
}

function writeChunkedInt(writer, value, chunkBits) {
    value = BigInt(value)
    if (value < 0n) {
        throw new Error('short id values must be non-negative integers')
    }
    const mask = (1n << BigInt(chunkBits)) - 1n
    const chunks = []
    let remaining = value
    do {
        chunks.unshift(remaining & mask)
        remaining >>= BigInt(chunkBits)
    } while (remaining > 0n)
    chunks.forEach((chunk, i) => {
        writer.writeBits(i === chunks.length - 1 ? 0 : 1, 1)
        writer.writeBits(chunk, chunkBits)
    })
}

function readChunkedInt(reader, chunkBits) {
    let value = 0n
    for (; ;) {
        const more = reader.readBits(1)
        const data = reader.readBits(chunkBits)
        value = (value << BigInt(chunkBits)) | data
        if (more === 0n) {
            return value
        }
    }
}

function schemaForKindAndArity(kindIndex, arity) {
    const schema = SCHEMAS[`${kindIndex}-${arity}`]
    if (!schema) {
        throw new Error(`unknown short id schema for kind ${kindIndex} with ${arity} field value(s)`)
    }
    return schema
}

export function encodeShortId(ints) {
    if (!Array.isArray(ints) || ints.length === 0) {
        throw new Error('short id must be a non-empty list of [kindIndex, ...fieldValues]')
    }
    const [kindIndex, ...fieldValues] = ints
    const schema = schemaForKindAndArity(kindIndex, fieldValues.length)
    if (fieldValues[fieldValues.length - 1] === 0) {
        throw new Error(
            `short id kind index ${kindIndex} ('${schema.name}'): the last field's value must not be 0 - `
            + `reserved so decodeShortId can tell a real trailing field apart from zero-padding without `
            + `knowing the kind's arity up front (see docs/handles-and-shortids.md)`
        )
    }
    const writer = new BitWriter()
    if (kindIndex < DIRECT_KIND_COUNT) {
        writer.writeBits(kindIndex, KIND_TAG_BITS)
    } else {
        writer.writeBits(DIRECT_KIND_COUNT, KIND_TAG_BITS)
        writeChunkedInt(writer, kindIndex - DIRECT_KIND_COUNT, CHUNK_BITS)
    }
    for (const value of fieldValues) {
        writeChunkedInt(writer, value, CHUNK_BITS)
    }
    return '~' + writer.toBase64Url()
}

export function decodeShortId(token) {
    if (typeof token !== 'string' || !token.startsWith('~')) {
        throw new Error("short id must start with '~'")
    }
    const reader = new BitReader(base64UrlToBits(token.slice(1)))
    let kindIndex = Number(reader.readBits(KIND_TAG_BITS))
    if (kindIndex === DIRECT_KIND_COUNT) {
        kindIndex = DIRECT_KIND_COUNT + Number(readChunkedInt(reader, CHUNK_BITS))
    }
    const fieldValues = []
    while (reader.bitsLeft() >= CHUNK_BITS + 1) {
        const value = readChunkedInt(reader, CHUNK_BITS)
        if (value === 0n && reader.bitsLeft() === 0) {
            break // a trailing all-zero chunk with nothing after it is padding, not a real field
        }
        fieldValues.push(Number(value))
    }
    return [kindIndex, ...fieldValues]
}

export function deserializeShortId(ints) {
    const [kindIndex, ...fieldValues] = ints
    const schema = schemaForKindAndArity(kindIndex, fieldValues.length)
    return schema.interpret(fieldValues)
}

export function serializeShortId({kind, ...fieldValues}) {
    const entry = Object.entries(SCHEMAS).find(([, s]) => s.name === kind)
    if (!entry) {
        throw new Error(`unknown short id kind: ${kind}`)
    }
    const [key, schema] = entry
    const kindIndex = Number(key.split('-')[0])
    const values = schema.fields.map(name => {
        const value = fieldValues[name]
        if (value === undefined) {
            throw new Error(`missing field '${name}' for short id kind '${kind}'`)
        }
        return value
    })
    return [kindIndex, ...values]
}
