import { equals, Pos, ratioWithinBase } from "../geo/Pos.ts"

import {
    BallState,
    Player,
    PlayerInfo,
    PlayerLike,
    PlayerPhantom,
    PlayerTeam,
} from "../model/tactic/Player.ts"
import {
    Ball,
    BALL_ID,
    BALL_TYPE,
    CourtObject,
} from "../model/tactic/CourtObjects.ts"
import {
    ComponentId,
    StepContent,
    TacticComponent,
} from "../model/tactic/Tactic.ts"

import { overlaps } from "../geo/Box.ts"
import { RackedCourtObject, RackedPlayer } from "../editor/RackedItems.ts"
import {
    getComponent,
    getOrigin,
    getPrecomputedPosition,
    removePlayer,
    tryGetComponent,
} from "./PlayerDomains.ts"
import { Action, ActionKind } from "../model/tactic/Action.ts"
import { spreadNewStateFromOriginStateChange } from "./ActionsDomains.ts"

export function placePlayerAt(
    refBounds: DOMRect,
    courtBounds: DOMRect,
    element: RackedPlayer,
): Player {
    const pos = ratioWithinBase(refBounds, courtBounds)

    return {
        type: "player",
        id: "player-" + element.key + "-" + element.team,
        team: element.team,
        role: element.key,
        pos,
        ballState: BallState.NONE,
        path: null,
        actions: [],
        frozen: false,
    }
}

export function placeObjectAt(
    refBounds: DOMRect,
    courtBounds: DOMRect,
    rackedObject: RackedCourtObject,
    content: StepContent,
): StepContent {
    const pos = ratioWithinBase(refBounds, courtBounds)

    let courtObject: CourtObject

    switch (rackedObject.key) {
        case BALL_TYPE: {
            const playerCollidedIdx = getComponentCollided(
                refBounds,
                content.components,
                BALL_ID,
            )
            if (playerCollidedIdx != -1) {
                return dropBallOnComponent(playerCollidedIdx, content, true)
            }

            courtObject = {
                type: BALL_TYPE,
                id: BALL_ID,
                pos,
                actions: [],
                frozen: false,
            }
            break
        }
        default:
            throw new Error("unknown court object " + rackedObject.key)
    }

    return {
        ...content,
        components: [...content.components, courtObject],
    }
}

export function dropBallOnComponent(
    targetedComponentIdx: number,
    content: StepContent,
    setAsOrigin: boolean,
): StepContent {
    const component = content.components[targetedComponentIdx]

    if (component.type === "player" || component.type === "phantom") {
        const newState =
            setAsOrigin ||
            component.ballState === BallState.PASSED_ORIGIN ||
            component.ballState === BallState.HOLDS_ORIGIN
                ? BallState.HOLDS_ORIGIN
                : BallState.HOLDS_BY_PASS

        content =
            spreadNewStateFromOriginStateChange(component, newState, content) ??
            content
    }

    return removeBall(content)
}

export function removeBall(content: StepContent): StepContent {
    const ballObjIdx = content.components.findIndex((o) => o.type == "ball")

    if (ballObjIdx == -1) {
        return content
    }

    return {
        ...content,
        components: content.components.toSpliced(ballObjIdx, 1),
    }
}

export function placeBallAt(
    refBounds: DOMRect,
    courtBounds: DOMRect,
    content: StepContent,
): StepContent {
    if (!overlaps(courtBounds, refBounds)) {
        return removeBall(content)
    }
    const playerCollidedIdx = getComponentCollided(
        refBounds,
        content.components,
        BALL_ID,
    )

    if (playerCollidedIdx != -1) {
        return dropBallOnComponent(playerCollidedIdx, content, true)
    }

    const ballIdx = content.components.findIndex((o) => o.type == "ball")

    const pos = ratioWithinBase(refBounds, courtBounds)

    const ball: Ball = {
        type: BALL_TYPE,
        id: BALL_ID,
        pos,
        actions: [],
        frozen: false,
    }

    let components = content.components

    if (ballIdx != -1) {
        components = components.toSpliced(ballIdx, 1, ball)
    } else {
        components = components.concat(ball)
    }

    return {
        ...content,
        components,
    }
}

export function moveComponent(
    newPos: Pos,
    component: TacticComponent,
    info: PlayerInfo,
    courtBounds: DOMRect,
    content: StepContent,
    removed: (content: StepContent) => StepContent,
): StepContent {
    const playerBounds = document
        .getElementById(info.id)!
        .getBoundingClientRect()

    // if the piece is no longer on the court, remove it
    if (!overlaps(playerBounds, courtBounds)) {
        return removed(content)
    }

    const isPhantom = component.type === "phantom"

    if (isPhantom && component.pos.type === "follows") {
        const referent = component.pos.attach
        const origin = getOrigin(component, content.components)
        const originPathItems = origin.path!.items
        const phantomIdx = originPathItems.indexOf(component.id)

        const playerBeforePhantom: PlayerLike =
            phantomIdx == 0
                ? origin
                : getComponent(
                      originPathItems[phantomIdx - 1],
                      content.components,
                  )
        // detach the action from the screen target and transform it to a regular move action to the phantom.
        content = updateComponent(
            {
                ...playerBeforePhantom,
                actions: playerBeforePhantom.actions.map((a) =>
                    a.target === referent
                        ? {
                              ...a,
                              segments: a.segments.toSpliced(
                                  a.segments.length - 2,
                                  1,
                                  {
                                      ...a.segments[a.segments.length - 1],
                                      next: component.id,
                                  },
                              ),
                              target: component.id,
                              type: ActionKind.MOVE,
                          }
                        : a,
                ),
            },
            content,
        )
    }

    content = updateComponent(
        <TacticComponent>{
            ...component,
            pos: isPhantom
                ? {
                      type: "fixed",
                      ...newPos,
                  }
                : newPos,
        },
        content,
    )
    return content
}

export function removeComponent(
    componentId: ComponentId,
    content: StepContent,
): StepContent {
    return {
        ...content,
        components: content.components.filter((c) => c.id !== componentId),
    }
}

export function updateComponent(
    component: TacticComponent,
    content: StepContent,
): StepContent {
    return {
        ...content,
        components: content.components.map((c) =>
            c.id === component.id ? component : c,
        ),
    }
}

export function getComponentCollided(
    bounds: DOMRect,
    components: TacticComponent[],
    ignore?: ComponentId,
): number | -1 {
    for (let i = 0; i < components.length; i++) {
        const component = components[i]

        if (component.id == ignore) continue

        const playerBounds = document
            .getElementById(component.id)!
            .getBoundingClientRect()

        if (overlaps(playerBounds, bounds)) {
            return i
        }
    }
    return -1
}

export 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 }))
}

/**
 * Returns a step content that only contains the terminal state of each components inside the given content
 * @param content
 * @param computedPositions
 */
export function computeTerminalState(
    content: StepContent,
    computedPositions: Map<string, Pos>,
): StepContent {
    const nonPhantomComponents: (Player | CourtObject)[] =
        content.components.filter(
            (c): c is Exclude<TacticComponent, PlayerPhantom> =>
                c.type !== "phantom",
        )

    const componentsTargetedState = nonPhantomComponents.map((comp) =>
        comp.type === "player"
            ? getPlayerTerminalState(comp, content, computedPositions)
            : {
                  ...comp,
                  frozen: true,
              },
    )

    return {
        components: componentsTargetedState,
    }
}

function getPlayerTerminalState(
    player: Player,
    content: StepContent,
    computedPositions: Map<string, Pos>,
): Player {
    function stateAfter(state: BallState): BallState {
        switch (state) {
            case BallState.HOLDS_ORIGIN:
                return BallState.HOLDS_ORIGIN
            case BallState.PASSED_ORIGIN:
            case BallState.PASSED:
                return BallState.NONE
            case BallState.HOLDS_BY_PASS:
                return BallState.HOLDS_ORIGIN
            case BallState.NONE:
                return BallState.NONE
        }
    }

    function getTerminalPos(component: PlayerLike): Pos {
        if (component.type === "phantom") {
            const pos = getPrecomputedPosition(component, computedPositions)
            if (!pos)
                throw new Error(
                    `Attempted to get the terminal state of a step content with missing position for phantom ${component.id}`,
                )
            return pos
        }
        return component.pos
    }

    const phantoms = player.path?.items
    if (!phantoms || phantoms.length === 0) {
        const pos = getTerminalPos(player)

        return {
            ...player,
            ballState: stateAfter(player.ballState),
            actions: [],
            pos,
            frozen: true,
        }
    }
    const lastPhantomId = phantoms[phantoms.length - 1]
    const lastPhantom = content.components.find(
        (c) => c.id === lastPhantomId,
    )! as PlayerPhantom

    const pos = getTerminalPos(lastPhantom)

    return {
        type: "player",
        path: { items: [] },
        role: player.role,
        team: player.team,

        actions: [],
        ballState: stateAfter(lastPhantom.ballState),
        id: player.id,
        pos,
        frozen: true,
    }
}

export function drainTerminalStateOnChildContent(
    parentTerminalState: StepContent,
    childContent: StepContent,
): StepContent | null {
    let gotUpdated = false

    for (const parentComponent of parentTerminalState.components) {
        let childComponent = tryGetComponent(
            parentComponent.id,
            childContent.components,
        )

        if (!childComponent) {
            //if the child does not contain the parent's component, add it to the children's content.
            childContent = {
                ...childContent,
                components: [...childContent.components, parentComponent],
            }
            gotUpdated = true
            continue
        }

        if (childComponent.type !== parentComponent.type)
            throw Error("child and parent components are not of the same type.")

        if (childComponent.type === "ball" && parentComponent.type === "ball") {
            gotUpdated = true
            childContent = updateComponent(
                {
                    ...childComponent,
                    frozen: true,
                    pos: parentComponent.pos,
                },
                childContent,
            )
        }

        // ensure that the component is a player
        if (
            parentComponent.type !== "player" ||
            childComponent.type !== "player"
        ) {
            continue
        }

        const newContentResult = spreadNewStateFromOriginStateChange(
            childComponent,
            parentComponent.ballState,
            childContent,
        )
        if (newContentResult) {
            gotUpdated = true
            childContent = newContentResult
            childComponent = getComponent<Player>(
                childComponent.id,
                newContentResult?.components,
            )
        }
        // update the position of the component if it has been moved
        // also force update if the child component is not frozen (the component was introduced previously by the child step but the parent added it afterward)
        if (
            !childComponent.frozen ||
            !equals(childComponent.pos, parentComponent.pos)
        ) {
            gotUpdated = true
            childContent = updateComponent(
                {
                    ...childComponent,
                    frozen: true,
                    pos: parentComponent.pos,
                },
                childContent,
            )
        }
    }

    const initialChildCompsCount = childContent.components.length

    //remove players if they are not present on the parent's anymore
    for (const component of childContent.components) {
        if (
            component.type !== "phantom" &&
            component.frozen &&
            !tryGetComponent(component.id, parentTerminalState.components)
        ) {
            if (component.type === "player")
                childContent = removePlayer(component, childContent)
            else
                childContent = {
                    ...childContent,
                    components: childContent.components.filter(
                        (c) => c.id !== component.id,
                    ),
                }
        }
    }

    gotUpdated ||= childContent.components.length !== initialChildCompsCount

    return gotUpdated ? childContent : null
}

export function mapToParentContent(content: StepContent): StepContent {
    function mapToParentActions(actions: Action[]): Action[] {
        return actions.map((a) => ({
            ...a,
            target: a.target + "-parent",
            segments: a.segments.map((s) => ({
                ...s,
                next: typeof s.next === "string" ? s.next + "-parent" : s.next,
            })),
        }))
    }

    return {
        ...content,
        components: content.components.map((p) => {
            if (p.type == "ball") return p
            if (p.type == "player") {
                return {
                    ...p,
                    id: p.id + "-parent",
                    actions: mapToParentActions(p.actions),
                    path: p.path && {
                        items: p.path.items.map((p) => p + "-parent"),
                    },
                }
            }
            return {
                ...p,
                pos:
                    p.pos.type == "follows"
                        ? { ...p.pos, attach: p.pos.attach + "-parent" }
                        : p.pos,
                id: p.id + "-parent",
                originPlayerId: p.originPlayerId + "-parent",
                actions: mapToParentActions(p.actions),
            }
        }),
    }
}

export function selectContent(
    id: string,
    content: StepContent,
    parentContent: StepContent | null,
): StepContent {
    return parentContent && id.endsWith("-parent") ? parentContent : content
}
