
import {
  GC,
  splitItem,
  Transaction, ID, Item, // eslint-disable-line
  Doc,
  getAtomicItemHash,
  Skip,
  DeleteItem
} from '../internals.js'

import * as math from 'lib0/math'
import * as error from 'lib0/error'

export class StructStore {
  constructor () {
    /**
     * @type {Map<number,Array<GC|Item>>}
     */
    this.clients = new Map()
    /**
     * @type {null | { missing: Map<number, Map<string, boolean>>, update: Uint8Array }}
     */
    this.pendingStructs = null
    /**
     * @type {null | Uint8Array}
     */
    this.pendingDs = null
  }
}

/**
 * Return the states as a Map<client,clock>.
 * Note that clock refers to the next expected clock id.
 *
 * @param {StructStore} store
 * @return {Map<number,number>}
 *
 * @public
 * @function
 */
export const getStateVector = store => {
  const sm = new Map()
  store.clients.forEach((structs, client) => {
    const struct = structs[structs.length - 1]
    sm.set(client, struct.id.clock + struct.length)
  })
  return sm
}

/**
 * @param {StructStore} store
 * @param {number} client
 * @return {number}
 *
 * @public
 * @function
 */
export const getState = (store, client) => {
  const structs = store.clients.get(client)
  if (structs === undefined) {
    return 0
  }
  const lastStruct = structs[structs.length - 1]
  return lastStruct.id.clock + lastStruct.length
}

/**
 * @param {StructStore} store
 *
 * @private
 * @function
 */
export const integretyCheck = store => {
  store.clients.forEach(structs => {
    for (let i = 1; i < structs.length; i++) {
      const l = structs[i - 1]
      const r = structs[i]
      if (l.id.clock + l.length !== r.id.clock) {
        throw new Error('StructStore failed integrety check')
      }
    }
  })
}

/**
 * @param {StructStore} store
 * @param {GC|Item} struct
 *
 * @private
 * @function
 */
export const addStruct = (store, struct) => {
  let structs = store.clients.get(struct.id.client)
  if (structs === undefined) {
    structs = []
    store.clients.set(struct.id.client, structs)
  } else {
    const lastStruct = structs[structs.length - 1]
    if (lastStruct.id.clock + lastStruct.length !== struct.id.clock) {
      // throw error.unexpectedCase()
    }
  }
  structs.push(struct)
  struct.isIntegrated = true
}

/**
 * 获取具有指定inde的item的clock，的最早item
 * @param {Array<Item|GC|DeleteItem>} structs
 * @param {number} index
 * @return {number}
 *
 * @private
 * @function
 */
export const getFirstItemIndex = (structs, index) => {
  const struct = structs[index]
  /**
   * 
   * @param {Item | GC | DeleteItem} struct 
   * @returns 
   */
  const getClock = (struct) => {
    return struct instanceof DeleteItem ? struct.clock : struct.id.clock
  }
  /**
   * 
   * @param {Item | GC | DeleteItem} struct 
   * @returns 
   */
  const getLen = (struct) => {
    return struct instanceof DeleteItem ? struct.len : struct.length
  }
  const clock = getClock(struct)

  if(index === 0) return index

  let currentIndex = index - 1
  // 如果发现该clock在该item内，就继续往前遍历
  while(currentIndex > -1 && currentIndex < structs.length && clock >= getClock(structs[currentIndex])
    && clock < getClock(structs[currentIndex]) + getLen(structs[currentIndex])){
    currentIndex --
  }
  return currentIndex + 1
}

/**
 * Perform a binary search on a sorted array
 * 该方法会返回具有该clock和hash的item，返回-1表示没找到
 * @param {Array<Item|GC>} structs
 * @param {number} clock
 * @param {string} hash
 * @return {number}
 *
 * @private
 * @function
 */
export const masterFindIndexSS = (structs, clock, hash) => {
  let index = findIndexSS(structs, clock)
  while(index < structs.length && index >= 0 &&
    structs[index].id.clock <= clock &&
    clock < structs[index].id.clock + structs[index].length
  ){
    const currentHash = getAtomicItemHash(structs[index], clock - structs[index].id.clock)
    if(currentHash === hash){
      return index
    } else {
      index ++
    }
  }
  return -1
}

/**
 * Perform a binary search on a sorted array
 * 该方法会返回第一个具有该clock的item
 * @param {Array<Item|GC>} structs
 * @param {number} clock
 * @return {number}
 *
 * @private
 * @function
 */
export const findIndexSS = (structs, clock) => {
  let left = 0
  let right = structs.length - 1
  let mid = structs[right]
  let midclock = mid.id.clock
  if (midclock === clock) {
    return getFirstItemIndex(structs, right)
  }
  // @todo does it even make sense to pivot the search?
  // If a good split misses, it might actually increase the time to find the correct item.
  // Currently, the only advantage is that search with pivoting might find the item on the first try.
  let midindex = math.floor((clock / (midclock + mid.length - 1)) * right) // pivoting the search
  while (left <= right) {
    mid = structs[midindex]
    midclock = mid.id.clock
    if (midclock <= clock) {
      if (clock < midclock + mid.length) {
        return getFirstItemIndex(structs, midindex)
      }
      left = midindex + 1
    } else {
      right = midindex - 1
    }
    midindex = math.floor((left + right) / 2)
  }
  // Always check state before looking for a struct in StructStore
  // Therefore the case of not finding a struct is unexpected
  throw error.unexpectedCase()
}

/**
 * Expects that id is actually in store. This function throws or is an infinite loop otherwise.
 * 找到指定id的名义item，hash值不比较，只返回最早匹配到的
 * @param {StructStore} store
 * @param {ID} id
 * @return {GC|Item|null}
 *
 * @private
 * @function
 */
export const find = (store, id) => {
  /**
   * @type {Array<GC|Item>}
   */
  // @ts-ignore
  const structs = store.clients.get(id.client)
  /**
   * @type {Item | GC | null | undefined}
   */
  return structs[findIndexSS(structs, id.clock)]
}

/**
 * Expects that id is actually in store. This function throws or is an infinite loop otherwise.
 * 找到指定id的精确item，包括hash要一致
 * @param {StructStore} store
 * @param {ID} id
 * @return {GC|Item|null}
 *
 * @private
 * @function
 */
export const masterFind = (store, id) => {
    /**
   * @type {Array<GC|Item>}
   */
  // @ts-ignore
  const structs = store.clients.get(id.client)
  /**
   * @type {number}
   */
  let index = findIndexSS(structs, id.clock)
  while(index < structs.length){
    const struct = structs[index]
    if(struct.id.clock <= id.clock && struct.id.clock + struct.length > id.clock){
      const hash = getAtomicItemHash(struct, id.clock - struct.id.clock)
      if(hash === id.hash){
        return struct
      } else {
        index ++
      }
    } else {
      break
    }
  }
  return null
}

/**
 * Expects that id is actually in store. This function throws or is an infinite loop otherwise.
 * @private
 * @function
 */
export const getItem = /** @type {function(StructStore,ID):Item} */ (find)

/**
 * @param {Transaction | null} transaction
 * @param {Array<Item|GC>} structs
 * @param {string} hash
 * @param {number} clock
 */
export const findIndexCleanStart = (transaction, structs, clock, hash = '') => {
  const index = hash ? masterFindIndexSS(structs, clock, hash) : findIndexSS(structs, clock)
  const struct = structs[index]
  if (struct.id.clock < clock && struct instanceof Item) {
    structs.splice(index + 1, 0, splitItem(transaction, struct, clock - struct.id.clock))
    return index + 1
  }
  return index
}

/**
 * Expects that id is actually in store. This function throws or is an infinite loop otherwise.
 *
 * @param {Transaction | null} transaction
 * @param {ID} id
 * @param {StructStore | null | undefined} store
 * @return {Item}
 *
 * @private
 * @function
 */
export const getItemCleanStart = (transaction, id, store = transaction?.doc.store) => {
  if(!store){
    throw new Error("store doesn't exist")
  }
  const structs = /** @type {Array<Item>} */ (store.clients.get(id.client))
  return structs[findIndexCleanStart(transaction, structs, id.clock, id.hash)]
}

/**
 * Expects that id is actually in store. This function throws or is an infinite loop otherwise.
 *
 * @param {Transaction | null} transaction
 * @param {StructStore} store
 * @param {ID} id
 * @return {Item}
 *
 * @private
 * @function
 */
export const getItemCleanEnd = (transaction, store, id) => {
  /**
   * @type {Array<Item>}
   */
  // @ts-ignore
  const structs = store.clients.get(id.client)
  const index = masterFindIndexSS(structs, id.clock, id.hash)
  const struct = structs[index]
  if (id.clock !== struct.id.clock + struct.length - 1 && struct.constructor !== GC) {
    structs.splice(index + 1, 0, splitItem(transaction, struct, id.clock - struct.id.clock + 1))
  }
  return struct
}

/**
 * Replace `item` with `newitem` in store
 * @param {StructStore} store
 * @param {GC|Item} struct
 * @param {GC|Item} newStruct
 *
 * @private
 * @function
 */
export const replaceStruct = (store, struct, newStruct) => {
  const structs = /** @type {Array<GC|Item>} */ (store.clients.get(struct.id.client))
  structs[findIndexSS(structs, struct.id.clock)] = newStruct
}

/**
 * Iterate over a range of structs
 *
 * @param {Transaction} transaction
 * @param {Array<Item|GC>} structs
 * @param {number} clockStart Inclusive start
 * @param {number} len
 * @param {function(GC|Item):void} f
 *
 * @function
 */
export const iterateStructs = (transaction, structs, clockStart, len, f) => {
  if (len === 0) {
    return
  }
  const clockEnd = clockStart + len
  let index = findIndexCleanStart(transaction, structs, clockStart)
  let struct
  do {
    struct = structs[index++]
    if (clockEnd < struct.id.clock + struct.length) {
      findIndexCleanStart(transaction, structs, clockEnd)
    }
    f(struct)
  } while (index < structs.length && structs[index].id.clock < clockEnd)
}
