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?react"
import HalfCourt from "../assets/court/half_court.svg?react"

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

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

import {Tactic, TacticComponent, TacticContent} from "../model/tactic/Tactic"
import {fetchAPI} from "../Fetcher"

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

import {BALL_ID, BALL_TYPE, CourtObject, Ball} from "../model/tactic/Ball"
import {CourtAction} from "./editor/CourtAction"
import {BasketCourt} from "../components/editor/BasketCourt"
import {Action, ActionKind} from "../model/tactic/Action"
import {BASE} from "../Constants"
import {overlaps} from "../geo/Box"
import {ratioWithinBase} from "../geo/Pos"

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: PlayerTeam
    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(PlayerTeam.Allies, content.components),
    )
    const [opponents, setOpponents] = useState(
        getRackPlayers(PlayerTeam.Opponents, content.components),
    )

    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 overlaps(courtBounds, bounds)
    }

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

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

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

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

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

        let courtObject: CourtObject

        switch (rackedObject.key) {
            case BALL_TYPE:
                const ballObj = content.components.findIndex(
                    (o) => o.type == BALL_TYPE,
                )
                const playerCollidedIdx = getComponentCollided(
                    refBounds,
                    content.components.toSpliced(ballObj, 1),
                )
                if (playerCollidedIdx != -1) {
                    onBallDropOnComponent(playerCollidedIdx)
                    return
                }

                courtObject = {
                    type: BALL_TYPE,
                    id: BALL_ID,
                    rightRatio: x,
                    bottomRatio: y,
                }
                break

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

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

    const getComponentCollided = (
        bounds: DOMRect,
        components: TacticComponent[],
    ): number | -1 => {
        for (let i = 0; i < components.length; i++) {
            const component = components[i]
            const playerBounds = document
                .getElementById(component.id)!
                .getBoundingClientRect()
            if (overlaps(playerBounds, bounds)) {
                return i
            }
        }
        return -1
    }

    function updateActions(actions: Action[], components: TacticComponent[]) {
        return actions.map((action) => {
            const originHasBall = (
                components.find(
                    (p) => p.type == "player" && p.id == action.fromId,
                )! as Player
            ).hasBall

            let type = action.type

            if (originHasBall && type == ActionKind.MOVE) {
                type = ActionKind.DRIBBLE
            } else if (originHasBall && type == ActionKind.SCREEN) {
                type = ActionKind.SHOOT
            } else if (type == ActionKind.DRIBBLE) {
                type = ActionKind.MOVE
            } else if (type == ActionKind.SHOOT) {
                type = ActionKind.SCREEN
            }
            return {
                ...action,
                type,
            }
        })
    }

    const onBallDropOnComponent = (collidedComponentIdx: number) => {
        setContent((content) => {
            const ballObj = content.components.findIndex(
                (p) => p.type == BALL_TYPE,
            )
            let component = content.components[collidedComponentIdx]
            if (component.type != "player") {
                return content //do nothing if the ball isn't dropped on a player.
            }
            const components = content.components.toSpliced(
                collidedComponentIdx,
                1,
                {
                    ...component,
                    hasBall: true,
                },
            )
            // Maybe the ball is not present on the court as an object component
            // if so, don't bother removing it from the court.
            // This can occur if the user drags and drop the ball from a player that already has the ball
            // to another component
            if (ballObj != -1) {
                components.splice(ballObj, 1)
            }
            return {
                ...content,
                actions: updateActions(content.actions, components),
                components,
            }
        })
    }

    const onBallMoved = (refBounds: DOMRect) => {
        if (!isBoundsOnCourt(refBounds)) {
            removeCourtBall()
            return
        }
        const playerCollidedIdx = getComponentCollided(
            refBounds,
            content.components,
        )
        if (playerCollidedIdx != -1) {
            setContent((content) => {
                return {
                    ...content,
                    components: content.components.map((c) =>
                        c.type == "player"
                            ? {
                                  ...c,
                                  hasBall: false,
                              }
                            : c,
                    ),
                }
            })
            onBallDropOnComponent(playerCollidedIdx)
            return
        }

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

        const courtBounds = courtDivContentRef.current!.getBoundingClientRect()
        const { x, y } = ratioWithinBase(refBounds, courtBounds)
        const courtObject = {
            type: BALL_TYPE,
            id: BALL_ID,
            rightRatio: x,
            bottomRatio: y,
        } as Ball

        let components = content.components.map((c) =>
            c.type == "player"
                ? {
                      ...c,
                      hasBall: false,
                  }
                : c,
        )
        components = [...components, courtObject]

        setContent((content) => ({
            ...content,
            actions: updateActions(content.actions, components),
            components,
        }))
    }

    const removePlayer = (player: Player) => {
        setContent((content) => ({
            ...content,
            components: replaceOrInsert(content.components, player, false),
            actions: content.actions.filter(
                (a) => a.toId !== player.id && a.fromId !== player.id,
            ),
        }))
        let setter
        switch (player.team) {
            case PlayerTeam.Opponents:
                setter = setOpponents
                break
            case PlayerTeam.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.components.findIndex(
                (o) => o.type == "ball",
            )
            const components = content.components.map((c) =>
                c.type == "player"
                    ? ({
                          ...c,
                          hasBall: false,
                      } as Player)
                    : c,
            )
            components.splice(ballObj, 1)
            return {
                ...content,
                components,
            }
        })
        setObjects([{ key: "ball" }])
    }

    return (
        <div id="main-div">
            <div id="topbar-div">
                <button onClick={() => (location.pathname = BASE + "/")}>
                    Home
                </button>
                <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={onRackPieceDetach}
                        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={onRackedObjectDetach}
                        render={renderCourtObject}
                    />

                    <Rack
                        id="opponent-rack"
                        objects={opponents}
                        onChange={setOpponents}
                        canDetach={(div) =>
                            isBoundsOnCourt(div.getBoundingClientRect())
                        }
                        onElementDetached={onRackPieceDetach}
                        render={({ team, key }) => (
                            <PlayerPiece
                                team={team}
                                text={key}
                                key={key}
                                hasBall={false}
                            />
                        )}
                    />
                </div>
                <div id="court-div">
                    <div id="court-div-bounds">
                        <BasketCourt
                            components={content.components}
                            actions={content.actions}
                            onBallMoved={onBallMoved}
                            courtImage={<Court courtType={courtType} />}
                            courtRef={courtDivContentRef}
                            setActions={(actions) =>
                                setContent((content) => ({
                                    ...content,
                                    actions: actions(content.actions),
                                }))
                            }
                            renderAction={(action, i) => (
                                <CourtAction
                                    key={i}
                                    action={action}
                                    courtRef={courtDivContentRef}
                                    onActionDeleted={() => {
                                        setContent((content) => ({
                                            ...content,
                                            actions: content.actions.toSpliced(
                                                i,
                                                1,
                                            ),
                                        }))
                                    }}
                                    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,
                                    components: replaceOrInsert(
                                        content.components,
                                        player,
                                        true,
                                    ),
                                }))
                            }}
                            onPlayerRemove={removePlayer}
                            onBallRemove={removeCourtBall}
                        />
                    </div>
                </div>
            </div>
        </div>
    )
}

function isBallOnCourt(content: TacticContent) {
    return (
        content.components.findIndex(
            (c) => (c.type == "player" && c.hasBall) || c.type == BALL_TYPE,
        ) != -1
    )
}

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

function Court({ courtType }: { courtType: string }) {
    return (
        <div id="court-image-div">
            {courtType == "PLAIN" ? (
                <PlainCourt id="court-image" />
            ) : (
                <HalfCourt id="court-image" />
            )}
        </div>
    )
}


function getRackPlayers(
    team: PlayerTeam,
    components: TacticComponent[],
): RackedPlayer[] {
    return ["1", "2", "3", "4", "5"]
        .filter(
            (role) =>
                components.findIndex(
                    (c) =>
                        c.type == "player" && c.team == team && c.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, reject) => {
            task = setTimeout(() => f(args).then(resolve).catch(reject), 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 replaceOrInsert<A extends TacticComponent>(
    array: A[],
    it: A,
    replace: boolean,
): A[] {
    const idx = array.findIndex((i) => i.id == it.id)
    return array.toSpliced(idx, 1, ...(replace ? [it] : []))
}
