/* eslint-disable
    camelcase,
    handle-callback-err,
    no-return-assign,
    no-undef,
    no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
 * decaffeinate suggestions:
 * DS101: Remove unnecessary use of Array.from
 * DS102: Remove unnecessary code created because of implicit returns
 * DS202: Simplify dynamic range loops
 * DS205: Consider reworking code to avoid use of IIFEs
 * DS207: Consider shorter variations of null checks
 * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
 */
const DocUpdaterClient = require('../../acceptance/coffee/helpers/DocUpdaterClient')
// MockTrackChangesApi = require "../../acceptance/js/helpers/MockTrackChangesApi"
// MockWebApi = require "../../acceptance/js/helpers/MockWebApi"
const assert = require('assert')
const async = require('async')

const insert = function (string, pos, content) {
  const result = string.slice(0, pos) + content + string.slice(pos)
  return result
}

const transform = function (op1, op2) {
  if (op2.p < op1.p) {
    return {
      p: op1.p + op2.i.length,
      i: op1.i
    }
  } else {
    return op1
  }
}

class StressTestClient {
  constructor(options) {
    if (options == null) {
      options = {}
    }
    this.options = options
    if (this.options.updateDelay == null) {
      this.options.updateDelay = 200
    }
    this.project_id = this.options.project_id || DocUpdaterClient.randomId()
    this.doc_id = this.options.doc_id || DocUpdaterClient.randomId()
    this.pos = this.options.pos || 0
    this.content = this.options.content || ''

    this.client_id = DocUpdaterClient.randomId()
    this.version = this.options.version || 0
    this.inflight_op = null
    this.charCode = 0

    this.counts = {
      conflicts: 0,
      local_updates: 0,
      remote_updates: 0,
      max_delay: 0
    }

    DocUpdaterClient.subscribeToAppliedOps((channel, update) => {
      update = JSON.parse(update)
      if (update.error != null) {
        console.error(new Error(`Error from server: '${update.error}'`))
        return
      }
      if (update.doc_id === this.doc_id) {
        return this.processReply(update)
      }
    })
  }

  sendUpdate() {
    const data = String.fromCharCode(65 + (this.charCode++ % 26))
    this.content = insert(this.content, this.pos, data)
    this.inflight_op = {
      i: data,
      p: this.pos++
    }
    this.resendUpdate()
    return (this.inflight_op_sent = Date.now())
  }

  resendUpdate() {
    assert(this.inflight_op != null)
    DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, {
      doc: this.doc_id,
      op: [this.inflight_op],
      v: this.version,
      meta: {
        source: this.client_id
      },
      dupIfSource: [this.client_id]
    })
    return (this.update_timer = setTimeout(() => {
      console.log(
        `[${new Date()}] \t[${this.client_id.slice(
          0,
          4
        )}] WARN: Resending update after 5 seconds`
      )
      return this.resendUpdate()
    }, 5000))
  }

  processReply(update) {
    if (update.op.v !== this.version) {
      if (update.op.v < this.version) {
        console.log(
          `[${new Date()}] \t[${this.client_id.slice(
            0,
            4
          )}] WARN: Duplicate ack (already seen version)`
        )
        return
      } else {
        console.error(
          `[${new Date()}] \t[${this.client_id.slice(
            0,
            4
          )}] ERROR: Version jumped ahead (client: ${this.version}, op: ${
            update.op.v
          })`
        )
      }
    }
    this.version++
    if (update.op.meta.source === this.client_id) {
      if (this.inflight_op != null) {
        this.counts.local_updates++
        this.inflight_op = null
        clearTimeout(this.update_timer)
        const delay = Date.now() - this.inflight_op_sent
        this.counts.max_delay = Math.max(this.counts.max_delay, delay)
        return this.continue()
      } else {
        return console.log(
          `[${new Date()}] \t[${this.client_id.slice(
            0,
            4
          )}] WARN: Duplicate ack`
        )
      }
    } else {
      assert(update.op.op.length === 1)
      this.counts.remote_updates++
      let external_op = update.op.op[0]
      if (this.inflight_op != null) {
        this.counts.conflicts++
        this.inflight_op = transform(this.inflight_op, external_op)
        external_op = transform(external_op, this.inflight_op)
      }
      if (external_op.p < this.pos) {
        this.pos += external_op.i.length
      }
      return (this.content = insert(this.content, external_op.p, external_op.i))
    }
  }

  continue() {
    if (this.updateCount > 0) {
      this.updateCount--
      return setTimeout(() => {
        return this.sendUpdate()
      }, this.options.updateDelay * (0.5 + Math.random()))
    } else {
      return this.updateCallback()
    }
  }

  runForNUpdates(n, callback) {
    if (callback == null) {
      callback = function (error) {}
    }
    this.updateCallback = callback
    this.updateCount = n
    return this.continue()
  }

  check(callback) {
    if (callback == null) {
      callback = function (error) {}
    }
    return DocUpdaterClient.getDoc(
      this.project_id,
      this.doc_id,
      (error, res, body) => {
        if (error != null) {
          throw error
        }
        if (body.lines == null) {
          return console.error(
            `[${new Date()}] \t[${this.client_id.slice(
              0,
              4
            )}] ERROR: Invalid response from get doc (${doc_id})`,
            body
          )
        }
        const content = body.lines.join('\n')
        const { version } = body
        if (content !== this.content) {
          if (version === this.version) {
            console.error(
              `[${new Date()}] \t[${this.client_id.slice(
                0,
                4
              )}] Error: Client content does not match server.`
            )
            console.error(`Server: ${content.split('a')}`)
            console.error(`Client: ${this.content.split('a')}`)
          } else {
            console.error(
              `[${new Date()}] \t[${this.client_id.slice(
                0,
                4
              )}] Error: Version mismatch (Server: '${version}', Client: '${
                this.version
              }')`
            )
          }
        }

        if (!this.isContentValid(this.content)) {
          const iterable = this.content.split('')
          for (let i = 0; i < iterable.length; i++) {
            const chunk = iterable[i]
            if (chunk != null && chunk !== 'a') {
              console.log(chunk, i)
            }
          }
          throw new Error('bad content')
        }
        return callback()
      }
    )
  }

  isChunkValid(chunk) {
    const char = 0
    for (let i = 0; i < chunk.length; i++) {
      const letter = chunk[i]
      if (letter.charCodeAt(0) !== 65 + (i % 26)) {
        console.error(
          `[${new Date()}] \t[${this.client_id.slice(0, 4)}] Invalid Chunk:`,
          chunk
        )
        return false
      }
    }
    return true
  }

  isContentValid(content) {
    for (const chunk of Array.from(content.split('a'))) {
      if (chunk != null && chunk !== '') {
        if (!this.isChunkValid(chunk)) {
          console.error(
            `[${new Date()}] \t[${this.client_id.slice(0, 4)}] Invalid content`,
            content
          )
          return false
        }
      }
    }
    return true
  }
}

const checkDocument = function (project_id, doc_id, clients, callback) {
  if (callback == null) {
    callback = function (error) {}
  }
  const jobs = clients.map((client) => (cb) => client.check(cb))
  return async.parallel(jobs, callback)
}

const printSummary = function (doc_id, clients) {
  const slot = require('cluster-key-slot')
  const now = new Date()
  console.log(
    `[${now}] [${doc_id.slice(0, 4)} (slot: ${slot(doc_id)})] ${
      clients.length
    } clients...`
  )
  return (() => {
    const result = []
    for (const client of Array.from(clients)) {
      console.log(
        `[${now}] \t[${client.client_id.slice(0, 4)}] { local: ${
          client.counts.local_updates
        }, remote: ${client.counts.remote_updates}, conflicts: ${
          client.counts.conflicts
        }, max_delay: ${client.counts.max_delay} }`
      )
      result.push(
        (client.counts = {
          local_updates: 0,
          remote_updates: 0,
          conflicts: 0,
          max_delay: 0
        })
      )
    }
    return result
  })()
}

const CLIENT_COUNT = parseInt(process.argv[2], 10)
const UPDATE_DELAY = parseInt(process.argv[3], 10)
const SAMPLE_INTERVAL = parseInt(process.argv[4], 10)

for (const doc_and_project_id of Array.from(process.argv.slice(5))) {
  ;(function (doc_and_project_id) {
    const [project_id, doc_id] = Array.from(doc_and_project_id.split(':'))
    console.log({ project_id, doc_id })
    return DocUpdaterClient.setDocLines(
      project_id,
      doc_id,
      [new Array(CLIENT_COUNT + 2).join('a')],
      null,
      null,
      (error) => {
        if (error != null) {
          throw error
        }
        return DocUpdaterClient.getDoc(
          project_id,
          doc_id,
          (error, res, body) => {
            let runBatch
            if (error != null) {
              throw error
            }
            if (body.lines == null) {
              return console.error(
                `[${new Date()}] ERROR: Invalid response from get doc (${doc_id})`,
                body
              )
            }
            const content = body.lines.join('\n')
            const { version } = body

            const clients = []
            for (
              let pos = 1, end = CLIENT_COUNT, asc = end >= 1;
              asc ? pos <= end : pos >= end;
              asc ? pos++ : pos--
            ) {
              ;(function (pos) {
                const client = new StressTestClient({
                  doc_id,
                  project_id,
                  content,
                  pos,
                  version,
                  updateDelay: UPDATE_DELAY
                })
                return clients.push(client)
              })(pos)
            }

            return (runBatch = function () {
              const jobs = clients.map((client) => (cb) =>
                client.runForNUpdates(SAMPLE_INTERVAL / UPDATE_DELAY, cb)
              )
              return async.parallel(jobs, (error) => {
                if (error != null) {
                  throw error
                }
                printSummary(doc_id, clients)
                return checkDocument(project_id, doc_id, clients, (error) => {
                  if (error != null) {
                    throw error
                  }
                  return runBatch()
                })
              })
            })()
          }
        )
      }
    )
  })(doc_and_project_id)
}
