
import {
  findIndexSS,
  getState,
  splitItem,
  iterateStructs,
  UpdateEncoderV2,
  DSDecoderV1, DSEncoderV1, DSDecoderV2, DSEncoderV2, Item, GC, StructStore, Transaction, ID, // eslint-disable-line
  DeleteSignature,
  getAllAtomicItemHash,
  getFirstItemIndex,
  UpdateEncoderV1,
  masterFindIndexSS
} from '../internals.js'

import * as array from 'lib0/array'
import * as math from 'lib0/math'
import * as map from 'lib0/map'
import * as encoding from 'lib0/encoding'
import * as decoding from 'lib0/decoding'

export class DeleteItem {
  /**
   * @param {number} clock
   * @param {number} len
   * @param {Array<DeleteSignature | null> | null} signature
   * @param {Array<string> | null} hashs
   */
  constructor (clock, len, signature = null, hashs = null) {
    /**
     * @type {number}
     */
    this.clock = clock
    /**
     * @type {number}
     */
    this.len = len
    /**
     * @type {Array<DeleteSignature | null> | null}
     */
    this.signature = signature

    /**
     * 删除的item的hash列表
     * @type {Array<string> | null}
     */
    this.hashs = hashs
  }
}

/**
 * We no longer maintain a DeleteStore. DeleteSet is a temporary object that is created when needed.
 * - When created in a transaction, it must only be accessed after sorting, and merging
 *   - This DeleteSet is send to other clients
 * - We do not create a DeleteSet when we send a sync message. The DeleteSet message is created directly from StructStore
 * - We read a DeleteSet as part of a sync/update message. In this case the DeleteSet is already sorted and merged.
 */
export class DeleteSet {
  constructor () {
    /**
     * @type {Map<number,Array<DeleteItem>>}
     */
    this.clients = new Map()
  }
}

/**
 * Iterate over all structs that the DeleteSet gc's.
 *
 * @param {Transaction} transaction
 * @param {DeleteSet} ds
 * @param {function(GC|Item):void} f
 *
 * @function
 */
export const iterateDeletedStructs = (transaction, ds, f) =>
  ds.clients.forEach((deletes, clientid) => {
    const structs = /** @type {Array<GC|Item>} */ (transaction.doc.store.clients.get(clientid))
    for (let i = 0; i < deletes.length; i++) {
      const del = deletes[i]
      iterateStructs(transaction, structs, del.clock, del.len, f)
    }
  })

/**
 * @param {Array<DeleteItem>} dis
 * @param {number} clock
 * @return {number|null}
 *
 * @private
 * @function
 */
export const findIndexDS = (dis, clock) => {
  let left = 0
  let right = dis.length - 1
  while (left <= right) {
    const midindex = math.floor((left + right) / 2)
    const mid = dis[midindex]
    const midclock = mid.clock
    if (midclock <= clock) {
      if (clock < midclock + mid.len) {
        return getFirstItemIndex(dis, midindex)
      }
      left = midindex + 1
    } else {
      right = midindex - 1
    }
  }
  return null
}

/**
 * @param {Array<DeleteItem>} dis
 * @param {number} clock
 * @param {string} hash
 * @return {number|null}
 *
 * @private
 * @function
 */
export const masterFindIndexDS = (dis, clock, hash) => {
  let index = findIndexDS(dis, clock) || -1
  while(index < dis.length && index >= 0 &&
    dis[index].clock <= clock &&
    clock < dis[index].clock + dis[index].len
  ){
    const hashs = dis[index].hashs
    if(!hashs){ 
      index ++
      continue
    }
    const currentHash = hashs[clock - dis[index].clock]
    if(currentHash === hash){
      return index
    } else {
      index ++
    }
  }
  return null
}

/**
 * @param {DeleteSet} ds
 * @param {ID} id
 * @return {boolean}
 *
 * @private
 * @function
 */
export const isDeleted = (ds, id) => {
  const dis = ds.clients.get(id.client)
  return dis !== undefined && masterFindIndexDS(dis, id.clock, id.hash) !== null
}

/**
 * @param {DeleteSet} ds
 *
 * @private
 * @function
 */
export const sortAndMergeDeleteSet = ds => {
  ds.clients.forEach(dels => {
    dels.sort((a, b) => a.clock - b.clock)
    // merge items without filtering or splicing the array
    // i is the current pointer
    // j refers to the current insert position for the pointed item
    // try to merge dels[i] into dels[j-1] or set dels[j]=dels[i]
    // let i, j
    // for (i = 1, j = 1; i < dels.length; i++) {
    //   const left = dels[j - 1]
    //   const right = dels[i]
    //   if (left.clock + left.len >= right.clock && 
    //     ((left.signature && right.signature) 
    //     || (!left.signature && !right.signature)) &&
    //     left.hashs && right.hashs
    //     ) {
    //     left.len = math.max(left.len, right.clock + right.len - left.clock)
    //     if(left.signature && right.signature){
    //       left.signature = [ ...left.signature?.splice(0, right.clock - left.clock), ...right.signature ]
    //     }
    //   } else {
    //     if (j < i) {
    //       dels[j] = right
    //     }
    //     j++
    //   }
    // }
    // dels.length = j
  })
}

/**
 * @param {Array<DeleteSet>} dss
 * @return {DeleteSet} A fresh DeleteSet
 */
export const mergeDeleteSets = dss => {
  const merged = new DeleteSet()
  for (let dssI = 0; dssI < dss.length; dssI++) {
    dss[dssI].clients.forEach((delsLeft, client) => {
      if (!merged.clients.has(client)) {
        // Write all missing keys from current ds and all following.
        // If merged already contains `client` current ds has already been added.
        /**
         * @type {Array<DeleteItem>}
         */
        const dels = delsLeft.slice()
        for (let i = dssI + 1; i < dss.length; i++) {
          array.appendTo(dels, dss[i].clients.get(client) || [])
        }
        merged.clients.set(client, dels)
      }
    })
  }
  sortAndMergeDeleteSet(merged)
  return merged
}

/**
 * @param {DeleteSet} ds
 * @param {number} client
 * @param {number} clock
 * @param {number} length
 * @param {Array<DeleteSignature | null> | null} signature
 * @param {Array<string> | null} hashs
 *
 * @private
 * @function
 */
export const addToDeleteSet = (ds, client, clock, length, signature = null, hashs = null) => {
  // @ts-ignore
  map.setIfUndefined(ds.clients, client, () => []).push(new DeleteItem(clock, length, signature, hashs))
}

/**
 * 从deleteset中剔除一个item
 * @param {DeleteSet} ds
 * @param {number} client
 * @param {number} clock
 * @param {number} length
 * @param {Array<DeleteSignature | null> | null} signature
 *
 * @private
 * @function
 */
export const removeFromDeleteSet = (ds, client, clock, length, signature = null) => {
  // @ts-ignore
  const clientDsList = map.setIfUndefined(ds.clients, client, () => [])
  const index = clientDsList.findIndex(deleteItem => deleteItem.clock === clock && deleteItem.len === length && deleteItem.signature === signature)
  if(index > -1){
    clientDsList.splice(index, 1)
  }
}


export const createDeleteSet = () => new DeleteSet()

/**
 * @param {StructStore} ss
 * @return {DeleteSet} Merged and sorted DeleteSet
 *
 * @private
 * @function
 */
export const createDeleteSetFromStructStore = ss => {
  const ds = createDeleteSet()
  ss.clients.forEach((structs, client) => {
    /**
     * @type {Array<DeleteItem>}
     */
    const dsitems = []
    for (let i = 0; i < structs.length; i++) {
      /**
       * @type {GC | Item | null | undefined}
       */
      let struct = structs[i]

      if (struct.deleted) {
        const clock = struct.id.clock
        let len = struct.length
        // /**
        //  * @type {Array<DeleteSignature | null>}
        //  */
        // let deleteSignatureList = struct.deleteSignature ? [ ...struct.deleteSignature ] : new Array(struct.length).fill(null)
        // /**
        //  * @type {Array<DeleteSignature | null>}
        //  */
        // if (i + 1 < structs.length) {
        //   for (let next = structs[i + 1]; i + 1 < structs.length && next.deleted; next = structs[++i + 1]) {
        //     len += next.length
        //     deleteSignatureList = deleteSignatureList.concat(next.deleteSignature ? [ ...next.deleteSignature ] : new Array(next.length).fill(null))
        //   }
        // }
        dsitems.push(new DeleteItem(clock, len, struct.deleteSignature, getAllAtomicItemHash(struct)))
      }
    }
    if (dsitems.length > 0) {
      ds.clients.set(client, dsitems)
    }
  })
  return ds
}

/**
 * @param {DSEncoderV1 | DSEncoderV2} encoder
 * @param {DeleteSet} ds
 *
 * @private
 * @function
 */
export const writeDeleteSet = (encoder, ds) => {
  encoding.writeVarUint(encoder.restEncoder, ds.clients.size)
  ds.clients.forEach((dsitems, client) => {
    encoder.resetDsCurVal()
    encoding.writeVarUint(encoder.restEncoder, client)
    const len = dsitems.length
    encoding.writeVarUint(encoder.restEncoder, len)
    for (let i = 0; i < len; i++) {
      const item = dsitems[i]
      encoder.writeDsClock(item.clock)
      encoder.writeDsLen(item.len)
      encoder.writeDsSignature(item.signature)
      encoder.writeDSHashs(item.hashs)
    }
  })
}

/**
 * @param {DSDecoderV1 | DSDecoderV2} decoder
 * @return {DeleteSet}
 *
 * @private
 * @function
 */
export const readDeleteSet = decoder => {
  const ds = new DeleteSet()
  const numClients = decoding.readVarUint(decoder.restDecoder)
  for (let i = 0; i < numClients; i++) {
    decoder.resetDsCurVal()
    const client = decoding.readVarUint(decoder.restDecoder)
    const numberOfDeletes = decoding.readVarUint(decoder.restDecoder)
    if (numberOfDeletes > 0) {
      // @ts-ignore
      const dsField = map.setIfUndefined(ds.clients, client, () => [])
      for (let i = 0; i < numberOfDeletes; i++) {
        dsField.push(new DeleteItem(decoder.readDsClock(), decoder.readDsLen(), decoder.readDsSignature(), decoder.readDSHashs()))
      }
    }
  }
  return ds
}

/**
 * @todo YDecoder also contains references to String and other Decoders. Would make sense to exchange YDecoder.toUint8Array for YDecoder.DsToUint8Array()..
 */

/**
 * @param {DSDecoderV1 | DSDecoderV2} decoder
 * @param {Transaction} transaction
 * @param {StructStore} store
 * @return {Uint8Array|null} Returns a v2 update containing all deletes that couldn't be applied yet; or null if all deletes were applied successfully.
 *
 * @private
 * @function
 */
export const readAndApplyDeleteSet = (decoder, transaction, store) => {
  const unappliedDS = new DeleteSet()
  const numClients = decoding.readVarUint(decoder.restDecoder)
  for (let i = 0; i < numClients; i++) {
    decoder.resetDsCurVal()
    const client = decoding.readVarUint(decoder.restDecoder)
    const numberOfDeletes = decoding.readVarUint(decoder.restDecoder)
    const structs = store.clients.get(client) || []
    const state = getState(store, client)
    for (let i = 0; i < numberOfDeletes; i++) {
      const clock = decoder.readDsClock()
      let len = decoder.readDsLen()
      let clockEnd = clock + len
      const signature = decoder.readDsSignature()
      const hashs = decoder.readDSHashs() ?? []
      if (clock < state) {
        if (state < clockEnd) {
          addToDeleteSet(unappliedDS, client, state, clockEnd - state, 
            signature?.splice(state - clock),
            hashs?.splice(state - clock))
          len = state - clock
          clockEnd = state
        }

        for(let currentClock = clock ; currentClock < clockEnd ; currentClock ++){
          const clientByzantineStore = transaction.doc.byzantineStore.clients.get(client)
          if(clientByzantineStore){
            const merkleTreeLeafNodes = clientByzantineStore.leafNodeList
            if(currentClock < merkleTreeLeafNodes.arr.length){
              const merkleTreeLeafNode = merkleTreeLeafNodes.arr[currentClock]
              const targets = merkleTreeLeafNode.targets
              const offset = currentClock - clock
              const remoteHash = hashs[offset]
              const targetIndex = targets.findIndex(target => target.hash === remoteHash)
              if(targetIndex > -1 && targets[targetIndex].struct.isIntegrated){
                const struct = targets[targetIndex].struct
                // 获取该struct的位置
                const index = masterFindIndexSS(structs, struct.id.clock, struct.id.hash)
                const offsetToStruct = currentClock - struct.id.clock
                if(offsetToStruct === 0){
                  if(struct.length > 1){
                    const rightItem = splitItem(transaction, struct, offsetToStruct + 1)
                    structs.splice(index + 1, 0, rightItem)
                  }
                  struct.delete(
                    transaction,
                    !signature ? null : signature.slice(offset, offset + 1)
                  )
                } else if(offsetToStruct === struct.length - 1){
                  const rightItem = splitItem(transaction, struct, offsetToStruct)
                  structs.splice(index + 1, 0, rightItem)
                  rightItem.delete(
                    transaction, 
                    !signature ? null : signature.slice(offset, offset + 1)
                  )
                } else {
                  const rightItem = splitItem(transaction, struct, offsetToStruct)
                  structs.splice(index + 1, 0, rightItem)
                  structs.splice(index + 2, 0, splitItem(transaction, rightItem, 1))
                  rightItem.delete(
                    transaction, 
                    !signature ? null : signature.slice(offset, offset + 1)
                  )
                }
              } else {
                addToDeleteSet(unappliedDS, client, currentClock, 1, 
                  signature?.slice(offset, offset + 1),
                  hashs?.slice(offset, offset + 1))
              }
            }
          }
        }
      } else {
        addToDeleteSet(
          unappliedDS, 
          client, 
          clock, 
          len,
          signature,
          hashs
        )
      }
    }
  }
  if (unappliedDS.clients.size > 0) {
    const ds = new UpdateEncoderV1()
    encoding.writeVarUint(ds.restEncoder, 0) // encode 0 structs
    writeDeleteSet(ds, unappliedDS)
    return ds.toUint8Array()
  }
  return null
}
