import { DicePip } from './Dices'
import { eog, EOGStatus } from './EOGStatus'
import { standardConf } from './GameConf'

/**
 * 相対表記で表現した盤面。常に動かす側の観点になっていて、0はバーポイント、25は上がり、
 * 正の数が自分の駒数となっている。内部的な盤の操作はすべてこのインターフェースを介して行い、
 * 外部へ表示する際にはAbsoluteBoardで変換する
 */
export interface BoardState {
    /** 各ポイントの駒数を格納した配列。正の数は自駒、負の数は相手駒 */
    points: number[]

    /** 最後尾の駒の位置 */
    lastPiecePos: number

    /** 相手の最後尾の駒の位置 */
    opponentLastPiecePos: number

    /** ベアリングオフの行き先となる位置（標準は25） */
    bearOffPos: number

    /** 指定の位置を相手側の視点で見た時の位置に変換する（e.g. 1=>24, 6=>18, ...）*/
    invertPos(pos: number): number

    /** ベアリングオフできるならtrue */
    isBearable: boolean

    /** 各ポイントの駒数を返す。正の数は自駒、負の数は相手駒 */
    piecesAt(n: number): number

    /** ベアリングオフ済みの自駒数を返す */
    myBornOff: number

    /** ベアリングオフ済みの相手駒数を返す */
    opponentBornOff: number

    /** 指定されたポイントの駒をpip分進めた盤面を返す。
     * ポイントに駒がなかったり、行き先がブロックされている・範囲外などの場合、自分自身が返ってくる */
    movePiece(from: number, pip: DicePip): BoardState

    /** 盤面を相手側視点に変換する */
    revert(): BoardState

    /** 盤面で自分側の駒が全て上がったかどうかを、ギャモン・バックギャモンの有無とあわせて返す。
     * ギャモン・バックギャモンは純粋に自駒の配置で判断され、ジャコビールールなどは適用されない。
     * 相手側が終局かどうかは影響しない。
     */
    eogStatus(): EOGStatus

    /**
     * 自分のピップカウント
     */
    myPipCount: number

    /**
     * 相手のピップカウント
     */
    opponentPipCount: number

    /**
     * 終局状態に達しているならtrue
     */
    isEndOfGame(): boolean

    /**
     * 終局状態に達していて、かつギャモンならtrue
     */
    isGammonish(): boolean

    /**
     * 終局状態に達していて、かつバックギャモンならtrue
     */
    isBackgammonishAlso(): boolean

    /**
     * ランニングゲーム（駒同士のコンタクトがない）状態ならtrue
     */
    isRunningGame(): boolean

    /**
     * 自分のまだ上がっていない駒の数
     */
    pieceCount: number

    /**
     * 相手のまだ上がっていない駒の数
     */
    opponentPieceCount: number

    /**
     * 自分のインナーボードの開始点（標準ルールでは19）
     */
    innerPos: number
}

/**
 * BoardStateの実装に必要な機能で、外部には公開する必要がないものの定義
 */
type Board = {
    // all gone
} & BoardState

function countWhitePieces(pieces: number[]) {
    return pieces.filter((n) => n > 0).reduce((n, m) => n + m, 0)
}

function countRedPieces(pieces: number[]) {
    return countWhitePieces(pieces.map((n) => -n))
}

/**
 * 駒の配置を格納した配列から、BoardStateを生成する
 *
 * @param pieces 駒の配置（省略時は標準ルールでの開始時な配置）
 * @param bornOffs すでにベアリングオフした駒の数の対（順に自分、相手：省略時は0）
 * @param innerPos この値とそれ以降がインナーボードとなる
 * @param isEoGFunc 終局判定関数（終局時にtrueを返す）
 * @returns 盤面
 */
export function boardState(
    pieces: number[] = standardConf.initialPos,
    bornOffs: [number, number] = [0, 0],
    innerPos = 19,
    isEoGFunc: (board: Board) => boolean = (board: Board) =>
        board.pieceCount === 0
): BoardState {
    return initBoardState(pieces, bornOffs, innerPos, isEoGFunc)
}

function posInverterFor(points: number[]): (pos: number) => number {
    return (pos: number) => points.length - 1 - pos
}

function initBoardState(
    points: number[],
    bornOffs: [number, number] = [0, 0],
    innerPos: number,
    isEoGFunc: (board: Board) => boolean
): BoardState {
    const myBornOff = bornOffs[0]
    const opponentBornOff = bornOffs[1]
    const pieceCount = countWhitePieces(points)
    const opponentPieceCount = countRedPieces(points)
    const bearOffPos = points.length - 1

    const reverted = revertPoints(points)
    const invertPos = posInverterFor(points)

    const lastPiecePos = points.findIndex((n) => 0 < n)
    const found = reverted.findIndex((n) => 0 < n)
    const opponentLastPiecePos = found === -1 ? 0 : invertPos(found)

    const isBearable = innerPos <= lastPiecePos

    const myPipCount = countPip(points)
    const opponentPipCount = countPip(reverted)

    const board = {
        points,
        pieceCount,
        opponentPieceCount,
        bearOffPos,
        invertPos,
        myBornOff,
        opponentBornOff,
        lastPiecePos,
        opponentLastPiecePos,
        isBearable,
        eogStatus() {
            const isEndOfGame = this.isEndOfGame()
            const isGammon = isEndOfGame && this.isGammonish()
            const isBackgammon = isGammon && this.isBackgammonishAlso()
            return eog({
                isEndOfGame,
                isGammon,
                isBackgammon,
            })
        },
        isEndOfGame(): boolean {
            return isEoGFunc(this)
        },
        isGammonish(): boolean {
            return this.opponentBornOff === 0
        },
        isBackgammonishAlso(): boolean {
            // 自分のinnerPosとバーの間が、相手のアウターになる
            const outerPos = points.length - innerPos // 7 = 26 - 19
            const opponentOuterAndBar = [...Array(outerPos)].map(
                (_, index) => index + innerPos
            )
            return (
                opponentOuterAndBar
                    .map((pos) => this.points[pos])
                    .filter((c) => c < 0)
                    .reduce((m, n) => m + n, 0) < 0
            )
        },
        isRunningGame(): boolean {
            return this.lastPiecePos < this.opponentLastPiecePos
        },
        piecesAt(n: number): number {
            return this.points[n]
        },
        movePiece(from: number, pip: number): Board {
            return doMove(this, from, pip, innerPos)
        },

        revert(): Board {
            const lastPiecePos = invertPos(this.opponentLastPiecePos)
            const opponentLastPiecePos = invertPos(this.lastPiecePos)
            const isBearable = innerPos <= lastPiecePos

            return {
                ...this,
                points: revertPoints(this.points),
                myBornOff: this.opponentBornOff,
                opponentBornOff: this.myBornOff,
                pieceCount: this.opponentPieceCount,
                opponentPieceCount: this.pieceCount,
                lastPiecePos,
                opponentLastPiecePos,
                isBearable,
                myPipCount: this.opponentPipCount,
                opponentPipCount: this.myPipCount,
            }
        },
        myPipCount,
        opponentPipCount,
        innerPos,
        outerPos: points.length - 1 - innerPos, // 6 = 25 - 19
    }

    return board
}

function revertPoints(points: number[]): number[] {
    const invertPos = posInverterFor(points)
    return points.map((_, index) => {
        const n = -points[invertPos(index)]

        // remove negative 0
        return n === 0 ? 0 : n
    })
}

function doMove(
    board: Board,
    from: number,
    pip: number,
    innerPos: number
): Board {
    const boardSize = board.points.length - 1 // 25
    // 動かそうとする駒の位置が範囲外
    if (from < 0 || boardSize < from) {
        return board
    }

    // 動かそうとする場所に駒がない
    const piecesToMove = board.points[from]
    if (piecesToMove <= 0) {
        return board
    }

    // ベアオフではなく、行先がブロックされている
    const to = from + pip > boardSize ? boardSize : from + pip
    const isBearOff = boardSize <= to
    if (!isBearOff && board.points[to] < -1) {
        return board
    }

    // 駒を取り上げる
    const piecesAfter = board.points.slice()
    piecesAfter[from] = piecesAfter[from] - 1

    // 必要なら自分の最後尾の駒の位置を更新する

    // 上がりなら、上がり数を更新して終了
    if (isBearOff) {
        const lastPiecePos = recalcLastPiecePos(from, board, piecesAfter)
        const myPipCount = board.myPipCount - (boardSize - from)
        return {
            ...board,
            points: piecesAfter,
            myBornOff: board.myBornOff + 1,
            pieceCount: board.pieceCount - 1,
            lastPiecePos,
            myPipCount,
        }
    }

    let opponentLastPiecePos
    let opponentPipCount = board.opponentPipCount
    // ヒット
    if (piecesAfter[to] === -1) {
        const bar = boardSize

        piecesAfter[to] = 0
        piecesAfter[bar] = piecesAfter[bar] - 1

        // 相手の最後尾の駒の位置をバーに更新する
        opponentLastPiecePos = boardSize

        // ヒットした分のpipcountを更新
        opponentPipCount += to
    } else {
        // ヒットでなければそのまま
        opponentLastPiecePos = board.opponentLastPiecePos
    }

    // 移動先に駒を置く
    piecesAfter[to] = piecesAfter[to] + 1
    const lastPiecePos = recalcLastPiecePos(from, board, piecesAfter)

    // 上がれるかどうかの更新が必要なのは、上がりでない時だけ
    const isBearable = innerPos <= lastPiecePos

    // 自分の分のピップカウントは進めた分だけ減る
    const myPipCount = board.myPipCount - pip
    return {
        ...board,
        points: piecesAfter,
        lastPiecePos,
        opponentLastPiecePos,
        isBearable,
        myPipCount,
        opponentPipCount,
    }
}

function recalcLastPiecePos(from: number, board: Board, piecesAfter: number[]) {
    return from == board.lastPiecePos && board.points[from] == 1
        ? piecesAfter.findIndex((n) => 0 < n)
        : board.lastPiecePos
}

function countPip(points: number[]) {
    return points.reduce((prev, cur, idx) => {
        prev += cur > 0 ? cur * (points.length - 1 - idx) : 0
        return prev
    }, 0)
}
