import { encode as encodeAsBase64 } from '@borderless/base64'

import { CubeOwner } from '../CubeState'
import { GameState } from '../GameState'
import { MatchState } from '../MatchState'
import { ResignOffer } from '../ResignOffer'

/**
 * 指定の局面から、GNU Backgammon仕様のMatchIDを含むオブジェクトを生成する
 *
 * @param matchState マッチスコアの状態
 * @param gameState 手番など、現在のゲームの状態
 * @returns
 */
export function toMatchID(matchState: MatchState, gameState: GameState) {
    const cubeState = gameState.cubeState

    // 1. Bit 1-4 contains the 2-logarithm of the cube value. For example, a 8-cube is encoded as 0011 binary (or 3), since 2 to the power of 3 is 8. The maximum value of the cube in with this encoding is 2 to the power of 15, i.e., a 32768-cube.
    const bit1_4 = Math.log2(cubeState.value)

    // 2. Bit 5-6 contains the cube owner. 00 if player 0 owns the cube, 01 if player 1 owns the cube, or 11 for a centered cube.
    const bit5_6 =
        cubeState.owner === CubeOwner.RED
            ? 0
            : cubeState.owner == CubeOwner.WHITE
            ? 1
            : 3

    // 3. Bit 7 is the player on roll or the player who did roll (0 and 1 for player 0 and 1, respectively).
    const bit7 =
        gameState.tag === 'GSInPlay'
            ? // キューブレスポンス中はロールしたプレイヤーとレスポンス中のプレイヤーが逆になる
              gameState.isDoubleOffered
                ? gameState.isRed
                    ? 1
                    : 0
                : gameState.isRed
                ? 0
                : 1
            : 0

    // 4. Bit 8 is the Crawford flag: 1 if this game is the Crawford game, 0 otherwise.
    const bit8 = matchState.isCrawford ? 1 : 0

    // 5. Bit 9-11 is the game state: 000 for no game started, 001 for playing a game, 010 if the game is over, 011 if the game was resigned, or 100 if the game was ended by dropping a cube.
    const bit9_11 =
        gameState.tag === 'GSOpening'
            ? 0
            : gameState.tag === 'GSInPlay'
            ? 1
            : gameState.isWonByPass
            ? 4
            : gameState.isWonByResign
            ? 3
            : 2

    // 6. Bit 12 indicates whose turn it is. For example, suppose player 0 is on roll then bit 7 above will be 0. Player 0 now decides to double, this will make bit 12 equal to 1, since it is now player 1's turn to decide whether she takes or passes the cube.
    const bit12 =
        gameState.tag === 'GSOpening' || gameState.tag === 'GSEoG'
            ? 0 // value for these cases are undefined
            : gameState.isRed
            ? 0
            : 1

    // 7. Bit 13 indicates whether an doubled is being offered. 0 if no double is being offered and 1 if a double is being offered.
    const bit13 =
        gameState.tag === 'GSInPlay' && gameState.isDoubleOffered ? 1 : 0

    // Bit 14-15 indicates whether an resignation was offered. 00 for no resignation, 01 for resign of a single game, 10 for resign of a gammon, or 11 for resign of a backgammon. The player offering the resignation is the inverse of bit 12, e.g., if player 0 resigns a gammon then bit 12 will be 1 (as it is now player 1 now has to decide whether to accept or reject the resignation) and bit 13-14 will be 10 for resign of a gammon.
    const bit14_15 = isResignOffered(gameState)

    // Bit 16-18 and bit 19-21 is the first and second die, respectively. 0 if the dice has not yet be rolled, otherwise the binary encoding of the dice, e.g., if 5-2 was rolled bit 16-21 will be 101-010.
    const { dice1, dice2 } = dices(gameState)
    const bit16_18 = dice1 > dice2 ? dice1 : dice2
    const bit19_21 = dice1 > dice2 ? dice2 : dice1

    // Bit 22 to 36 is the match length. The maximum value for the match length is 32767. A match score of zero indicates that the game is a money game.
    const bit22_36 = matchState.matchLength

    // Bit 37-51 and bit 52-66 is the score for player 0 and player 1 respectively. The maximum value of the match score is 32767.
    const matchScore = matchState.isEoG
        ? matchState.scoreAfter
        : matchState.scoreBefore
    const bit37_51 = matchScore.redScore
    const bit52_66 = matchScore.whiteScore

    // Bit67 : no Jacoby: これはドキュメントに記載がない：Jacobyが無効（ポイントマッチ）なら1, 有効（マネーゲームなど）なら0
    const bit67 = matchState.stakeConf.jacobyRule ? 0 : 1

    const toEnc = [
        { bit: bit1_4, len: 4 },
        { bit: bit5_6, len: 2 },
        { bit: bit7 },
        { bit: bit8 },
        { bit: bit9_11, len: 3 },
        { bit: bit12 },
        { bit: bit13 },
        { bit: bit14_15, len: 2 },
        { bit: bit16_18, len: 3 },
        { bit: bit19_21, len: 3 },
        { bit: bit22_36, len: 15 },
        { bit: bit37_51, len: 15 },
        { bit: bit52_66, len: 15 },
        { bit: bit67 },
    ]

    const buffer = new ArrayBuffer(9)
    const reducer = littleEndianReducer(buffer)
    toEnc.map(revertBits).reduce(reducer)
    return {
        matchID: encodeAsBase64(buffer).substring(0, 12),
        cube: bit1_4,
        cubeOwner: bit5_6,
        diceOwner: bit7,
        crawford: bit8,
        gameState: bit9_11,
        turnOwner: bit12,
        double: bit13,
        resign: bit14_15,
        dice1: bit16_18,
        dice2: bit19_21,
        matchLen: bit22_36,
        score1: bit37_51,
        score2: bit52_66,
        noJacoby: bit67,
        bit: Array.from(new Uint8Array(buffer)).map((b) => b.toString(2)),
    }
}

function dices(gameState: GameState): { dice1: number; dice2: number } {
    return gameState.tag === 'GSInPlay' && gameState.dices !== undefined
        ? {
              dice1: gameState.dices[0].pip,
              dice2: gameState.dices[1].pip,
          }
        : { dice1: 0, dice2: 0 }
}

function isResignOffered(gameState: GameState) {
    if (
        gameState.tag == 'GSOpening' ||
        gameState.tag == 'GSEoG' ||
        !gameState.isResignOffered ||
        gameState.offer == undefined
    ) {
        return 0
    }
    const offer = gameState.offer
    switch (offer) {
        case ResignOffer.Single:
            return 1
        case ResignOffer.Gammon:
            return 2
        case ResignOffer.Backgammon:
            return 3
    }
}

function revertBits(v: Bit): Bit {
    let n = v.bit
    let ret = 0
    for (let i = 0; i < (v.len ?? 1); i++) {
        ret = (ret << 1) | (n & 1)
        n = n >> 1
    }
    return { bit: ret, len: v.len }
}

/**
 * 与えられたBitオブジェクトを8bitずつリトルエンディアンでまとめてBufferに書き込むreducerを生成する
 * @param buffer
 * @param pos
 * @returns
 */
export function littleEndianReducer(
    buffer: ArrayBuffer,
    pos = 0
): (prev: Bit, value: Bit, idx: number, arr: Bit[]) => Bit {
    const dataView = new DataView(buffer)
    const byteWriter = {
        pos,
        write: (value: number) => {
            if (byteWriter.pos < buffer.byteLength) {
                dataView.setUint8(byteWriter.pos, revertByte(value))
                byteWriter.pos += 1
            }
        },
    }

    return (prev: Bit, value: Bit, idx: number, arr: Bit[]) => {
        // データの最後になったら、とにかく1バイト分書くように補完する
        const isLast = idx == arr.length - 1
        return accumulateBit(prev, value, byteWriter.write, isLast)
    }

    // little endian に変換
    function revertByte(v: number) {
        return (
            (v & 1) * 128 +
            ((v >> 1) & 1) * 64 +
            ((v >> 2) & 1) * 32 +
            ((v >> 3) & 1) * 16 +
            ((v >> 4) & 1) * 8 +
            ((v >> 5) & 1) * 4 +
            ((v >> 6) & 1) * 2 +
            ((v >> 7) & 1)
        )
    }
}

/**
 * 任意の長さのビット列を表す
 */
export type Bit = {
    bit: number
    len?: number
}

/**
 * 与えられたビット列を既存のビット列の後に接続し、1バイト以上になれば
 * 各バイトごとにbyteConsumerに渡して切り捨てた結果を返す
 * @param prev 既存のビット列
 * @param value prevに付加するビット列
 * @param byteConsumer 1バイトごとに呼ばれる関数
 * @param isLast valueが最後であるならtrue(1バイトに足りない分を0で詰めてbyteConsumerを呼ぶ)
 * @returns
 */
function accumulateBit(
    prev: Bit,
    value: Bit,
    byteConsumer: (b: number) => void,
    isLast = false
): Bit {
    let { bit: curBit = 0 } = prev
    const { len: lastLen = 0 } = prev
    let { bit, len = 1 } = value

    // bitが1バイト分に満たないうちは、追加されたbitを保持する
    // ただし、データの最後になったら、とにかく1バイト分書く
    let curLen = lastLen + len
    if (curLen < 8) {
        if (!isLast) {
            return {
                bit: (curBit << len) | bit,
                len: curLen,
            }
        }
        // データの最後で、書き込む分が1バイトに満たなければ0を詰める
        bit = bit << (8 - curLen)
        len = len + (8 - curLen)
    }

    // オーバーフローしないようにマスク
    bit = bit & ((1 << len) - 1)

    // まず以前から引き継いでいるbitがあれば、1バイト分書き出す
    if (lastLen > 0) {
        const shiftForLast = 8 - lastLen
        const shiftForCur = len - shiftForLast // == lenToWrite - 8 なので、常に > 0
        const valueToWrite = (curBit << shiftForLast) | (bit >> shiftForCur)

        byteConsumer(valueToWrite)

        curLen -= 8
        curBit = ((1 << shiftForCur) - 1) & bit
    } else {
        curBit = bit
    }

    // 新規追加のbitから1バイト分書ける分を全部書き出す
    while (curLen >= 8) {
        const shiftForCur = curLen - 8
        const valueToWrite = (curBit >> shiftForCur) & 255
        byteConsumer(valueToWrite)

        curLen -= 8
        curBit = ((1 << shiftForCur) - 1) & curBit
    }

    // 最後の場合は、まだ残っているビットに0を詰めて出力する
    if (isLast && curLen > 0) {
        const valueToWrite = (curBit << (8 - curLen)) & 255
        byteConsumer(valueToWrite)
        // 一応一貫性のある値を返させる
        curLen = 0
        curBit = 0
    }

    // 1バイトに満たない分を次に引き継ぐ(len<8が保証される)
    return { bit: curBit, len: curLen }
}
