import * as React from "react";
import {useEffect, useReducer, useRef, useState} from "react";
import {
    Direction,
    GRID_LENGTH,
    GridArrowData,
    GridArrowDispatchAction,
    GridArrowDispatchActionType,
    HighlightableLetterData,
    LocationType,
    matchCoordinate,
    mergeTrays,
    PlayableLetterData,
    Settings,
    TileDispatchAction,
    TileDispatchActionType
} from "./utils";
import {TileExchangeModal} from "./TileExchange";
import {Grid, Scores, TileTray} from "./UI";
import {API, APIState, GameState, TurnAction, Tray, PlayedTile, Letter} from "./api";

function addLogInfo(existingLog: React.JSX.Element[], newItem: React.JSX.Element) {
    newItem = React.cloneElement(newItem, {key: existingLog.length})
    existingLog.push(newItem);
    return existingLog.slice();
}

export function Game(props: {
    api: API,
    settings: Settings,
    end_game_fn: () => void,
}) {

    const [api_state, setAPIState] = useState<APIState>(undefined);
    const [confirmedScorePoints, setConfirmedScorePoints] = useState<number>(-1);
    const currentTurnNumber = useRef<number>(-1);
    const historyProcessedNumber = useRef<number>(0);

    let isGameOver = false;
    if (api_state != null) {
        isGameOver = api_state.public_information.game_state.type === "Ended";
    }


    function waitForUpdate() {
        const result = props.api.load(true);

        result.then(
            (state) => {
                setAPIState(state);
            }
        )
            .catch((error) => {
                console.log("waitForUpdate() failed")
                console.log(error);
            });
    }

    const [boardLetters, setBoardLetters] = useState<HighlightableLetterData[]>(() => {
        const newLetterData = [] as HighlightableLetterData[];
        for (let i = 0; i < GRID_LENGTH * GRID_LENGTH; i++) {
            newLetterData.push(null);
        }
        return newLetterData;

    });

    function adjustGridArrow(existing: GridArrowData, update: GridArrowDispatchAction): GridArrowData {
        if (update.action == GridArrowDispatchActionType.CLEAR) {
            return null;
        } else if (update.action == GridArrowDispatchActionType.CYCLE) {
            // if there's nothing where the user clicked, we create a right arrow.
            if (existing == null || existing.position != update.position) {
                return {
                    direction: Direction.RIGHT, position: update.position
                }
                // if there's a right arrow, we shift to downwards
            } else if (existing.direction == Direction.RIGHT) {
                return {
                    direction: Direction.DOWN, position: existing.position
                }
                // if there's a down arrow, we clear it
            } else if (existing.direction == Direction.DOWN) {
                return null;
            }

        } else if (update.action == GridArrowDispatchActionType.SHIFT) {
            if (existing == null) {
                // no arrow to shift
                return null;
            } else {
                let current_x = existing.position % GRID_LENGTH;
                let current_y = Math.floor(existing.position / GRID_LENGTH);

                // we loop because we want to skip over letters that are already set
                while (current_x < GRID_LENGTH && current_y < GRID_LENGTH) {
                    if (existing.direction == Direction.RIGHT) {
                        current_x += 1;
                    } else {
                        current_y += 1;
                    }

                    const new_position = current_x + current_y * GRID_LENGTH;
                    let tray_letter_at_position = false; // need to also check if the player put a letter in the spot
                    for (const letter of update.playerLetters) {
                        if (letter != null && letter.location == LocationType.GRID && letter.index == new_position) {
                            tray_letter_at_position = true;
                            break
                        }
                    }

                    if (current_x < GRID_LENGTH && current_y < GRID_LENGTH && boardLetters[new_position] == null && !tray_letter_at_position) {
                        return {
                            direction: existing.direction,
                            position: new_position,
                        }
                    }

                }

                // if we reached this point without returning then we went off the board, remove arrow
                return null;
            }
        }
    }

    function exchangeFunction(selectedArray: Array<boolean>) {
        const result = props.api.exchange(selectedArray);

        result
            .then(
                (api_state) => {
                    setAPIState(api_state);
                })
            .catch((error) => {
                console.error({error});
                logDispatch(<div>{error}</div>);
            });

    }

    const [gridArrow, gridArrowDispatch] = useReducer(adjustGridArrow, null);
    const [logInfo, logDispatch] = useReducer(addLogInfo, []);

    useEffect(() => {
        props.api.load(false)
            .then((api_state) => {
                setAPIState(api_state);
            });
    }, []);

    function movePlayableLetters(playerLetters: PlayableLetterData[], update: TileDispatchAction) {
        if (update.action === TileDispatchActionType.RETRIEVE) {

            let tray: Tray = api_state.tray;

            if (update.override) {
                playerLetters = [];
            }

            return mergeTrays(playerLetters, tray.letters);
        } else if (update.action === TileDispatchActionType.MOVE) {

            let startIndex = matchCoordinate(playerLetters, update.start);
            let endIndex = matchCoordinate(playerLetters, update.end);

            if (startIndex != null) {
                let startLetter = playerLetters[startIndex];
                startLetter.location = update.end.location;
                startLetter.index = update.end.index;
            }

            if (endIndex != null) {
                let endLetter = playerLetters[endIndex];
                endLetter.location = update.start.location;
                endLetter.index = update.start.index;
            }

            setConfirmedScorePoints(-1);

            return playerLetters.slice();
        } else if (update.action === TileDispatchActionType.SET_BLANK) {
            const blankLetter = playerLetters[update.blankIndex];

            if (blankLetter.text !== update.newBlankValue) {
                blankLetter.text = update.newBlankValue;
                if (blankLetter.location == LocationType.GRID) {
                    setConfirmedScorePoints(-1);
                }
            }
            return playerLetters.slice();
        } else if (update.action === TileDispatchActionType.RETURN) {
            gridArrowDispatch({action: GridArrowDispatchActionType.CLEAR});
            return mergeTrays(playerLetters, playerLetters);
        } else if (update.action === TileDispatchActionType.MOVE_TO_ARROW) {
            // let's verify that the arrow is defined, otherwise do nothing
            if (gridArrow != null) {
                const end_position = {
                    location: LocationType.GRID,
                    index: gridArrow.position,
                };
                gridArrowDispatch({
                    action: GridArrowDispatchActionType.SHIFT,
                    playerLetters: playerLetters
                });
                return movePlayableLetters(playerLetters, {
                    action: TileDispatchActionType.MOVE,
                    start: update.start,
                    end: end_position,
                });
            } else {
                return playerLetters;
            }
        } else {
            console.error("Unknown tray update");
            console.error({update});
        }

    }

    const [playerLetters, trayDispatch] = useReducer(movePlayableLetters, []);
    const logDivRef = useRef(null);

    const [isTileExchangeOpen, setIsTileExchangeOpen] = useState<boolean>(false);

    useEffect(() => {
        // Effect is to keep the log window scrolled down
        if (logDivRef.current != null) {
            logDivRef.current.scrollTo(0, logDivRef.current.scrollHeight); // scroll down
        }
    }, [logInfo]);

    function handlePlayerAction(action: TurnAction, playerName: string) {

        if (action.type == "PlayTiles") {
            const result = action.result;
            result.words.sort((a, b) => b.score - a.score);
            for (let word of result.words) {
                logDispatch(<div>{playerName} received {word.score} points for playing '{word.word}.'</div>);
            }
            logDispatch(<div>{playerName} received a total of <strong>{result.total} points</strong> for their turn.
            </div>);
        } else if (action.type == "ExchangeTiles") {
            logDispatch(
                <div>{playerName} exchanged {action.tiles_exchanged} tile{action.tiles_exchanged > 1 ? 's' : ''} for
                    their turn.</div>);
        } else if (action.type == "Pass") {
            logDispatch(<div>{playerName} passed.</div>);
        } else if (action.type == "AddToDictionary") {
            logDispatch(<div>{playerName} added {action.word} to the dictionary.</div>)
        } else {
            console.error("Received unknown turn action: ", action);
        }

    }

    function endGame(state: GameState) {

        if (state.type != "InProgress") {
            logDispatch(<h4>Scoring</h4>);

            const scores = api_state.public_information.players;
            let pointsBonus = 0;
            for (const playerAndScore of scores) {
                const name = playerAndScore.name;

                if (name == state.finisher) {
                    // we'll do the finisher last
                    continue
                }

                const letters = state.remaining_tiles[name];
                if (letters.length == 0) {
                    logDispatch(<div>{name} has no remaining tiles.</div>);
                } else {
                    let pointsLost = 0;
                    let letterListStr = '';
                    for (let i = 0; i < letters.length; i++) {
                        const letter = letters[i];
                        const letterText = letter.is_blank ? 'a blank' : letter.text;
                        pointsLost += letter.points;

                        letterListStr += letterText;

                        // we're doing a list of 3 or more so add commas
                        if (letters.length > 2) {
                            if (i == letters.length - 2) {
                                letterListStr += ', and ';
                            } else if (i < letters.length - 2) {
                                letterListStr += ', ';
                            }
                        } else if (i == 0 && letters.length == 2) {
                            // list of 2
                            letterListStr += ' and ';
                        }

                    }
                    logDispatch(<div>{name} penalized {pointsLost} points for not using {letterListStr}.</div>);
                    pointsBonus += pointsLost;
                }
            }

            if (state.finisher != null) {
                logDispatch(<div>{state.finisher} receives {pointsBonus} bonus for completing first.</div>);
            }

            const highestScore = scores
                .map((score) => score.score)
                .sort((a, b) => b - a)
                .at(0);

            const playersAtHighest = scores.filter((score) => score.score == highestScore);
            let endGameMsg: string;

            if (playersAtHighest.length > 1 && state.finisher == null) {
                endGameMsg = "Tie game!";
            } else if (playersAtHighest.length > 1 && state.finisher != null) {
                // if there's a tie then the finisher gets the win
                endGameMsg = `${playersAtHighest[0].name} won by finishing first!`;
            } else {
                endGameMsg = `${playersAtHighest[0].name} won!`;
            }
            logDispatch(<h4>Game over - {endGameMsg}</h4>);
        } else {
            // what are we doing in this function?!
            console.error("endGame was called despite the state being InProgress!");
        }

    }

    function updateBoardLetters(newLetters: Array<Letter>) {
        const newLetterData = newLetters as HighlightableLetterData[];

        for (let i = 0; i < newLetterData.length; i++) {
            const newLetter = newLetterData[i];
            if (newLetter != null) {
                newLetter.highlight = false;
            }
        }

        // loop through the histories backwards until we reach our player
        for (let j = api_state.public_information.history.length - 1; j >= 0; j--) {
            const update = api_state.public_information.history[j];
            if (update.player == props.settings.playerName) {
                break
            }
            if (update.type.type === "PlayTiles") {
                for (let i of update.type.locations) {
                    newLetterData[i].highlight = true;
                }
            }
        }

        setBoardLetters(newLetterData);
    }

    useEffect(() => {
        if (api_state) {
            console.log("In state: ", api_state.public_information.current_turn_number);
            console.log("In ref: ", currentTurnNumber.current);
            console.debug(api_state);
            if(currentTurnNumber.current < api_state.public_information.current_turn_number){
                // We only clear everything if there's a chance the board changed
                // We may have gotten a dictionary update event which doesn't count
                gridArrowDispatch({action: GridArrowDispatchActionType.CLEAR});
                trayDispatch({action: TileDispatchActionType.RETRIEVE});
                setConfirmedScorePoints(-1);
                updateBoardLetters(api_state.public_information.board);
            }

            for (let i = historyProcessedNumber.current; i < api_state.public_information.history.length; i++) {
                const update = api_state.public_information.history[i];
                if (update.turn_number > currentTurnNumber.current) {
                    currentTurnNumber.current = update.turn_number;
                    logDispatch(<h4>Turn {update.turn_number + 1}</h4>);
                    const playerAtTurn = api_state.public_information.players[(update.turn_number) % api_state.public_information.players.length].name;
                    logDispatch(<div>{playerAtTurn}'s turn</div>);

                }
                handlePlayerAction(update.type, update.player);

            }
            historyProcessedNumber.current = api_state.public_information.history.length;

            if (!isGameOver) {
                if(api_state.public_information.current_turn_number > currentTurnNumber.current){
                    logDispatch(<h4>Turn {api_state.public_information.current_turn_number + 1}</h4>);
                    logDispatch(<div>{api_state.public_information.current_player}'s turn</div>);
                    currentTurnNumber.current = api_state.public_information.current_turn_number;
                }
            } else {
                endGame(api_state.public_information.game_state);
            }

            if(api_state.public_information.current_player != props.settings.playerName) {
                waitForUpdate();
            }
        }
    }, [api_state]);

    if(api_state == null){
        return <div>Still loading</div>;
    }

    const playerAndScores = api_state.public_information.players;
    const remainingTiles = api_state.public_information.remaining_tiles;
    const isPlayersTurn = api_state.public_information.current_player.toLowerCase() == props.settings.playerName.toLowerCase();

    return <>
        <TileExchangeModal
            playerLetters={playerLetters}
            isOpen={isTileExchangeOpen}
            setOpen={setIsTileExchangeOpen}
            exchangeFunction={exchangeFunction}
        />
        <div className="board-log">
            <Grid
                cellTypes={api_state.public_information.cell_types}
                playerLetters={playerLetters}
                boardLetters={boardLetters}
                tileDispatch={trayDispatch}
                arrow={gridArrow}
                arrowDispatch={gridArrowDispatch}
            />
            <div className="message-log">
                <button className="end-game"
                        onClick={() => {
                            props.end_game_fn();
                        }}
                >
                    End Game
                </button>
                <Scores playerScores={playerAndScores}/>
                <div className="log" ref={logDivRef}>
                    {logInfo}
                </div>
            </div>
        </div>
        <div className="tray-row">
            <div>
                <div>
                    {remainingTiles} letters remaining
                </div>
                <button
                    disabled={remainingTiles == 0 || isGameOver || !isPlayersTurn}
                    onClick={() => {
                        trayDispatch({action: TileDispatchActionType.RETURN}); // want all tiles back on tray for tile exchange
                        setIsTileExchangeOpen(true);
                    }}>Open Tile Exchange
                </button>
            </div>
            <TileTray letters={playerLetters} trayLength={props.settings.trayLength} trayDispatch={trayDispatch}/>
            <div className="player-controls">
                <button
                    className="check"
                    disabled={isGameOver || !isPlayersTurn}
                    onClick={async () => {
                        const playedTiles = playerLetters.map((i) => {
                            if (i == null) {
                                return null;
                            }

                            if (i.location === LocationType.GRID) {
                                let result: PlayedTile = {
                                    index: i.index,
                                    character: null
                                };
                                if (i.is_blank) {
                                    result.character = i.text;
                                }

                                return result;
                            }

                            return null;
                        });

                        const committing = confirmedScorePoints > -1;
                        const result = props.api.play(playedTiles, committing);

                        result
                            .then(
                                (api_state) => {
                                    console.log("Testing45")
                                    console.log({api_state});

                                    const play_tiles: TurnAction = api_state.update.type;
                                    if (play_tiles.type == "PlayTiles") {
                                        setConfirmedScorePoints(play_tiles.result.total);

                                        if (committing) {
                                            setAPIState(api_state);
                                        }

                                    } else {
                                        console.error("Inaccessible branch!!!")
                                    }
                                }
                            )
                            .catch((error) => {
                                console.error({error});

                                if (error.endsWith(" is not a valid word")) {
                                    const word = error.split(' ')[0];
                                    // For whatever reason I can't pass props.api.add_to_dictionary directly
                                    logDispatch(<AddWordButton word={word}
                                                               addWordFn={(word) => {
                                                                   props.api.add_to_dictionary(word)
                                                                       .then((api_state) => {
                                                                           setAPIState(api_state);
                                                                       })
                                                               }}/>);
                                } else {
                                    logDispatch(<div>{error}</div>);
                                }


                            });

                    }}>{confirmedScorePoints > -1 ? `Score ${confirmedScorePoints} points` : "Check"}</button>
                <button
                    className="return"
                    disabled={isGameOver}
                    onClick={() => {
                        trayDispatch({action: TileDispatchActionType.RETURN});
                    }}>Return Tiles
                </button>
                <button
                    className="pass"
                    disabled={isGameOver || !isPlayersTurn}
                    onClick={() => {
                        if (window.confirm("Are you sure you want to pass?")) {
                            const result = props.api.pass();

                            result
                                .then(
                                    (api_state) => {
                                        setAPIState(api_state);
                                    })
                                .catch((error) => {
                                    console.error({error});
                                    logDispatch(<div>{error}</div>);
                                });
                        }
                    }}>Pass
                </button>

            </div>
        </div>
    </>;

}

function AddWordButton(props: { word: string, addWordFn: (x: string) => void }) {
    const [isClicked, setIsClicked] = useState<boolean>(false);

    if (!isClicked) {
        return <div>
            <em>{props.word} is not a valid word.</em>
            <button
                className="add-to-dictionary"
                disabled={isClicked}
                onClick={() => {
                    setIsClicked(true);
                    props.addWordFn(props.word);
                }}>
                Add to dictionary
            </button>
        </div>;
    } else {
        return <div>
            <em>Adding {props.word} to dictionary.</em>
        </div>;
    }

}

