import chalk from "chalk"

import { connect } from "../src/room.js"

import MockConnection, {
  getUserID,
  getPeerHandle,
  getPeerFootprint,
  send,
  //sendListener,
  broadcast,
  broadcastListener,
  terminatePeer,
  destructor,
  addEventListener,
  eventListeners,
} from "../src/connection/MockConnection.js"

import {
  handshake,
  dotDraw,
  dotErase,
  pathDraw,
  pathErase,
} from "./benchmark.data.js"

function printBenchmark(title, iterations, results) {
  process.stdout.write(`\n  ${title} (${iterations} iterations):\n`)

  for (const title in results) {
    const { time, packets, size } = results[title]

    process.stdout.write(
      chalk`    {yellow ⧗} {dim ${title}:}  {yellow.inverse ${(
        time /
        (1e6 * iterations)
      ).toFixed(
        3,
      )}ms / it} {magenta.inverse ${size}B} {dim in ${packets} packet(s)}\n`,
    )
  }

  process.stdout.write(`\n`)
}

let room = null

describe("drawing app mesh", () => {
  beforeEach(async () => {
    getUserID.mockClear()
    getPeerHandle.mockClear()
    getPeerFootprint.mockClear()
    send.mockClear()
    broadcast.mockClear()
    terminatePeer.mockClear()
    destructor.mockClear()
    addEventListener.mockClear()
    MockConnection.mockClear()

    room = await connect("data", MockConnection)
    eventListeners.get("messageReceived")({ detail: handshake })
  })

  afterEach(() => {
    room.disconnect()
    room = null
  })

  it("communicates a single draw and erase update", () => {
    let dotID

    return new Promise((resolve) => {
      broadcastListener.callback = (channel, message) => {
        expect(channel).toEqual("y-js")
        expect(message.message instanceof Uint8Array).toBe(true)
        resolve()
      }

      dotID = room.addPath(dotDraw[0])
    }).then(
      () =>
        new Promise((resolve) => {
          broadcastListener.callback = (channel, message) => {
            expect(channel).toEqual("y-js")
            expect(message.message instanceof Uint8Array).toBe(true)
            resolve()
          }

          room.extendErasureIntervals(dotID, dotErase[0][0], dotErase[0][1])
        }),
    )
  })

  it("benchmarks a single draw and erase update sequentially", () => {
    const ITERATIONS = 1000

    jest.setTimeout(ITERATIONS * 10)

    const dotIDs = []
    let prevTime

    let broadcasts = 0

    let addTime = 0
    let addPackets = 0
    let addSize = 0
    let eraseTime = 0
    let erasePackets = 0
    let eraseSize = 0

    return new Promise((resolve) => {
      broadcastListener.callback = (channel, message) => {
        const currTime = process.hrtime()

        broadcasts += 1

        if (broadcasts <= ITERATIONS) {
          addTime +=
            (currTime[0] - prevTime[0]) * 1e9 + (currTime[1] - prevTime[1])
          addPackets += 1
          addSize += message.message.length
        } else {
          eraseTime +=
            (currTime[0] - prevTime[0]) * 1e9 + (currTime[1] - prevTime[1])
          erasePackets += 1
          eraseSize += message.message.length
        }

        prevTime = process.hrtime()

        if (broadcasts < ITERATIONS) {
          dotIDs.push(room.addPath(dotDraw[0]))
        } else if (broadcasts < ITERATIONS * 2) {
          room.extendErasureIntervals(
            dotIDs[broadcasts - ITERATIONS],
            dotErase[0][0],
            dotErase[0][1],
          )
        } else {
          printBenchmark("single draw and erase [sequential]", ITERATIONS, {
            addPath: { time: addTime, packets: addPackets, size: addSize },
            extendErasureIntervals: {
              time: eraseTime,
              packets: erasePackets,
              size: eraseSize,
            },
          })

          resolve()
        }
      }

      prevTime = process.hrtime()

      dotIDs.push(room.addPath(dotDraw[0]))
    })
  })

  it("benchmarks a single draw and erase update in parallel", () => {
    const ITERATIONS = 1000

    jest.setTimeout(ITERATIONS * 10)

    const dotIDs = []
    let prevTime

    let addTime
    let addPackets = 0
    let addSize = 0
    let eraseTime
    let erasePackets = 0
    let eraseSize = 0

    let timeout

    return new Promise((resolve) => {
      broadcastListener.callback = (channel, message) => {
        const currTime = process.hrtime()

        addTime =
          (currTime[0] - prevTime[0]) * 1e9 + (currTime[1] - prevTime[1])
        addPackets += 1
        addSize += message.message.length

        clearTimeout(timeout)
        timeout = setTimeout(() => resolve(), 1000)
      }

      prevTime = process.hrtime()

      for (let i = 0; i < ITERATIONS; i++) {
        dotIDs.push(room.addPath(dotDraw[0]))
      }
    }).then(
      () =>
        new Promise((resolve) => {
          broadcastListener.callback = (channel, message) => {
            const currTime = process.hrtime()

            eraseTime =
              (currTime[0] - prevTime[0]) * 1e9 + (currTime[1] - prevTime[1])
            erasePackets += 1
            eraseSize += message.message.length

            clearTimeout(timeout)
            timeout = setTimeout(() => {
              printBenchmark("single draw and erase [parallel]", ITERATIONS, {
                addPath: { time: addTime, packets: addPackets, size: addSize },
                extendErasureIntervals: {
                  time: eraseTime,
                  packets: erasePackets,
                  size: eraseSize,
                },
              })

              resolve()
            }, 1000)
          }

          prevTime = process.hrtime()

          for (let i = 0; i < ITERATIONS; i++) {
            room.extendErasureIntervals(
              dotIDs[i],
              dotErase[0][0],
              dotErase[0][1],
            )
          }
        }),
    )
  })

  it("communicates a path draw and erase update", () => {
    let pathID

    let timeout

    return new Promise((resolve) => {
      broadcastListener.callback = (channel, message) => {
        expect(channel).toEqual("y-js")
        expect(message.message instanceof Uint8Array).toBe(true)

        clearTimeout(timeout)
        timeout = setTimeout(() => resolve(), 1000)
      }

      pathID = room.addPath(pathDraw[0])
      for (let i = 1; i < pathDraw.lenth; i++) {
        room.extendPath(pathID, pathDraw[1])
      }
    }).then(
      () =>
        new Promise((resolve) => {
          broadcastListener.callback = (channel, message) => {
            expect(channel).toEqual("y-js")
            expect(message.message instanceof Uint8Array).toBe(true)

            clearTimeout(timeout)
            timeout = setTimeout(() => resolve(), 1000)
          }

          for (let i = 0; i < pathErase.length; i++) {
            room.extendErasureIntervals(
              pathID,
              pathErase[i][0],
              pathErase[i][1],
            )
          }
        }),
    )
  })
})