"use strict"
import CanvasAdapter from './CanvasAdapter.js'
import BoundingRectangle from './BoundingRectangle.js'
import Bitmap from './Bitmap.js'
import {run} from './gameloop.js'
import Camera from './Camera.js'
import EventAdapter from './EventAdapter.js'
import WsClient from './WsClient.js'
import Graphics from './Graphics.js'

if (!GAME_ID) throw '[ GAME_ID not set ]'
if (!WS_ADDRESS) throw '[ WS_ADDRESS not set ]'

function addCanvasToDom(canvas) {
    document.body.append(canvas)
    return canvas
}

function pointInBounds(pt, rect) {
  return pt.x >= rect.x0
    && pt.x <= rect.x1
    && pt.y >= rect.y0
    && pt.y <= rect.y1
}

function getSurroundingTilesByIdx(puzzle, idx) {
  var _X = puzzle.info.coords[idx].x
  var _Y = puzzle.info.coords[idx].y

  return [
    // top
    _Y === 0 ? null : puzzle.tiles[idx - puzzle.info.tiles_x],
    // right
    (_X === puzzle.info.tiles_x - 1) ? null : puzzle.tiles[idx + 1],
    // bottom
    (_Y === puzzle.info.tiles_y - 1) ? null : puzzle.tiles[idx + puzzle.info.tiles_x],
    // left
    _X === 0 ? null : puzzle.tiles[idx - 1]
  ]
}

async function createPuzzleTileBitmaps(bitmap, tiles, info) {
  let img = await bitmap.toImage()
  var tileSize = info.tileSize
  var tileMarginWidth = info.tileMarginWidth
  var tileDrawSize = info.tileDrawSize
  var tileRatio = tileSize / 100.0

  var curvyCoords = [
    0, 0, 40, 15, 37, 5,
    37, 5, 40, 0, 38, -5,
    38, -5, 20, -20, 50, -20,
    50, -20, 80, -20, 62, -5,
    62, -5, 60, 0, 63, 5,
    63, 5, 65, 15, 100, 0
  ];

  const bitmaps = new Array(tiles.length)

  const paths = {}
  function pathForShape(shape) {
    const key = `${shape.top}${shape.right}${shape.left}${shape.bottom}`
    if (paths[key]) {
      return paths[key]
    }

    const path = new Path2D()
    const topLeftEdge = { x: tileMarginWidth, y: tileMarginWidth }
    path.moveTo(topLeftEdge.x, topLeftEdge.y)
    for (let i = 0; i < curvyCoords.length / 6; i++) {
      const p1 = pointAdd(topLeftEdge, { x: curvyCoords[i * 6 + 0] * tileRatio, y: shape.top * curvyCoords[i * 6 + 1] * tileRatio })
      const p2 = pointAdd(topLeftEdge, { x: curvyCoords[i * 6 + 2] * tileRatio, y: shape.top * curvyCoords[i * 6 + 3] * tileRatio })
      const p3 = pointAdd(topLeftEdge, { x: curvyCoords[i * 6 + 4] * tileRatio, y: shape.top * curvyCoords[i * 6 + 5] * tileRatio })
      path.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
    }
    const topRightEdge = pointAdd(topLeftEdge, { x: tileSize, y: 0 })
    for (let i = 0; i < curvyCoords.length / 6; i++) {
      const p1 = pointAdd(topRightEdge, { x: -shape.right * curvyCoords[i * 6 + 1] * tileRatio, y: curvyCoords[i * 6 + 0] * tileRatio })
      const p2 = pointAdd(topRightEdge, { x: -shape.right * curvyCoords[i * 6 + 3] * tileRatio, y: curvyCoords[i * 6 + 2] * tileRatio })
      const p3 = pointAdd(topRightEdge, { x: -shape.right * curvyCoords[i * 6 + 5] * tileRatio, y: curvyCoords[i * 6 + 4] * tileRatio })
      path.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
    }
    const bottomRightEdge = pointAdd(topRightEdge, { x: 0, y: tileSize })
    for (let i = 0; i < curvyCoords.length / 6; i++) {
      let p1 = pointSub(bottomRightEdge, { x: curvyCoords[i * 6 + 0] * tileRatio, y: shape.bottom * curvyCoords[i * 6 + 1] * tileRatio })
      let p2 = pointSub(bottomRightEdge, { x: curvyCoords[i * 6 + 2] * tileRatio, y: shape.bottom * curvyCoords[i * 6 + 3] * tileRatio })
      let p3 = pointSub(bottomRightEdge, { x: curvyCoords[i * 6 + 4] * tileRatio, y: shape.bottom * curvyCoords[i * 6 + 5] * tileRatio })
      path.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
    }
    const bottomLeftEdge = pointSub(bottomRightEdge, { x: tileSize, y: 0 })
    for (let i = 0; i < curvyCoords.length / 6; i++) {
      let p1 = pointSub(bottomLeftEdge, { x: -shape.left * curvyCoords[i * 6 + 1] * tileRatio, y: curvyCoords[i * 6 + 0] * tileRatio })
      let p2 = pointSub(bottomLeftEdge, { x: -shape.left * curvyCoords[i * 6 + 3] * tileRatio, y: curvyCoords[i * 6 + 2] * tileRatio })
      let p3 = pointSub(bottomLeftEdge, { x: -shape.left * curvyCoords[i * 6 + 5] * tileRatio, y: curvyCoords[i * 6 + 4] * tileRatio })
      path.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
    }
    paths[key] = path
    return path
  }

  for (let tile of tiles) {
    const srcRect = srcRectByIdx(info, tile.idx)
    const path = pathForShape(info.shapes[tile.idx])


    const c = Graphics.createCanvas(tileDrawSize, tileDrawSize)
    const ctx = c.getContext('2d')
    // -----------------------------------------------------------
    // -----------------------------------------------------------
    ctx.lineWidth = 2
    ctx.stroke(path)
    // -----------------------------------------------------------
    // -----------------------------------------------------------
    ctx.save();
    ctx.clip(path)
    ctx.drawImage(
      img,
      srcRect.x0 - tileMarginWidth,
      srcRect.y0 - tileMarginWidth,
      tileDrawSize,
      tileDrawSize,
      0,
      0,
      tileDrawSize,
      tileDrawSize,
    )
    ctx.stroke(path)
    ctx.restore();

    const bitmap = Graphics.canvasToBitmap(c, ctx)

    bitmaps[tile.idx] = bitmap
  }

  return bitmaps
}

function srcRectByIdx(puzzleInfo, idx) {
  let c = puzzleInfo.coords[idx]
  let cx = c.x * puzzleInfo.tileSize
  let cy = c.y * puzzleInfo.tileSize
  return new BoundingRectangle(
    cx,
    cx + puzzleInfo.tileSize,
    cy,
    cy + puzzleInfo.tileSize
  )
}

const pointSub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y })

const pointAdd = (a, b) => ({x: a.x + b.x, y: a.y + b.y})

// Returns the index of the puzzle tile with the highest z index
// that is not finished yet and that matches the position
const unfinishedTileByPos = (puzzle, pos) => {
  let maxZ = -1
  let tileIdx = -1
  for (let idx = 0; idx < puzzle.tiles.length; idx++) {
    const tile = puzzle.tiles[idx]
    if (tile.owner === -1) {
      continue
    }

    const collisionRect = new BoundingRectangle(
      tile.pos.x,
      tile.pos.x + puzzle.info.tileSize - 1,
      tile.pos.y,
      tile.pos.y + puzzle.info.tileSize - 1,
    )
    if (pointInBounds(pos, collisionRect)) {
      if (maxZ === -1 || tile.z > maxZ) {
        maxZ = tile.z
        tileIdx = idx
      }
    }
  }
  return tileIdx
}

async function loadPuzzleBitmaps(puzzle) {
  // load bitmap, to determine the original size of the image
  const bmp = await Graphics.loadImageToBitmap(puzzle.info.imageUrl)

  // creation of tile bitmaps
  // then create the final puzzle bitmap
  // NOTE: this can decrease OR increase in size!
  const bmpResized = Graphics.resizeBitmap(bmp, puzzle.info.width, puzzle.info.height)
  return await createPuzzleTileBitmaps(bmpResized, puzzle.tiles, puzzle.info)
}

function uniqId() {
  return Date.now().toString(36) + Math.random().toString(36).substring(2)
}

function initme() {
  // return uniqId()
  let ID = localStorage.getItem("ID")
  if (!ID) {
    ID = uniqId()
    localStorage.setItem("ID", ID)
  }
  return ID
}

function setupNetwork(me) {
  const wsc = new WsClient(WS_ADDRESS, me)
  wsc.connect()
  return wsc
}

async function main () {
  let gameId = GAME_ID
  let me = initme()

  let cursorGrab = await Graphics.loadImageToBitmap('/grab.png')
  let cursorHand = await Graphics.loadImageToBitmap('/hand.png')

  let conn = setupNetwork(me + '|' + gameId)
  conn.send(JSON.stringify({ type: 'init' }))
  conn.onSocket('message', async ({data}) => {
    const d = JSON.parse(data)
    if (d.type === 'init') {
      console.log('the game ', d.game)
      let bitmaps = await loadPuzzleBitmaps(d.game.puzzle)
      startGame(d.game, bitmaps, conn)
    } else {
      // console.log(d)
    }
  })

  const _STATE = {
    changes: [],
  }
  let _STATE_CHANGED = false

  class renderRect {
    constructor() {
      this.reset()
    }
    get () {
      return this.x0 === null ? null : [
        {x0: this.x0, x1: this.x1, y0: this.y0, y1: this.y1}
      ]
    }
    add (pos, offset) {
      const x0 = pos.x - offset
      const x1 = pos.x + offset
      const y0 = pos.y - offset
      const y1 = pos.y + offset
      this.x0 = this.x0 === null ? x0 : Math.min(this.x0, x0)
      this.x1 = this.x1 === null ? x1 : Math.max(this.x1, x1)
      this.y0 = this.y0 === null ? y0 : Math.min(this.y0, y0)
      this.y1 = this.y1 === null ? y1 : Math.max(this.y1, y1)
    }
    reset () {
      this.x0 = null
      this.x1 = null
      this.y0 = null
      this.y1 = null
    }
  }

  const startGame = (game, bitmaps, conn) => {
    let puzzle = game.puzzle
    let players = game.players
    // information for next render cycle
    let rectPlayer = new renderRect()
    let rerenderPlayer = true
    let rectTable = new renderRect()
    let rerenderTable = true
    let rerender = true

    const changePlayer = (change) => {
      for (let k of Object.keys(change)) {
        players[me][k] = change[k]
      }
      _STATE.changes.push({type: 'change_player', player: players[me]})
      _STATE_CHANGED = true
    }
    const changeData = (change) => {
      for (let k of Object.keys(change)) {
        puzzle.data[k] = change[k]
      }
      _STATE.changes.push({type: 'change_data', data: puzzle.data})
      _STATE_CHANGED = true
    }

    const changeTile = (t, change) => {
      for (let k of Object.keys(change)) {
        t[k] = change[k]
      }
      _STATE.changes.push({type: 'change_tile', tile: t})
      _STATE_CHANGED = true
    }

    // Create a dom and attach adapters to it so we can work with it
    const canvas = addCanvasToDom(Graphics.createCanvas())
    const adapter = new CanvasAdapter(canvas)
    const evts = new EventAdapter(canvas)

    // initialize some view data
    // this global data will change according to input events
    const viewport = new Camera(canvas)

    conn.onSocket('message', ({data}) => {
      const d = JSON.parse(data)
      if (d.type === 'state_changed' && d.origin !== me) {
        for (let change of d.changes) {
          switch (change.type) {
            case 'change_player': {
              if (players[change.player.id]) {
                rectPlayer.add(viewport.worldToViewport(players[change.player.id]), cursorGrab.width)
              }

              players[change.player.id] = change.player

              rectPlayer.add(viewport.worldToViewport(players[change.player.id]), cursorGrab.width)
            } break;

            case 'change_tile': {
              rectTable.add(puzzle.tiles[change.tile.idx].pos, puzzle.info.tileDrawSize)

              puzzle.tiles[change.tile.idx] = change.tile

              rectTable.add(puzzle.tiles[change.tile.idx].pos, puzzle.info.tileDrawSize)
            } break;
            case 'change_data': {
              puzzle.data = change.data
            } break;
          }
        }
      }
    })

    // Information about what tile is the player currently grabbing
    let grabbingTileIdx = -1

    // The actual place for the puzzle. The tiles may
    // not be moved around infinitely, just on the (invisible)
    // puzzle table. however, the camera may move away from the table
    const puzzleTableColor = [40, 40, 40, 0]
    const puzzleTable = new Bitmap(
      puzzle.info.table.width,
      puzzle.info.table.height,
      puzzleTableColor
    )

    // In the middle of the table, there is a board. this is to
    // tell the player where to place the final puzzle
    const boardColor = [80, 80, 80, 255]
    const board = new Bitmap(
      puzzle.info.width,
      puzzle.info.height,
      boardColor
    )
    const boardPos = {
      x: (puzzleTable.width - board.width) / 2,
      y: (puzzleTable.height - board.height) / 2
    } // relative to table.


    // Some helper functions for working with the grabbing and snapping
    // ---------------------------------------------------------------

    // get all grouped tiles for a tile
    function getGroupedTiles(tile) {
      let grouped = []
      if (tile.group) {
        for (let other of puzzle.tiles) {
          if (other.group === tile.group) {
            grouped.push(other)
          }
        }
      } else {
        grouped.push(tile)
      }
      return grouped
    }

    // put both tiles (and their grouped tiles) in the same group
    const groupTiles = (tile, other) => {
      let targetGroup
      let searchGroups = []
      if (tile.group) {
        searchGroups.push(tile.group)
      }
      if (other.group) {
        searchGroups.push(other.group)
      }
      if (tile.group) {
        targetGroup = tile.group
      } else if (other.group) {
        targetGroup = other.group
      } else {
        changeData({ maxGroup: puzzle.data.maxGroup + 1 })
        targetGroup = puzzle.data.maxGroup
      }

      changeTile(tile, { group: targetGroup })
      changeTile(other, { group: targetGroup })

      if (searchGroups.length > 0) {
        for (let tmp of puzzle.tiles) {
          if (searchGroups.includes(tmp.group)) {
            changeTile(tmp, { group: targetGroup })
          }
        }
      }
    }

    // determine if two tiles are grouped together
    const areGrouped = (t1, t2) => {
      return t1.group && t1.group === t2.group
    }

    // get the center position of a tile
    const tileCenterPos = (tile) => {
      return tileRectByTile(tile).center()
    }

    // get the would-be visible bounding rect if a tile was
    // in given position
    const tileRectByPos = (pos) => {
      return new BoundingRectangle(
        pos.x,
        pos.x + puzzle.info.tileSize,
        pos.y,
        pos.y + puzzle.info.tileSize
      )
    }

    // get the current visible bounding rect for a tile
    const tileRectByTile = (tile) => {
      return tileRectByPos(tile.pos)
    }

    const tilesSortedByZIndex = () => {
      const sorted = puzzle.tiles.slice()
      return sorted.sort((t1, t2) => t1.z - t2.z)
    }

    const setGroupedZIndex = (tile, zIndex) => {
      for (let t of getGroupedTiles(tile)) {
        changeTile(t, { z: zIndex })
      }
    }

    const setGroupedOwner = (tile, owner) => {
      for (let t of getGroupedTiles(tile)) {
        // may only change own tiles or untaken tiles
        if (t.owner === me || t.owner === 0) {
          changeTile(t, { owner: owner })
        }
      }
    }

    const moveGroupedTilesDiff = (tile, diffX, diffY) => {
      for (let t of getGroupedTiles(tile)) {
        changeTile(t, { pos: pointAdd(t.pos, { x: diffX, y: diffY }) })

        // TODO: instead there could be a function to
        //       get min/max x/y of a group
        rectTable.add(tileCenterPos(t), puzzle.info.tileDrawSize)
      }
    }
    const moveGroupedTiles = (tile, dst) => {
      let diff = pointSub(tile.pos, dst)
      moveGroupedTilesDiff(tile, -diff.x, -diff.y)
    }
    const finishGroupedTiles = (tile) => {
      for (let t of getGroupedTiles(tile)) {
        changeTile(t, { owner: -1, z: 1 })
      }
    }
    // ---------------------------------------------------------------







    let _last_mouse = null
    let _last_mouse_down = null
    const onUpdate = () => {
      let last_x = null
      let last_y = null

      if (_last_mouse_down !== null) {
        last_x = _last_mouse_down.x
        last_y = _last_mouse_down.y
      }
      for (let mouse of evts.consumeAll()) {
        const tp = viewport.viewportToWorld(mouse)
        if (mouse.type === 'move') {
          changePlayer({ x: tp.x, y: tp.y })
          if (_last_mouse) {
            rectPlayer.add(_last_mouse, cursorGrab.width)
          }
          rectPlayer.add(mouse, cursorGrab.width)

          if (_last_mouse_down !== null) {
            _last_mouse_down = mouse

            if (last_x === null || last_y === null) {
              last_x = mouse.x
              last_y = mouse.y
            }

            if (grabbingTileIdx >= 0) {
              let tp_last = viewport.viewportToWorld({ x: last_x, y: last_y })
              const diffX = tp.x - tp_last.x
              const diffY = tp.y - tp_last.y

              let t = puzzle.tiles[grabbingTileIdx]
              moveGroupedTilesDiff(t, diffX, diffY)

              // todo: dont +- tileDrawSize, we can work with less?
              rectTable.add(tp, puzzle.info.tileDrawSize)
              rectTable.add(tp_last, puzzle.info.tileDrawSize)
            } else {
              // move the cam
              const diffX = Math.round(mouse.x - last_x)
              const diffY = Math.round(mouse.y - last_y)
              viewport.move(diffX, diffY)
              rerender = true
            }
          }
        } else if (mouse.type === 'down') {
          changePlayer({ down: true })
          rectPlayer.add(mouse, cursorGrab.width)

          _last_mouse_down = mouse
          if (last_x === null || last_y === null) {
            last_x = mouse.x
            last_y = mouse.y
          }

          grabbingTileIdx = unfinishedTileByPos(puzzle, tp)
          console.log(grabbingTileIdx)
          if (grabbingTileIdx >= 0) {
            changeData({ maxZ: puzzle.data.maxZ + 1 })
            setGroupedZIndex(puzzle.tiles[grabbingTileIdx], puzzle.data.maxZ)
            setGroupedOwner(puzzle.tiles[grabbingTileIdx], me)
          }
          console.log('down', tp)

        } else if (mouse.type === 'up') {
          changePlayer({ down: false })
          if (_last_mouse) {
            rectPlayer.add(_last_mouse, cursorGrab.width)
          }
          rectPlayer.add(mouse, cursorGrab.width)

          _last_mouse_down = null
          last_x = null
          last_y === null

          if (grabbingTileIdx >= 0) {
            // Check if the tile was dropped at the correct
            // location

            let tile = puzzle.tiles[grabbingTileIdx]
            setGroupedOwner(tile, 0)
            let pt = pointSub(tile.pos, boardPos)
            let dst = tileRectByPos(pt)
            let srcRect = srcRectByIdx(puzzle.info, grabbingTileIdx)
            if (srcRect.centerDistance(dst) < puzzle.info.snapDistance) {
              // Snap the tile to the final destination
              console.log('ok! !!!')
              moveGroupedTiles(tile, {
                x: srcRect.x0 + boardPos.x,
                y: srcRect.y0 + boardPos.y,
              })
              finishGroupedTiles(tile)
              rectTable.add(tp, puzzle.info.tileDrawSize)
            } else {
              // Snap to other tiles
              const check = (t, off, other) => {
                if (!other || (other.owner === -1) || areGrouped(t, other)) {
                  return false
                }
                let trec_ = tileRectByTile(t)
                let otrec = tileRectByTile(other).moved(
                  off[0] * puzzle.info.tileSize,
                  off[1] * puzzle.info.tileSize
                )
                if (trec_.centerDistance(otrec) < puzzle.info.snapDistance) {
                  moveGroupedTiles(t, { x: otrec.x0, y: otrec.y0 })
                  groupTiles(t, other)
                  setGroupedZIndex(t, t.z)
                  rectTable.add(tileCenterPos(t), puzzle.info.tileDrawSize)
                  return true
                }
                return false
              }

              for (let t of getGroupedTiles(tile)) {
                let others = getSurroundingTilesByIdx(puzzle, t.idx)
                if (
                  check(t, [0, 1], others[0]) // top
                  || check(t, [-1, 0], others[1]) // right
                  || check(t, [0, -1], others[2]) // bottom
                  || check(t, [1, 0], others[3]) // left
                ) {
                  break
                }
              }
            }
          }
          grabbingTileIdx = -1
          console.log('up', tp)
        } else if (mouse.type === 'wheel') {
          if (
            mouse.deltaY < 0 && viewport.zoomIn()
            || mouse.deltaY > 0 && viewport.zoomOut()
          ) {
            rerender = true
            changePlayer({ x: tp.x, y: tp.y })
            if (_last_mouse) {
              rectPlayer.add(_last_mouse, cursorGrab.width)
            }
            rectPlayer.add(mouse, cursorGrab.width)
          }
        }
        // console.log(mouse)
        _last_mouse = mouse
      }
      if (rectTable.get()) {
        rerenderTable = true
      }
      if (rectPlayer.get()) {
        rerenderPlayer = true
      }

      if (_STATE_CHANGED) {
        conn.send(JSON.stringify({
          type: 'state',
          state: _STATE,
        }))
        _STATE.changes = []
        _STATE_CHANGED = false
      }
    }


    // helper for measuring performance
    let _pt = 0
    let _mindiff = 0
    const checkpoint_start = (mindiff) => {
      _pt = performance.now()
      _mindiff = mindiff
    }
    const checkpoint = (n) => {
      const now = performance.now();
      const diff = now - _pt
      if (diff > _mindiff) {
        console.log(n + ': ' + (diff));
      }
      _pt = now;
    }

    // TODO:
    // try out layered rendering and see
    // if it improves performance:
    //   1. background
    //   2. tiles
    //   3. (moving tiles)
    //   4. (players)
    // (currently, if a player moves, everthing needs to be
    //  rerendered at that position manually, maybe it is faster
    //  when using layers)
    const onRender = () => {
      if (!rerenderTable && !rerenderPlayer && !rerender) {
        return
      }

      checkpoint_start(20)

      // draw the puzzle table
      if (rerenderTable) {

        Graphics.fillBitmapCapped(puzzleTable, puzzleTableColor, rectTable.get())
        checkpoint('after fill')

        // draw the puzzle board on the table
        Graphics.mapBitmapToBitmapCapped(board, board.getBoundingRect(), puzzleTable, new BoundingRectangle(
          boardPos.x,
          boardPos.x + board.width - 1,
          boardPos.y,
          boardPos.y + board.height - 1,
        ), rectTable.get())
        checkpoint('imgtoimg')

        // draw all the tiles on the table

        for (let tile of tilesSortedByZIndex()) {
          let rect = new BoundingRectangle(
            puzzle.info.tileDrawOffset + tile.pos.x,
            puzzle.info.tileDrawOffset + tile.pos.x + puzzle.info.tileDrawSize,
            puzzle.info.tileDrawOffset + tile.pos.y,
            puzzle.info.tileDrawOffset + tile.pos.y + puzzle.info.tileDrawSize,
          )
          let bmp = bitmaps[tile.idx]
          Graphics.mapBitmapToBitmapCapped(
            bmp,
            bmp.getBoundingRect(),
            puzzleTable,
            rect,
            rectTable.get()
          )
        }
        checkpoint('tiles')
      }

      if (rerenderTable || rerender) {
        // finally draw the finished table onto the canvas
        // only part of the table may be visible, depending on the
        // camera
        adapter.clear()
        adapter.apply()
        checkpoint('afterclear_1')

        // TODO: improve the rendering
        // atm it is pretty slow (~40-50ms)
        Graphics.mapBitmapToAdapter(
          puzzleTable,
          viewport.rect(),
          adapter,
          adapter.getBoundingRect()
        )
        checkpoint('to_adapter_1')
      } else if (rerenderPlayer) {
        adapter.clearRect(rectPlayer.get())
        checkpoint('afterclear_2')
        Graphics.mapBitmapToAdapterCapped(
          puzzleTable,
          viewport.rect(),
          adapter,
          adapter.getBoundingRect(),
          rectPlayer.get()
        )
        checkpoint('to_adapter_2')
      }

      if (rerenderPlayer) {
        for (let id of Object.keys(players)) {
          let p = players[id]
          let cursor = p.down ? cursorGrab : cursorHand
          let back = viewport.worldToViewport(p)
          Graphics.mapBitmapToAdapter(
            cursor,
            cursor.getBoundingRect(),
            adapter,
            new BoundingRectangle(
              back.x - (cursor.width / 2),
              back.x - (cursor.width / 2) + cursor.width - 1,
              back.y - (cursor.width / 2),
              back.y - (cursor.width / 2) + cursor.height - 1,
            )
          )
        }
        checkpoint('after_players')
      }

      adapter.apply()
      checkpoint('finals')

      rerenderTable = false
      rerenderPlayer = false
      rerender = false
      rectTable.reset()
      rectPlayer.reset()
    }

    run({
      update: onUpdate,
      render: onRender,
    })
  }
}

main()
