import { Vector3 } from "three"
import { Container } from "../../container/container"
import { Aim } from "../../controller/aim"
import { Controller } from "../../controller/controller"
import { PlaceBall } from "../../controller/placeball"
import { WatchAim } from "../../controller/watchaim"
import { PlaceBallEvent } from "../../events/placeballevent"
import { RerackEvent } from "../../events/rerackevent"
import { WatchEvent } from "../../events/watchevent"
import { Ball } from "../../model/ball"
import { Outcome, OutcomeType } from "../../model/outcome"
import { Table } from "../../model/table"
import { Rack } from "../../utils/rack"
import { Rules } from "./rules"
import { R } from "../../model/physics/constants"
import { Respot } from "../../utils/respot"
import { TableGeometry } from "../../view/tablegeometry"
import { TableConfig } from "../../view/tableconfig"
import { StartAimEvent } from "../../events/startaimevent"
import { MatchResultHelper } from "../../network/client/matchresult"
import { Session } from "../../network/client/session"
import { isFirstShot } from "../../utils/utils"
import { roundVec } from "../../utils/three-utils"

export class NineBall implements Rules {
  readonly container: Container

  cueball: Ball
  currentBreak = 0
  previousBreak = 0
  rulename = "nineball"

  constructor(container: Container) {
    this.container = container
  }

  startTurn(): void {
    this.previousBreak = this.currentBreak
    this.currentBreak = 0
  }

  nextCandidateBall(_p1type?: number): Ball | undefined {
    return this.container.table.balls
      .filter((b) => b !== this.cueball && b.onTable())
      .sort((a, b) => (a.label || 0) - (b.label || 0))[0]
  }

  placeBall(target?: Vector3): Vector3 {
    const baulkline = (-R * 11) / 0.5
    if (target) {
      const max = new Vector3(TableGeometry.tableX, TableGeometry.tableY)
      const min = new Vector3(-TableGeometry.tableX, -TableGeometry.tableY)
      if (isFirstShot(this.container.recorder)) {
        max.setX(baulkline)
        min.setX(baulkline)
      }
      return target.clone().clamp(min, max)
    }
    return new Vector3(baulkline, 0, 0)
  }

  readonly asset = "models/p8.min.gltf"

  tableGeometry(): void {
    TableConfig.apply(this.rulename, TableConfig.tableSizeFromUrl())
  }

  table(): Table {
    const table = new Table(this.rack())
    this.cueball = table.cueball
    return table
  }

  rack(): Ball[] {
    return Rack.fromInitParam(Rack.diamond())
  }

  update(outcome: Outcome[]): Controller {
    const reason = NineBall.foulReason(this.container.table, outcome)

    if (reason) {
      return this.handleFoul(outcome, reason)
    }

    if (Outcome.potCount(outcome) > 0) {
      return this.handlePot(outcome)
    }

    return this.handleMiss()
  }

  private handleFoul(outcome: Outcome[], reason: string): Controller {
    this.container.notify({
      type: "Foul",
      title: "FOUL",
      subtext: reason,
      extra: "Ball in hand",
    })
    this.startTurn()
    const pots = Outcome.pots(outcome)
    const nineBallPotted = pots.includes(this.container.table.balls[9])
    const cueball = this.container.table.cueball

    if (nineBallPotted) {
      this.respotAndBroadcastNineBall(outcome)
    }

    const startPos = cueball.onTable() ? cueball.pos.clone() : this.placeBall()
    roundVec(startPos)
    const placeBallEvent = new PlaceBallEvent(startPos, undefined, true)
    this.container.sendEvent(placeBallEvent)

    if (this.container.isSinglePlayer) {
      return new PlaceBall(this.container, startPos)
    }
    return new WatchAim(this.container)
  }

  private handlePot(outcome: Outcome[]): Controller {
    const table = this.container.table
    const pots = Outcome.potCount(outcome)
    this.currentBreak += pots
    Session.getInstance().addMyScore(pots)

    this.container.sound.playSuccess(table.inPockets())
    if (this.isEndOfGame(outcome)) {
      return this.handleGameEnd(true)
    }

    this.container.sendEvent(new WatchEvent(table.serialise()))
    return new Aim(this.container)
  }

  handleGameEnd(isWinner: boolean, endSubtext?: string): Controller {
    return MatchResultHelper.presentGameEnd(
      this.container,
      this.rulename,
      isWinner,
      endSubtext
    )
  }

  private handleMiss(): Controller {
    const table = this.container.table
    // if no pot and no foul switch to other player
    this.container.sendEvent(new StartAimEvent())
    if (this.container.isSinglePlayer) {
      this.container.sendEvent(new WatchEvent(table.serialise()))
      this.startTurn()
      return new Aim(this.container)
    }
    return new WatchAim(this.container)
  }

  isPartOfBreak(outcome: Outcome[]): boolean {
    return Outcome.isBallPottedNoFoul(this.container.table.cueball, outcome)
  }

  isEndOfGame(outcome: Outcome[]): boolean {
    const nineBall = this.container.table.balls[9]
    const nineBallPotted = Outcome.pots(outcome).includes(nineBall)
    return nineBallPotted && !this.isFoul(outcome)
  }

  otherPlayersCueBall(): Ball {
    // only for three cushion
    return this.cueball
  }

  secondToPlay(): void {
    // only for three cushion
  }

  allowsPlaceBall(): boolean {
    return true
  }

  protected isFoul(outcome: Outcome[]): boolean {
    return NineBall.foulReason(this.container.table, outcome) !== null
  }

  foulReason(outcome: Outcome[]): string | null {
    return NineBall.foulReason(this.container.table, outcome)
  }

  getAmountScored(outcome: Outcome[]): number {
    return Outcome.potCount(outcome)
  }

  respot(outcome: Outcome[]): Ball[] {
    const nineBall = this.container.table.balls[9]
    if (nineBall && Outcome.pots(outcome).includes(nineBall)) {
      Respot.nineBall(this.container.table)
      return [nineBall]
    }
    return []
  }

  public static foulReason(table: Table, outcome: Outcome[]): string | null {
    const cueball = table.cueball

    // 1. Cue ball potted
    if (Outcome.isCueBallPotted(cueball, outcome)) {
      return "Cue ball potted"
    }

    // 2. Wrong ball hit first
    const lowestBall = NineBall.getLowestBallAtStartOfShot(table, outcome)
    const firstCollision = Outcome.firstCollision(
      Outcome.cueBallFirst(cueball, outcome)
    )

    if (!firstCollision) {
      return "No ball hit"
    }

    if (firstCollision.ballB !== lowestBall) {
      if (Session.isPracticeMode()) {
        if (
          firstCollision.ballB === table.balls[9] &&
          NineBall.hasOtherObjectBalls(table)
        ) {
          return "Wrong ball hit first"
        }
      } else {
        return "Wrong ball hit first"
      }
    }

    // 3. No cushion after contact
    if (Outcome.potCount(outcome) === 0) {
      // Find cushions after first collision
      const firstCollisionIndex = outcome.indexOf(firstCollision)
      const cushionsAfter = outcome
        .slice(firstCollisionIndex + 1)
        .some((o) => o.type === OutcomeType.Cushion)
      if (!cushionsAfter) {
        return "No cushion after contact"
      }
    }

    return null
  }

  public static getLowestBallAtStartOfShot(
    table: Table,
    outcome: Outcome[]
  ): Ball | undefined {
    const potted = Outcome.pots(outcome)
    const onTable = table.balls.filter(
      (b) => b !== table.cueball && b.onTable()
    )
    const all = [...potted, ...onTable]
    all.sort((a, b) => (a.label || 0) - (b.label || 0))
    return all[0]
  }

  private static hasOtherObjectBalls(table: Table): boolean {
    return table.balls.some(
      (b) => b !== table.cueball && b !== table.balls[9] && b.onTable()
    )
  }

  private respotAndBroadcastNineBall(outcome: Outcome[]) {
    const respotted = this.respot(outcome)
    const nineBall = respotted[0]
    if (nineBall) {
      nineBall.fround()
      const respotEvent = RerackEvent.fromJson({
        balls: [nineBall.serialise()],
      })
      console.log("Respot nine ball sending rerack event", respotEvent)
      this.container.sendEvent(respotEvent)
    }
  }
}
