import { Container } from "./container"
import { ContainerConfig } from "./containerconfig"
import { Keyboard } from "../events/keyboard"
import { EventUtil } from "../events/eventutil"
import { BreakEvent } from "../events/breakevent"
import { GameEvent } from "../events/gameevent"
import {
  bounceHan,
  bounceHanBlend,
  mathavanAdapter,
} from "../model/physics/physics"
import { strongeAdapter } from "../model/physics/stronge"
import { ReplayCodec } from "../utils/replay-codec"
import { Assets } from "../view/assets"
import { SnookerConfig } from "../utils/snookerconfig"
import { ThreeCushionConfig } from "../utils/threecushionconfig"
import { logNetEvent } from "../utils/event-log"
import { Session } from "../network/client/session"
import { MessageRelay } from "../network/client/messagerelay"
import { BotRelay } from "../network/bot/botrelay"
import { Logger } from "../network/bot/logger"
import { getUID } from "../utils/uid"
import { DrillPanel } from "../view/drillpanel"
import { AnalysisPanel } from "../view/analysispanel"
import { applyPhysicsParams } from "../utils/physicsparams"

/**
 * Integrate game container into HTML page
 */
export class BrowserContainer {
  container: Container
  canvas3d
  tableId
  clientId
  wss
  lobbyUrl
  ruletype
  playername: string
  replay: string | null
  messageRelay: MessageRelay | null = null
  breakState: {
    init: any
    shots: any[]
    now: number
    score: number
    players?: { player1: string; player2: string }
    tableSize?: number
  } = {
    init: null,
    shots: [],
    now: 0,
    score: 0,
  }
  cushionModel
  spectator
  first
  assets: Assets
  now
  botMode: boolean = false
  botName: string = ""
  practiceMode: boolean = false
  drillMode: boolean = false
  analysisMode: boolean = false
  examMode: boolean = false
  speedrun: boolean = false
  localMesh: boolean = false
  readonly botDelay: number = 500
  constructor(canvas3d, params) {
    this.now = Date.now()
    this.playername = params.get("userName") ?? "Anon"
    this.tableId = params.get("tableId") ?? "default"
    this.clientId = params.get("userId") ?? `G_${getUID()}`
    this.replay = params.get("state")
    this.ruletype = params.get("ruletype") ?? "nineball"
    // This Cloudflare Pages edition is deliberately local-only. Ignore all
    // transport and spectator query parameters so a crafted URL cannot turn
    // the public build back into an nchan/WebSocket client.
    this.lobbyUrl = undefined
    this.wss = undefined
    this.canvas3d = canvas3d
    this.cushionModel = this.cushion(params.get("cushionModel"))
    this.spectator = false
    this.first = false
    this.botMode = params.has("bot")
    this.botName = params.get("bot") ?? ""
    this.practiceMode = params.has("practice")
      ? params.get("practice") !== "false"
      : this.ruletype !== "nineball"
    this.drillMode = params.has("drill")
    this.analysisMode = params.has("analysis")
    this.examMode = params.has("exam")
    this.speedrun = params.has("speedrun")
    this.localMesh = params.has("localmesh")
    SnookerConfig.reds = Number.parseInt(params.get("reds") ?? "15") || 15
    ThreeCushionConfig.raceTo =
      Number.parseInt(params.get("raceTo") ?? "7") || 7
    console.log(
      `clientId: ${this.clientId} playername: ${this.playername} tableId: ${this.tableId} spectator: ${this.spectator} botMode: ${this.botMode} practiceMode: ${this.practiceMode} drillMode: ${this.drillMode}`
    )
    Session.init(
      this.clientId,
      this.playername,
      this.tableId,
      this.spectator,
      this.botMode,
      this.examMode,
      this.practiceMode,
      Number.parseInt(params.get("lod") ?? "2"),
      this.first,
      this.speedrun
    )
    const localSessionParams = new URLSearchParams(params)
    for (const key of [...localSessionParams.keys()]) {
      if (key.startsWith("opponent.")) {
        localSessionParams.delete(key)
      }
    }
    Session.getInstance().applyUrlParams(localSessionParams)
    console.log(Session.getInstance())
    applyPhysicsParams(params)
  }

  cushion(model) {
    switch (model) {
      case "bounceHan":
        return bounceHan
      case "bounceHanBlend":
        return bounceHanBlend
      case "stronge": {
        return strongeAdapter
      }
      default:
        return mathavanAdapter
    }
  }

  private createContainer() {
    // Analysis mode reuses the drill rules (no rings/popups); only its panel and
    // layout differ.
    const effectiveRuletype =
      (this.drillMode || this.analysisMode) && this.ruletype === "threecushion"
        ? "threecushion-drill"
        : this.ruletype
    const config: ContainerConfig = {
      element: this.canvas3d,
      log: console.log,
      assets: this.assets,
      ruletype: effectiveRuletype,
      keyboard: new Keyboard(this.canvas3d, { disabled: this.analysisMode }),
      id: this.playername,
      relay: this.messageRelay,
      messagingUrl: undefined,
      scoreReporter: null,
      replayMode: !!this.replay,
      botMode: this.botMode,
      isSinglePlayer: !this.wss && !this.botMode && !this.replay,
      examMode: this.examMode,
    }
    return new Container(config)
  }

  start() {
    // If replay state embeds a non-default tableSize and the URL doesn't have
    // one yet, add it and redirect so that TableGeometry, scaleTableModel, and
    // Camera all see the correct value from the start.
    if (this.replay) {
      try {
        const state = this.parse(this.replay)
        const stateTableSize = state.tableSize
        if (
          stateTableSize !== undefined &&
          stateTableSize !== 10 &&
          !new URLSearchParams(globalThis.location.search).has("tableSize")
        ) {
          const url = new URL(globalThis.location.href)
          url.searchParams.set("tableSize", String(stateTableSize))
          globalThis.location.href = url.toString()
          return
        }
      } catch {
        // If parsing fails, proceed normally
      }
    }

    this.assets = new Assets(this.ruletype)
    if (this.localMesh) {
      this.assets.createLocal()
      this.onAssetsReady()
    } else {
      this.assets.loadFromWeb(() => {
        this.onAssetsReady()
      })
    }
  }

  private initBotMode() {
    this.container = this.createContainer()
    this.container.init()
    const logs = new Logger()
    this.messageRelay = new BotRelay(logs, this.container)
    this.messageRelay.subscribe(this.tableId, (e) => {
      this.netEvent(e)
    })
    this.container.notify({
      type: "Info",
      title: this.ruletype,
      subtext: `Playing vs 🦞 ${this.botName}`,
      extra: "You first",
    } as const)
  }

  private initLocalMode() {
    this.messageRelay = null
    this.container = this.createContainer()
    this.container.init()
  }

  onAssetsReady() {
    console.log(`${this.playername} assets ready`)
    if (this.botMode) {
      this.initBotMode()
    } else {
      this.initLocalMode()
    }

    this.container.broadcast = (e) => {
      this.broadcast(e)
    }
    this.container.table.cushionModel = this.cushionModel
    if (this.analysisMode) {
      new AnalysisPanel(this.container)
    } else if (this.drillMode) {
      new DrillPanel(this.container)
    }
    this.setReplayLink()

    this.initGameLoop()

    // trigger animation loops
    this.container.animate(performance.now())

    // Expose container for debugging/playwright verification
    globalThis.container = this.container
  }

  private initGameLoop() {
    if (this.replay) {
      this.startReplay(this.replay)
    } else if (this.container.isSinglePlayer) {
      this.container.eventQueue.push(new BreakEvent())
    }
  }

  netEvent(e: string) {
    const event = EventUtil.fromSerialised(e)
    if (event.clientId === Session.getInstance().clientId) {
      return
    }

    if (!Session.getInstance().vsNotificationShown) {
      this.container.notification.clear()
    }

    logNetEvent(this.playername, event, "receive")

    const session = Session.getInstance()
    if (
      !session.vsNotificationShown &&
      !this.botMode &&
      !this.spectator &&
      session.playername &&
      session.opponentName
    ) {
      const names = session.orderedNamesForHud()
      if (names.p1Name && names.p2Name) {
        this.container.notifyLocal({
          type: "Info",
          title: `${this.ruletype}, ${names.p1Name} vs ${names.p2Name}`,
          extra:
            this.ruletype === "threecushion"
              ? `Race to: ${ThreeCushionConfig.raceTo}`
              : undefined,
        })
        session.vsNotificationShown = true
      }
    }
    this.container.eventQueue.push(event)
  }

  broadcast(event: GameEvent) {
    if (this.messageRelay) {
      event.clientId = Session.getInstance().clientId
      event.playername = Session.getInstance().playername
      logNetEvent(this.playername, event, "broadcast")
      this.messageRelay.publish(this.tableId, EventUtil.serialise(event))
    }
  }

  setReplayLink() {
    const url = globalThis.location.href.split("?")[0]
    const prefix = `${url}?ruletype=${this.ruletype}&state=`
    this.container.linkFormatter.replayUrl = prefix
  }

  startReplay(replay) {
    this.breakState = this.parse(replay)
    const session = Session.getInstance()
    if (this.breakState.players) {
      session.playername = this.breakState.players.player1
      session.opponentName = this.breakState.players.player2
    }
    const orderedScores = session.orderedScoresForHud()
    this.container.updateScoreHud(orderedScores.p1, orderedScores.p2, 0, 0)
    const breakEvent = new BreakEvent(
      this.breakState.init,
      this.breakState.shots
    )
    this.container.eventQueue.push(breakEvent)
  }

  parse(s) {
    try {
      return JSON.parse(s)
    } catch {
      return ReplayCodec.decode(s)
    }
  }
}
