import {
    BallState,
    Player,
    PlayerInfo,
    PlayerLike,
    PlayerPhantom,
} from "../model/tactic/Player.ts"
import {
    ComponentId,
    StepContent,
    TacticComponent,
} from "../model/tactic/Tactic.ts"

import { removeComponent, updateComponent } from "./TacticContentDomains.ts"
import {
    removeAllActionsTargeting,
    spreadNewStateFromOriginStateChange,
} from "./ActionsDomains.ts"
import { ActionKind } from "../model/tactic/Action.ts"
import {
    add,
    minus,
    norm,
    Pos,
    posWithinBase,
    ratioWithinBase,
    relativeTo,
} from "../geo/Pos.ts"
import { PLAYER_RADIUS_PIXELS } from "../components/editor/CourtPlayer.tsx"

export function getOrigin(
    pathItem: PlayerPhantom,
    components: TacticComponent[],
): Player {
    // Trust the components to contains only phantoms with valid player origin identifiers
    return components.find((c) => c.id == pathItem.originPlayerId)! as Player
}

export function getPlayerNextTo(
    player: PlayerLike,
    n: number,
    components: TacticComponent[],
): PlayerLike | undefined {
    const playerOrigin =
        player.type === "player" ? player : getOrigin(player, components)
    const pathItems = playerOrigin.path!.items

    // add one as there is a shifting because a Player is never at the head of its own path
    const idx = pathItems.indexOf(player.id) + 1 // is 0 if the player is the origin

    const targetIdx = idx + n

    // remove the screen phantom
    return targetIdx == 0
        ? playerOrigin
        : getComponent<PlayerLike>(pathItems[targetIdx - 1], components)
}

export function getPrecomputedPosition(
    phantom: PlayerPhantom,
    computedPositions: Map<string, Pos>,
): Pos | undefined {
    const pos = phantom.pos

    // If the position is already known and fixed, return the pos
    if (pos.type === "fixed") return pos

    return computedPositions.get(phantom.id)
}

export function computePhantomPositioning(
    phantom: PlayerPhantom,
    content: StepContent,
    computedPositions: Map<string, Pos>,
    area: DOMRect,
): Pos {
    const positioning = phantom.pos

    // If the position is already known and fixed, return the pos
    if (positioning.type === "fixed") return positioning

    const storedPos = computedPositions.get(phantom.id)
    if (storedPos) return storedPos

    // If the position is to determine (positioning.type = "follows"), determine the phantom's pos
    // by calculating it from the referent position, and the action that targets the referent.

    const components = content.components

    // Get the referent from the components
    const referent: PlayerLike = getComponent(positioning.attach, components)
    const referentPos =
        referent.type === "player"
            ? referent.pos
            : computePhantomPositioning(
                  referent,
                  content,
                  computedPositions,
                  area,
              )

    // Get the origin
    const origin = getOrigin(phantom, components)
    const originPathItems = origin.path!.items
    const phantomIdx = originPathItems.indexOf(phantom.id)

    const playerBeforePhantom: PlayerLike =
        phantomIdx == 0
            ? origin
            : getComponent(originPathItems[phantomIdx - 1], components)
    const action = playerBeforePhantom.actions.find(
        (a) => a.target === positioning.attach,
    )!

    const segments = action.segments
    const lastSegment = segments[segments.length - 1]
    const lastSegmentStart = segments[segments.length - 2]?.next
    let pivotPoint = lastSegment.controlPoint

    if (!pivotPoint) {
        if (lastSegmentStart) {
            pivotPoint =
                typeof lastSegmentStart === "string"
                    ? document
                          .getElementById(lastSegmentStart)!
                          .getBoundingClientRect()
                    : lastSegmentStart
        } else {
            pivotPoint =
                playerBeforePhantom.type === "phantom"
                    ? computePhantomPositioning(
                          playerBeforePhantom,
                          content,
                          computedPositions,
                          area,
                      )
                    : playerBeforePhantom.pos
        }
    }

    const segment = posWithinBase(relativeTo(referentPos, pivotPoint), area)
    const segmentLength = norm(segment)
    const segmentProjection = minus(area, {
        x: (segment.x / segmentLength) * PLAYER_RADIUS_PIXELS,
        y: (segment.y / segmentLength) * PLAYER_RADIUS_PIXELS,
    })
    const segmentProjectionRatio: Pos = ratioWithinBase(segmentProjection, area)

    const result = add(referentPos, segmentProjectionRatio)
    computedPositions.set(phantom.id, result)
    return result
}

export function getComponent<T extends TacticComponent>(
    id: string,
    components: TacticComponent[],
): T {
    return tryGetComponent<T>(id, components)!
}

export function tryGetComponent<T extends TacticComponent>(
    id: string,
    components: TacticComponent[],
): T | undefined {
    return components.find((c) => c.id === id) as T
}

export function areInSamePath(a: PlayerLike, b: PlayerLike) {
    if (a.type === "phantom" && b.type === "phantom") {
        return a.originPlayerId === b.originPlayerId
    }
    if (a.type === "phantom") {
        return b.id === a.originPlayerId
    }
    if (b.type === "phantom") {
        return a.id === b.originPlayerId
    }
    return false
}

/**
 * @param origin
 * @param other
 * @param components
 * @returns true if the `other` player is the phantom next-to the origin's path.
 */
export function isNextInPath(
    origin: PlayerLike,
    other: PlayerLike,
    components: TacticComponent[],
): boolean {
    if (origin.type === "player") {
        return origin.path?.items[0] === other.id
    }
    const originPath = getOrigin(origin, components).path!
    return (
        originPath.items!.indexOf(origin.id) ===
        originPath.items!.indexOf(other.id) - 1
    )
}

export function clearPlayerPath(
    player: Player,
    content: StepContent,
): StepContent {
    if (player.path == null) {
        return content
    }

    for (const pathElement of player.path.items) {
        content = removeComponent(pathElement, content)
        content = removeAllActionsTargeting(pathElement, content)
    }
    return updateComponent(
        {
            ...player,
            path: null,
        },
        content,
    )
}

function removeAllPhantomsAttached(
    to: ComponentId,
    content: StepContent,
): StepContent {
    let i = 0
    while (i < content.components.length) {
        const component = content.components[i]
        if (component.type === "phantom") {
            if (
                component.pos.type === "follows" &&
                component.pos.attach === to
            ) {
                content = removePlayer(component, content)
                continue
            }
        }
        i++
    }
    return content
}

export function removePlayer(
    player: PlayerLike,
    content: StepContent,
): StepContent {
    content = removeAllActionsTargeting(player.id, content)
    content = removeAllPhantomsAttached(player.id, content)

    if (player.type === "phantom") {
        const pos = player.pos
        // if the phantom was attached to another player, remove the action that symbolizes the attachment
        if (pos.type === "follows") {
            const playerBefore = getPlayerNextTo(
                player,
                -1,
                content.components,
            )!
            const actions = playerBefore.actions.filter(
                (a) => a.target !== pos.attach,
            )
            content = updateComponent(
                {
                    ...playerBefore,
                    actions,
                },
                content,
            )
        }

        const origin = getOrigin(player, content.components)
        return truncatePlayerPath(origin, player, content)
    }

    content = clearPlayerPath(player, content)
    content = removeComponent(player.id, content)

    for (const action of player.actions) {
        if (action.type !== ActionKind.SHOOT) {
            continue
        }
        if (typeof action.target !== "string") continue
        const actionTarget = tryGetComponent<PlayerLike>(
            action.target,
            content.components,
        )
        if (actionTarget === undefined) continue //the target was maybe removed
        return (
            spreadNewStateFromOriginStateChange(
                actionTarget,
                BallState.NONE,
                content,
            ) ?? content
        )
    }

    return content
}

export function truncatePlayerPath(
    player: Player,
    phantom: PlayerPhantom,
    content: StepContent,
): StepContent {
    if (player.path == null) return content

    const path = player.path!

    const truncateStartIdx = path.items.indexOf(phantom.id)

    for (let i = truncateStartIdx; i < path.items.length; i++) {
        const pathPhantomId = path.items[i]

        //remove the phantom from the domains
        content = removeComponent(pathPhantomId, content)
        content = removeAllActionsTargeting(pathPhantomId, content)
    }

    return updateComponent(
        {
            ...player,
            path:
                truncateStartIdx == 0
                    ? null
                    : {
                          ...path,
                          items: path.items.toSpliced(truncateStartIdx),
                      },
        },
        content,
    )
}

export function getPhantomInfo(
    phantom: PlayerPhantom,
    content: StepContent,
    relativePositions: ComputedRelativePositions,
    courtBounds: DOMRect,
): PlayerInfo {
    const origin = getOrigin(phantom, content.components)

    return {
        id: phantom.id,
        team: origin.team,
        role: origin.role,
        pos: computePhantomPositioning(
            phantom,
            content,
            relativePositions,
            courtBounds,
        ),
        ballState: phantom.ballState,
    }
}

export type ComputedRelativePositions = Map<ComponentId, Pos>

export function computeRelativePositions(
    courtBounds: DOMRect,
    content: StepContent,
) {
    const relativePositionsCache: ComputedRelativePositions = new Map()

    for (const component of content.components) {
        if (component.type !== "phantom") continue
        computePhantomPositioning(
            component,
            content,
            relativePositionsCache,
            courtBounds,
        )
    }

    return relativePositionsCache
}
