
import _Y from '../src/Y.dist.js'
import { DomBinding } from '../src/Y.js'
import TestConnector from './test-connector.js'

import Chance from 'chance'
import ItemJSON from '../src/Struct/ItemJSON.js'
import ItemString from '../src/Struct/ItemString.js'
import { defragmentItemContent } from '../src/Util/defragmentItemContent.js'
import Quill from 'quill'
import GC from '../src/Struct/GC.js'

export const Y = _Y

export const database = { name: 'memory' }
export const connector = { name: 'test', url: 'http://localhost:1234' }

Y.test = TestConnector

function getStateSet (y) {
  let ss = {}
  for (let [user, clock] of y.ss.state) {
    ss[user] = clock
  }
  return ss
}

function getDeleteSet (y) {
  var ds = {}
  y.ds.iterate(null, null, function (n) {
    var user = n._id.user
    var counter = n._id.clock
    var len = n.len
    var gc = n.gc
    var dv = ds[user]
    if (dv === void 0) {
      dv = []
      ds[user] = dv
    }
    dv.push([counter, len, gc])
  })
  return ds
}

/*
 * 1. reconnect and flush all
 * 2. user 0 gc
 * 3. get type content
 * 4. disconnect & reconnect all (so gc is propagated)
 * 5. compare os, ds, ss
 */
export async function compareUsers (t, users) {
  await Promise.all(users.map(u => u.reconnect()))
  if (users[0].connector.testRoom == null) {
    await wait(100)
  }
  await flushAll(t, users)
  await wait()
  await flushAll(t, users)
  await wait()
  await flushAll(t, users)
  await wait()
  await flushAll(t, users)

  var userArrayValues = users.map(u => u.define('array', Y.Array).toJSON().map(val => JSON.stringify(val)))
  var userMapValues = users.map(u => u.define('map', Y.Map).toJSON())
  var userXmlValues = users.map(u => u.define('xml', Y.Xml).toString())
  var userTextValues = users.map(u => u.define('text', Y.Text).toDelta())
  var userQuillValues = users.map(u => {
    u.quill.update('yjs') // get latest changes
    return u.quill.getContents().ops
  })

  var data = users.map(u => {
    defragmentItemContent(u)
    var data = {}
    let ops = []
    u.os.iterate(null, null, function (op) {
      let json
      if (op.constructor === GC) {
        json = {
          type: 'GC',
          id: op._id,
          length: op._length
        }
      } else {
        json = {
          id: op._id,
          left: op._left === null ? null : op._left._lastId,
          right: op._right === null ? null : op._right._id,
          length: op._length,
          deleted: op._deleted,
          parent: op._parent._id
        }
      }
      if (op instanceof ItemJSON || op instanceof ItemString) {
        json.content = op._content
      }
      ops.push(json)
    })
    data.os = ops
    data.ds = getDeleteSet(u)
    data.ss = getStateSet(u)
    return data
  })
  for (var i = 0; i < data.length - 1; i++) {
    await t.asyncGroup(async () => {
      t.compare(userArrayValues[i], userArrayValues[i + 1], 'array types')
      t.compare(userMapValues[i], userMapValues[i + 1], 'map types')
      t.compare(userXmlValues[i], userXmlValues[i + 1], 'xml types')
      t.compare(userTextValues[i], userTextValues[i + 1], 'text types')
      t.compare(userQuillValues[i], userQuillValues[i + 1], 'quill delta content')
      t.compare(data[i].os, data[i + 1].os, 'os')
      t.compare(data[i].ds, data[i + 1].ds, 'ds')
      t.compare(data[i].ss, data[i + 1].ss, 'ss')
    }, `Compare user${i} with user${i + 1}`)
  }
  users.map(u => u.destroy())
}

function domFilter (nodeName, attrs) {
  if (nodeName === 'HIDDEN') {
    return null
  }
  attrs.delete('hidden')
  return attrs
}

export async function initArrays (t, opts) {
  var result = {
    users: []
  }
  var chance = opts.chance || new Chance(t.getSeed() * 1000000000)
  var conn = Object.assign({ room: 'debugging_' + t.name, testContext: t, chance }, connector)
  for (let i = 0; i < opts.users; i++) {
    let connOpts
    if (i === 0) {
      connOpts = Object.assign({ role: 'master' }, conn)
    } else {
      connOpts = Object.assign({ role: 'slave' }, conn)
    }
    let y = new Y(connOpts.room, {
      userID: i, // evil hackery, don't try this at home
      connector: connOpts
    })
    result.users.push(y)
    result['array' + i] = y.define('array', Y.Array)
    result['map' + i] = y.define('map', Y.Map)
    const yxml = y.define('xml', Y.XmlElement)
    result['xml' + i] = yxml
    const dom = document.createElement('my-dom')
    const domBinding = new DomBinding(yxml, dom, { domFilter })
    result['domBinding' + i] = domBinding
    result['dom' + i] = dom
    const textType = y.define('text', Y.Text)
    result['text' + i] = textType
    const quill = new Quill(document.createElement('div'))
    result['quillBinding' + i] = new Y.QuillBinding(textType, quill)
    result['quill' + i] = quill
    y.quill = quill // put quill on the y object (so we can use it later)
    y.dom = dom
    y.on('afterTransaction', function () {
      for (let missing of y._missingStructs.values()) {
        if (missing.size > 0) {
          console.error(new Error('Test check in "afterTransaction": missing should be empty!'))
        }
      }
    })
  }
  result.array0.delete(0, result.array0.length)
  if (result.users[0].connector.testRoom != null) {
    // flush for sync if test-connector
    await result.users[0].connector.testRoom.flushAll(result.users)
  }
  await Promise.all(result.users.map(u => {
    return new Promise(function (resolve) {
      u.connector.whenSynced(resolve)
    })
  }))
  await flushAll(t, result.users)
  return result
}

export async function flushAll (t, users) {
  // users = users.filter(u => u.connector.isSynced)
  if (users.length === 0) {
    return
  }
  await wait(10)
  if (users[0].connector.testRoom != null) {
    // use flushAll method specified in Test Connector
    await users[0].connector.testRoom.flushAll(users)
  } else {
    var flushCounter = users[0].get('flushHelper', Y.Map).get('0') || 0
    flushCounter++
    await Promise.all(users.map(async (u, i) => {
      // wait for all users to set the flush counter to the same value
      await new Promise(resolve => {
        function observer () {
          var allUsersReceivedUpdate = true
          for (var i = 0; i < users.length; i++) {
            if (u.get('flushHelper', Y.Map).get(i + '') !== flushCounter) {
              allUsersReceivedUpdate = false
              break
            }
          }
          if (allUsersReceivedUpdate) {
            resolve()
          }
        }
        u.get('flushHelper', Y.Map).observe(observer)
        u.get('flushHelper').set(i + '', flushCounter)
      })
    }))
  }
}

export async function flushSome (t, users) {
  if (users[0].connector.testRoom == null) {
    // if not test-connector, wait for some time for operations to arrive
    await wait(100)
  }
}

export function wait (t) {
  return new Promise(function (resolve) {
    setTimeout(resolve, t != null ? t : 100)
  })
}

export async function applyRandomTests (t, mods, iterations) {
  const chance = new Chance(t.getSeed() * 1000000000)
  var initInformation = await initArrays(t, { users: 5, chance: chance })
  let { users } = initInformation
  for (var i = 0; i < iterations; i++) {
    if (chance.bool({likelihood: 10})) {
      // 10% chance to disconnect/reconnect a user
      // we make sure that the first users always is connected
      let user = chance.pickone(users.slice(1))
      if (user.connector.isSynced) {
        if (users.filter(u => u.connector.isSynced).length > 1) {
          // make sure that at least one user remains in the room
          await user.disconnect()
          if (users[0].connector.testRoom == null) {
            await wait(100)
          }
        }
      } else {
        await user.reconnect()
        if (users[0].connector.testRoom == null) {
          await wait(100)
        }
        await new Promise(function (resolve) {
          user.connector.whenSynced(resolve)
        })
      }
    } else if (chance.bool({likelihood: 5})) {
      // 20%*!prev chance to flush all & garbagecollect
      // TODO: We do not gc all users as this does not work yet
      // await garbageCollectUsers(t, users)
      await flushAll(t, users)
      // await users[0].db.emptyGarbageCollector()
      await flushAll(t, users)
    } else if (chance.bool({likelihood: 10})) {
      // 20%*!prev chance to flush some operations
      await flushSome(t, users)
    }
    let user = chance.pickone(users)
    var test = chance.pickone(mods)
    test(t, user, chance)
  }
  await compareUsers(t, users)
  return initInformation
}
