import {CSSProperties, Dispatch, SetStateAction, useCallback, useMemo, useRef, useState,} from "react"
import "../style/editor.css"
import TitleInput from "../components/TitleInput"
import plainCourt from "../assets/court/full_court.svg"
import halfCourt from "../assets/court/half_court.svg"

import {BallPiece} from "../components/editor/BallPiece"

import {Rack} from "../components/Rack"
import {PlayerPiece} from "../components/editor/PlayerPiece"
import {Player} from "../tactic/Player"

import {Tactic, TacticContent} from "../tactic/Tactic"
import {fetchAPI} from "../Fetcher"
import {Team} from "../tactic/Team"
import {calculateRatio} from "../Utils"

import SavingState, {SaveState, SaveStates,} from "../components/editor/SavingState"

import {CourtObject} from "../tactic/CourtObjects"
import {CourtAction} from "./editor/CourtAction"
import {BasketCourt} from "../components/editor/BasketCourt";

const ERROR_STYLE: CSSProperties = {
    borderColor: "red",
}

const GUEST_MODE_CONTENT_STORAGE_KEY = "guest_mode_content"
const GUEST_MODE_TITLE_STORAGE_KEY = "guest_mode_title"

export interface EditorViewProps {
    tactic: Tactic
    onContentChange: (tactic: TacticContent) => Promise<SaveState>
    onNameChange: (name: string) => Promise<boolean>
    courtType: "PLAIN" | "HALF"
}

export interface EditorProps {
    id: number
    name: string
    content: string
    courtType: "PLAIN" | "HALF"
}

/**
 * information about a player that is into a rack
 */
interface RackedPlayer {
    team: Team
    key: string
}

type RackedCourtObject = { key: "ball" }

export default function Editor({ id, name, courtType, content }: EditorProps) {
    const isInGuestMode = id == -1

    const storage_content = localStorage.getItem(GUEST_MODE_CONTENT_STORAGE_KEY)
    const editorContent =
        isInGuestMode && storage_content != null ? storage_content : content

    const storage_name = localStorage.getItem(GUEST_MODE_TITLE_STORAGE_KEY)
    const editorName =
        isInGuestMode && storage_name != null ? storage_name : name

    return (
        <EditorView
            tactic={{
                name: editorName,
                id,
                content: JSON.parse(editorContent),
            }}
            onContentChange={async (content: TacticContent) => {
                if (isInGuestMode) {
                    localStorage.setItem(
                        GUEST_MODE_CONTENT_STORAGE_KEY,
                        JSON.stringify(content),
                    )
                    return SaveStates.Guest
                }
                return fetchAPI(`tactic/${id}/save`, { content }).then((r) =>
                    r.ok ? SaveStates.Ok : SaveStates.Err,
                )
            }}
            onNameChange={async (name: string) => {
                if (isInGuestMode) {
                    localStorage.setItem(GUEST_MODE_TITLE_STORAGE_KEY, name)
                    return true //simulate that the name has been changed
                }
                return fetchAPI(`tactic/${id}/edit/name`, { name }).then(
                    (r) => r.ok,
                )
            }}
            courtType={courtType}
        />
    )
}

function EditorView({
    tactic: { id, name, content: initialContent },
    onContentChange,
    onNameChange,
    courtType,
}: EditorViewProps) {
    const isInGuestMode = id == -1

    const [titleStyle, setTitleStyle] = useState<CSSProperties>({})
    const [content, setContent, saveState] = useContentState(
        initialContent,
        isInGuestMode ? SaveStates.Guest : SaveStates.Ok,
        useMemo(
            () =>
                debounceAsync(
                    (content) =>
                        onContentChange(content).then((success) =>
                            success ? SaveStates.Ok : SaveStates.Err,
                        ),
                    250,
                ),
            [onContentChange],
        ),
    )

    const [allies, setAllies] = useState(
        getRackPlayers(Team.Allies, content.players),
    )
    const [opponents, setOpponents] = useState(
        getRackPlayers(Team.Opponents, content.players),
    )

    const [objects, setObjects] = useState<RackedCourtObject[]>(
        isBallOnCourt(content) ? [] : [{ key: "ball" }],
    )

    const courtDivContentRef = useRef<HTMLDivElement>(null)

    const isBoundsOnCourt = (bounds: DOMRect) => {
        const courtBounds = courtDivContentRef.current!.getBoundingClientRect()

        // check if refBounds overlaps courtBounds
        return !(
            bounds.top > courtBounds.bottom ||
            bounds.right < courtBounds.left ||
            bounds.bottom < courtBounds.top ||
            bounds.left > courtBounds.right
        )
    }

    const onPieceDetach = (ref: HTMLDivElement, element: RackedPlayer) => {
        const refBounds = ref.getBoundingClientRect()
        const courtBounds = courtDivContentRef.current!.getBoundingClientRect()

        const { x, y } = calculateRatio(refBounds, courtBounds)

        setContent((content) => {
            return {
                ...content,
                players: [
                    ...content.players,
                    {
                        id: "player-" + content.players.length,
                        team: element.team,
                        role: element.key,
                        rightRatio: x,
                        bottomRatio: y,
                        hasBall: false,
                    },
                ],
                actions: content.actions,
            }
        })
    }

    const onObjectDetach = (
        ref: HTMLDivElement,
        rackedObject: RackedCourtObject,
    ) => {
        const refBounds = ref.getBoundingClientRect()
        const courtBounds = courtDivContentRef.current!.getBoundingClientRect()

        const { x, y } = calculateRatio(refBounds, courtBounds)

        let courtObject: CourtObject

        switch (rackedObject.key) {
            case "ball":
                const ballObj = content.objects.findIndex(
                    (o) => o.type == "ball",
                )
                const playerCollidedIdx = getPlayerCollided(
                    refBounds,
                    content.players,
                )
                if (playerCollidedIdx != -1) {
                    onBallDropOnPlayer(playerCollidedIdx)
                    setContent((content) => {
                        return {
                            ...content,
                            objects: content.objects.toSpliced(ballObj, 1),
                        }
                    })
                    return
                }

                courtObject = {
                    type: "ball",
                    rightRatio: x,
                    bottomRatio: y,
                }
                break

            default:
                throw new Error("unknown court object ", rackedObject.key)
        }

        setContent((content) => {
            return {
                ...content,
                objects: [...content.objects, courtObject],
            }
        })
    }

    const getPlayerCollided = (
        bounds: DOMRect,
        players: Player[],
    ): number | -1 => {
        for (let i = 0; i < players.length; i++) {
            const player = players[i]
            const playerBounds = document
                .getElementById(player.id)!
                .getBoundingClientRect()
            const doesOverlap = !(
                bounds.top > playerBounds.bottom ||
                bounds.right < playerBounds.left ||
                bounds.bottom < playerBounds.top ||
                bounds.left > playerBounds.right
            )
            if (doesOverlap) {
                return i
            }
        }
        return -1
    }

    const onBallDropOnPlayer = (playerCollidedIdx: number) => {
        setContent((content) => {
            const ballObj = content.objects.findIndex((o) => o.type == "ball")
            let player = content.players.at(playerCollidedIdx) as Player
            return {
                ...content,
                players: content.players.toSpliced(playerCollidedIdx, 1, {
                    ...player,
                    hasBall: true,
                }),
                objects: content.objects.toSpliced(ballObj, 1),
            }
        })
    }

    const onBallDrop = (refBounds: DOMRect) => {
        if (!isBoundsOnCourt(refBounds)) {
            removeCourtBall()
            return
        }
        const playerCollidedIdx = getPlayerCollided(refBounds, content.players)
        if (playerCollidedIdx != -1) {
            setContent((content) => {
                return {
                    ...content,
                    players: content.players.map((player) => ({
                        ...player,
                        hasBall: false,
                    })),
                }
            })
            onBallDropOnPlayer(playerCollidedIdx)
            return
        }

        if (content.objects.findIndex((o) => o.type == "ball") != -1) {
            return
        }

        const courtBounds = courtDivContentRef.current!.getBoundingClientRect()
        const { x, y } = calculateRatio(refBounds, courtBounds)
        let courtObject: CourtObject

        courtObject = {
            type: "ball",
            rightRatio: x,
            bottomRatio: y,
        }

        setContent((content) => {
            return {
                ...content,
                players: content.players.map((player) => ({
                    ...player,
                    hasBall: false,
                })),
                objects: [...content.objects, courtObject],
            }
        })
    }

    const removePlayer = (player: Player) => {
        setContent((content) => ({
            ...content,
            players: toSplicedPlayers(content.players, player, false),
            objects: [...content.objects],
            actions: content.actions.filter(a => a.toPlayerId !== player.id && a.fromPlayerId !== player.id),
        }))
        let setter
        switch (player.team) {
            case Team.Opponents:
                setter = setOpponents
                break
            case Team.Allies:
                setter = setAllies
        }
        if (player.hasBall) {
            setObjects([{ key: "ball" }])
        }
        setter((players) => [
            ...players,
            {
                team: player.team,
                pos: player.role,
                key: player.role,
            },
        ])
    }

    const removeCourtBall = () => {
        setContent((content) => {
            const ballObj = content.objects.findIndex((o) => o.type == "ball")
            return {
                ...content,
                players: content.players.map((player) => ({
                    ...player,
                    hasBall: false,
                })),
                objects: content.objects.toSpliced(ballObj, 1),
            }
        })
        setObjects([{ key: "ball" }])
    }

    return (
        <div id="main-div">
            <div id="topbar-div">
                <div id="topbar-left">
                    <SavingState state={saveState} />
                </div>
                <div id="title-input-div">
                    <TitleInput
                        style={titleStyle}
                        default_value={name}
                        on_validated={(new_name) => {
                            onNameChange(new_name).then((success) => {
                                setTitleStyle(success ? {} : ERROR_STYLE)
                            })
                        }}
                    />
                </div>
                <div id="topbar-right" />
            </div>
            <div id="edit-div">
                <div id="racks">
                    <Rack
                        id="allies-rack"
                        objects={allies}
                        onChange={setAllies}
                        canDetach={(div) =>
                            isBoundsOnCourt(div.getBoundingClientRect())
                        }
                        onElementDetached={onPieceDetach}
                        render={({ team, key }) => (
                            <PlayerPiece
                                team={team}
                                text={key}
                                key={key}
                                hasBall={false}
                            />
                        )}
                    />

                    <Rack
                        id={"objects"}
                        objects={objects}
                        onChange={setObjects}
                        canDetach={(div) =>
                            isBoundsOnCourt(div.getBoundingClientRect())
                        }
                        onElementDetached={onObjectDetach}
                        render={renderCourtObject}
                    />

                    <Rack
                        id="opponent-rack"
                        objects={opponents}
                        onChange={setOpponents}
                        canDetach={(div) =>
                            isBoundsOnCourt(div.getBoundingClientRect())
                        }
                        onElementDetached={onPieceDetach}
                        render={({ team, key }) => (
                            <PlayerPiece
                                team={team}
                                text={key}
                                key={key}
                                hasBall={false}
                            />
                        )}
                    />
                </div>
                <div id="court-div">
                    <div id="court-div-bounds">
                        <BasketCourt
                            players={content.players}
                            objects={content.objects}
                            onBallMoved={onBallDrop}
                            courtImage={
                                courtType == "PLAIN" ? plainCourt : halfCourt
                            }
                            courtRef={courtDivContentRef}
                            actions={content.actions}
                            setActions={(actions) =>
                                setContent((content) => ({
                                    ...content,
                                    players: content.players,
                                    actions: actions(content.actions),
                                }))
                            }
                            renderAction={(action, i) => (
                                <CourtAction
                                    key={i}
                                    action={action}
                                    onActionChanges={(a) =>
                                        setContent((content) => ({
                                            ...content,
                                            actions: content.actions.toSpliced(i, 1, a),
                                        }))
                                    }
                                />
                            )}
                            onPlayerChange={(player) => {
                                const playerBounds = document
                                    .getElementById(player.id)!
                                    .getBoundingClientRect()
                                if (!isBoundsOnCourt(playerBounds)) {
                                    removePlayer(player)
                                    return
                                }
                                setContent((content) => ({
                                    ...content,
                                    players: toSplicedPlayers(
                                        content.players,
                                        player,
                                        true,
                                    ),
                                }))
                            }}
                            onPlayerRemove={removePlayer}
                            onBallRemove={removeCourtBall}
                        />
                    </div>
                </div>
            </div>
        </div>
    )
}

function isBallOnCourt(content: TacticContent) {
    if (content.players.findIndex((p) => p.hasBall) != -1) {
        return true
    }
    return content.objects.findIndex((o) => o.type == "ball") != -1
}

function renderCourtObject(courtObject: RackedCourtObject) {
    if (courtObject.key == "ball") {
        return <BallPiece />
    }
    throw new Error("unknown racked court object ", courtObject.key)
}

function getRackPlayers(team: Team, players: Player[]): RackedPlayer[] {
    return ["1", "2", "3", "4", "5"]
        .filter(
            (role) =>
                players.findIndex((p) => p.team == team && p.role == role) ==
                -1,
        )
        .map((key) => ({ team, key }))
}

function debounceAsync<A, B>(
    f: (args: A) => Promise<B>,
    delay = 1000,
): (args: A) => Promise<B> {
    let task = 0
    return (args: A) => {
        clearTimeout(task)
        return new Promise((resolve) => {
            task = setTimeout(() => f(args).then(resolve), delay)
        })
    }
}

function useContentState<S>(
    initialContent: S,
    initialSaveState: SaveState,
    saveStateCallback: (s: S) => Promise<SaveState>,
): [S, Dispatch<SetStateAction<S>>, SaveState] {
    const [content, setContent] = useState(initialContent)
    const [savingState, setSavingState] = useState(initialSaveState)

    const setContentSynced = useCallback(
        (newState: SetStateAction<S>) => {
            setContent((content) => {
                const state =
                    typeof newState === "function"
                        ? (newState as (state: S) => S)(content)
                        : newState
                if (state !== content) {
                    setSavingState(SaveStates.Saving)
                    saveStateCallback(state)
                        .then(setSavingState)
                        .catch(() => setSavingState(SaveStates.Err))
                }
                return state
            })
        },
        [saveStateCallback],
    )

    return [content, setContentSynced, savingState]
}

function toSplicedPlayers(
    players: Player[],
    player: Player,
    replace: boolean,
): Player[] {
    const idx = players.findIndex(
        (p) => p.team === player.team && p.role === player.role,
    )

    return players.toSpliced(idx, 1, ...(replace ? [player] : []))
}
