import {
    BallState,
    Player,
    PlayerLike,
    PlayerPhantom,
} from "../model/tactic/Player.ts"
import { ratioWithinBase } from "../geo/Pos.ts"
import {
    ComponentId,
    StepContent,
    TacticComponent,
} from "../model/tactic/Tactic.ts"
import { overlaps } from "../geo/Box.ts"
import { Action, ActionKind, moves } from "../model/tactic/Action.ts"
import { removeBall, updateComponent } from "./TacticContentDomains.ts"
import {
    areInSamePath,
    getComponent,
    getOrigin,
    getPlayerNextTo,
    isNextInPath,
    removePlayer,
} from "./PlayerDomains.ts"
import { BALL_TYPE } from "../model/tactic/CourtObjects.ts"

export function getActionKind(
    target: TacticComponent | null,
    ballState: BallState,
): { kind: ActionKind; nextState: BallState } {
    switch (ballState) {
        case BallState.HOLDS_ORIGIN:
            return target
                ? { kind: ActionKind.SHOOT, nextState: BallState.PASSED_ORIGIN }
                : { kind: ActionKind.DRIBBLE, nextState: ballState }
        case BallState.HOLDS_BY_PASS:
            return target
                ? { kind: ActionKind.SHOOT, nextState: BallState.PASSED }
                : { kind: ActionKind.DRIBBLE, nextState: ballState }
        case BallState.PASSED_ORIGIN:
        case BallState.PASSED:
        case BallState.NONE:
            return {
                kind:
                    target && target.type != BALL_TYPE
                        ? ActionKind.SCREEN
                        : ActionKind.MOVE,
                nextState: ballState,
            }
    }
}

export function getActionKindBetween(
    origin: PlayerLike,
    target: TacticComponent | null,
    state: BallState,
): { kind: ActionKind; nextState: BallState } {
    //remove the target if the target is a phantom that is within the origin's path
    if (
        target != null &&
        target.type == "phantom" &&
        areInSamePath(origin, target)
    ) {
        target = null
    }

    return getActionKind(target, state)
}

export function isActionValid(
    origin: TacticComponent,
    target: TacticComponent | null,
    components: TacticComponent[],
): boolean {
    /// action is valid if the origin is neither a phantom nor a player
    if (origin.type != "phantom" && origin.type != "player") {
        return true
    }

    // action is invalid if the origin already moves (unless the origin holds a ball which will lead to a ball pass)
    if (
        origin.ballState != BallState.HOLDS_BY_PASS &&
        origin.ballState != BallState.HOLDS_ORIGIN &&
        origin.actions.find((a) => moves(a.type))
    ) {
        return false
    }
    //Action is valid if the target is null
    if (target == null) {
        return true
    }

    // action is invalid if it targets its own origin
    if (origin.id === target.id) {
        return false
    }

    // action is invalid if the target already moves and is not indirectly bound with origin
    if (
        target.actions.find((a) => moves(a.type)) &&
        (hasBoundWith(target, origin, components) ||
            hasBoundWith(origin, target, components))
    ) {
        return false
    }

    // Action is invalid if there is already an action between origin and target.
    if (
        origin.actions.find((a) => a.target === target?.id) ||
        target?.actions.find((a) => a.target === origin.id)
    ) {
        return false
    }

    // Action is invalid if there is already an anterior action within the target's path
    if (target.type == "phantom" || target.type == "player") {
        // cant have an action with current path
        if (areInSamePath(origin, target)) return false

        if (alreadyHasAnAnteriorActionWith(origin, target, components)) {
            return false
        }
    }

    return true
}

function hasBoundWith(
    origin: TacticComponent,
    target: TacticComponent,
    components: TacticComponent[],
): boolean {
    const toVisit = [origin.id]
    const visited: string[] = []

    let itemId: string | undefined
    while ((itemId = toVisit.pop())) {
        if (visited.indexOf(itemId) !== -1) continue

        visited.push(itemId)

        const item = components.find((c) => c.id === itemId)!

        const itemBounds = item.actions.flatMap((a) =>
            typeof a.target == "string" ? [a.target] : [],
        )
        if (itemBounds.indexOf(target.id) !== -1) {
            return true
        }

        toVisit.push(...itemBounds)
    }

    return false
}

function alreadyHasAnAnteriorActionWith(
    origin: PlayerLike,
    target: PlayerLike,
    components: TacticComponent[],
): boolean {
    const targetOrigin =
        target.type === "phantom" ? getOrigin(target, components) : target
    const targetOriginPath = [
        targetOrigin.id,
        ...(targetOrigin.path?.items ?? []),
    ]

    const originOrigin =
        origin.type === "phantom" ? getOrigin(origin, components) : origin
    const originOriginPath = [
        originOrigin.id,
        ...(originOrigin.path?.items ?? []),
    ]

    const targetIdx = targetOriginPath.indexOf(target.id)
    for (let i = targetIdx; i < targetOriginPath.length; i++) {
        const component = getComponent(targetOriginPath[i], components)
        if (
            component.actions.find(
                (a) =>
                    typeof a.target === "string" &&
                    moves(a.type) &&
                    originOriginPath.indexOf(a.target) !== -1,
            )
        ) {
            return true
        }
    }

    const originIdx = originOriginPath.indexOf(origin.id)
    for (let i = 0; i <= originIdx; i++) {
        const phantom = components.find(
            (c) => c.id === originOriginPath[i],
        )! as PlayerLike
        if (
            phantom.actions.find(
                (a) =>
                    typeof a.target === "string" &&
                    moves(a.type) &&
                    targetOriginPath.indexOf(a.target) > targetIdx,
            )
        ) {
            return true
        }
    }

    return false
}

export function createAction(
    origin: PlayerLike,
    courtBounds: DOMRect,
    arrowHead: DOMRect,
    content: StepContent,
): { createdAction: Action; newContent: StepContent } {
    /**
     * Creates a new phantom component.
     * Be aware that this function will reassign the `content` parameter.
     */
    function createPhantom(
        forceHasBall: boolean,
        attachedTo?: ComponentId,
    ): ComponentId {
        const { x, y } = ratioWithinBase(arrowHead, courtBounds)

        let itemIndex: number
        let originPlayer: Player

        if (origin.type == "phantom") {
            // if we create a phantom from another phantom,
            // simply add it to the phantom's path
            const originPlr = getOrigin(origin, content.components)!
            itemIndex = originPlr.path!.items.length
            originPlayer = originPlr
        } else {
            // if we create a phantom directly from a player
            // create a new path and add it into
            itemIndex = 0
            originPlayer = origin
        }

        const path = originPlayer.path

        const phantomId = "phantom-" + itemIndex + "-" + originPlayer.id

        content = updateComponent(
            {
                ...originPlayer,
                path: {
                    items: path ? [...path.items, phantomId] : [phantomId],
                },
            },
            content,
        )

        let phantomState: BallState
        if (forceHasBall) phantomState = BallState.HOLDS_ORIGIN
        else
            switch (origin.ballState) {
                case BallState.HOLDS_ORIGIN:
                    phantomState = BallState.HOLDS_BY_PASS
                    break
                case BallState.PASSED:
                case BallState.PASSED_ORIGIN:
                    phantomState = BallState.NONE
                    break
                default:
                    phantomState = origin.ballState
            }

        const phantom: PlayerPhantom = {
            type: "phantom",
            id: phantomId,
            pos: attachedTo
                ? {
                      type: "follows",
                      attach: attachedTo,
                  }
                : {
                      type: "fixed",
                      x,
                      y,
                  },
            originPlayerId: originPlayer.id,
            ballState: phantomState,
            actions: [],
        }
        content = {
            ...content,
            components: [...content.components, phantom],
        }
        return phantom.id
    }

    for (const component of content.components) {
        if (component.id == origin.id) {
            continue
        }

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

        if (overlaps(componentBounds, arrowHead)) {
            let toId = component.id

            if (component.type == "ball") {
                toId = createPhantom(true)
                content = removeBall(content)
            }

            const actionKind = getActionKind(component, origin.ballState).kind

            let action: Action

            if (actionKind === ActionKind.SCREEN) {
                createPhantom(false, toId)

                action = {
                    target: toId,
                    type: actionKind,
                    segments: [{ next: toId }],
                }
            } else {
                action = {
                    target: toId,
                    type: actionKind,
                    segments: [{ next: toId }],
                }
            }

            return {
                newContent: updateComponent(
                    {
                        ...content.components.find((c) => c.id == origin.id)!,
                        actions: [...origin.actions, action],
                    },
                    content,
                ),
                createdAction: action,
            }
        }
    }

    const actionKind = getActionKind(null, origin.ballState).kind

    if (actionKind === ActionKind.SCREEN)
        throw new Error(
            "Attempted to create a screen action with nothing targeted",
        )

    const phantomId = createPhantom(false)

    const action: Action = {
        target: phantomId,
        type: actionKind,
        segments: [{ next: phantomId }],
    }
    return {
        newContent: updateComponent(
            {
                ...content.components.find((c) => c.id == origin.id)!,
                actions: [...origin.actions, action],
            },
            content,
        ),
        createdAction: action,
    }
}

export function removeAllActionsTargeting(
    componentId: ComponentId,
    content: StepContent,
): StepContent {
    const components = []
    for (let i = 0; i < content.components.length; i++) {
        const component = content.components[i]
        components.push({
            ...component,
            actions: component.actions.filter((a) => a.target != componentId),
        })
    }

    return {
        ...content,
        components,
    }
}

export function removeAction(
    origin: TacticComponent,
    actionIdx: number,
    content: StepContent,
): StepContent {
    const action = origin.actions[actionIdx]

    origin = {
        ...origin,
        actions: origin.actions.toSpliced(actionIdx, 1),
    }
    content = updateComponent(origin, content)

    if (action.target == null) return content

    const target = content.components.find((c) => action.target == c.id)!

    // if the removed action is a shoot, set the origin as holding the ball
    if (
        action.type == ActionKind.SHOOT &&
        (origin.type === "player" || origin.type === "phantom")
    ) {
        if (target.type === "player" || target.type === "phantom")
            content =
                spreadNewStateFromOriginStateChange(
                    target,
                    BallState.NONE,
                    content,
                ) ?? content

        if (origin.ballState === BallState.PASSED) {
            content =
                spreadNewStateFromOriginStateChange(
                    origin,
                    BallState.HOLDS_BY_PASS,
                    content,
                ) ?? content
        } else if (origin.ballState === BallState.PASSED_ORIGIN) {
            content =
                spreadNewStateFromOriginStateChange(
                    origin,
                    BallState.HOLDS_ORIGIN,
                    content,
                ) ?? content
        }
    }

    if (target.type === "phantom") {
        let path = null
        if (origin.type === "player") {
            path = origin.path
        } else if (origin.type === "phantom") {
            path = getOrigin(origin, content.components).path
        }

        if (path != null && path.items.find((c) => c === target.id)) {
            content = removePlayer(target, content)
        }
    }

    // if the action type is a screen over a player, remove the phantom bound to the target
    if (
        action.type === ActionKind.SCREEN &&
        (origin.type === "phantom" || origin.type === "player")
    ) {
        const screenPhantom = getPlayerNextTo(origin, 1, content.components)!
        content = removePlayer(screenPhantom, content)
    }

    return content
}

/**
 * Spreads the changes to others actions and components, directly or indirectly bound to the origin, implied by the change of the origin's actual state with
 * the given newState.
 * @param origin
 * @param newState
 * @param content
 * @returns the new state if it has been updated, or null if no changes were operated
 */
export function spreadNewStateFromOriginStateChange(
    origin: PlayerLike,
    newState: BallState,
    content: StepContent,
): StepContent | null {
    if (origin.ballState === newState) {
        return null
    }

    origin = {
        ...origin,
        ballState: newState,
    }

    content = updateComponent(origin, content)

    for (let i = 0; i < origin.actions.length; i++) {
        const action = origin.actions[i]
        if (typeof action.target !== "string") {
            continue
        }

        const actionTarget: PlayerLike = getComponent(
            action.target,
            content.components,
        )

        let targetState: BallState = actionTarget.ballState
        let deleteAction = false

        if (isNextInPath(origin, actionTarget, content.components)) {
            switch (newState) {
                case BallState.PASSED:
                case BallState.PASSED_ORIGIN:
                    targetState = BallState.NONE
                    break
                case BallState.HOLDS_ORIGIN:
                    targetState = BallState.HOLDS_BY_PASS
                    break
                default:
                    targetState = newState
            }
        } else if (
            newState === BallState.NONE &&
            action.type === ActionKind.SHOOT
        ) {
            /// if the new state removes the ball from the player, remove all actions that were meant to shoot the ball
            deleteAction = true
            targetState = BallState.NONE // Then remove the ball for the target as well
        } else if (
            (newState === BallState.HOLDS_BY_PASS ||
                newState === BallState.HOLDS_ORIGIN) &&
            action.type === ActionKind.SCREEN
        ) {
            targetState = BallState.HOLDS_BY_PASS
            const screenPhantom = getPlayerNextTo(
                origin,
                1,
                content.components,
            )!
            if (
                screenPhantom.type === "phantom" &&
                screenPhantom.pos.type === "follows"
            ) {
                content = removePlayer(screenPhantom, content)
                origin = getComponent(origin.id, content.components)
            }
        }

        if (deleteAction) {
            content = removeAction(origin, i, content)
            origin = getComponent(origin.id, content.components)
            i-- // step back
        } else {
            // do not change the action type if it is a shoot action
            const { kind, nextState } = getActionKindBetween(
                origin,
                actionTarget,
                newState,
            )

            origin = {
                ...origin,
                ballState: nextState,
                actions: origin.actions.toSpliced(i, 1, {
                    ...action,
                    type: kind,
                }),
            }
            content = updateComponent(origin, content)
        }

        content =
            spreadNewStateFromOriginStateChange(
                actionTarget,
                targetState,
                content,
            ) ?? content
    }

    return content
}
