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

import {
  DeleteSignature,
  ID, IntegrateSignature,
  Signature
} from '../internals.js'

export class DSEncoderV1 {
  constructor () {
    this.restEncoder = encoding.createEncoder()
  }

  toUint8Array () {
    return encoding.toUint8Array(this.restEncoder)
  }

  resetDsCurVal () {
    // nop
  }

  /**
   * @param {number} clock
   */
  writeDsClock (clock) {
    encoding.writeVarUint(this.restEncoder, clock)
  }

  /**
   * @param {number} len
   */
  writeDsLen (len) {
    encoding.writeVarUint(this.restEncoder, len)
  }

  /**
   * @param {Array<DeleteSignature | null> | null} signature 
   */
  writeDsSignature(signature){
    const haveSignature = !!signature
    encoding.writeVarUint(this.restEncoder, haveSignature ? 1 : 0)
    if(haveSignature){
      encoding.writeVarUint(this.restEncoder, signature.length)
      for(let signatureItem of signature){
        encoding.writeVarUint(this.restEncoder, signatureItem ? 1 : 0)
        if(signatureItem){
          signatureItem.toEncoder(this.restEncoder)
        }
      }
    }
  }

  /**
   * @param {Array<string> | null} hashs
   */
  writeDSHashs(hashs){
    const haveHashs = !!hashs
    encoding.writeVarUint(this.restEncoder, haveHashs ? 1 : 0)
    if(haveHashs){
      encoding.writeVarUint(this.restEncoder, hashs.length)
      for(let hash of hashs){
          encoding.writeVarString(this.restEncoder, hash)
      }
    }
  }
}

export class UpdateEncoderV1 extends DSEncoderV1 {
  /**
   * 写入该item的clock
   * @param {ID} id
   */
  writeClock(id){
    encoding.writeVarUint(this.restEncoder, id.clock)
  }

  /**
   * @param {ID} id
   */
  writeLeftID (id) {
    encoding.writeVarUint(this.restEncoder, id.client)
    encoding.writeVarUint(this.restEncoder, id.clock)
    encoding.writeVarString(this.restEncoder, id.hash)
  }

  /**
   * @param {ID} id
   */
  writeRightID (id) {
    encoding.writeVarUint(this.restEncoder, id.client)
    encoding.writeVarUint(this.restEncoder, id.clock)
    encoding.writeVarString(this.restEncoder, id.hash)
  }

  /**
   * Use writeClient and writeClock instead of writeID if possible.
   * @param {number} client
   */
  writeClient (client) {
    encoding.writeVarUint(this.restEncoder, client)
  }

  /**
   * @param {number} info An unsigned 8-bit integer
   */
  writeInfo (info) {
    encoding.writeUint8(this.restEncoder, info)
  }

  /**
   * @param {string} s
   */
  writeString (s) {
    encoding.writeVarString(this.restEncoder, s)
  }

  /**
   * @param {boolean} isYKey
   */
  writeParentInfo (isYKey) {
    encoding.writeVarUint(this.restEncoder, isYKey ? 1 : 0)
  }

  /**
   * @param {number} info An unsigned 8-bit integer
   */
  writeTypeRef (info) {
    encoding.writeVarUint(this.restEncoder, info)
  }

  /**
   * @param {boolean} haveSignature An unsigned 8-bit integer
   */
  writeHaveSignature(haveSignature){
    encoding.writeVarUint(this.restEncoder, haveSignature ? 1 : 0)
  }

  /**
   * @param {boolean} haveSignature An unsigned 8-bit integer
   */
  writeHaveIntegrateSignature(haveSignature){
    encoding.writeVarUint(this.restEncoder, haveSignature ? 1 : 0)
  }

  /**
   * @param {Array<Signature> | null} signature An unsigned 8-bit integer
   * @param {number} offset
   */
  writeSignature(signature, offset){
    if(signature){
      const len = signature.length - offset
      encoding.writeVarUint(this.restEncoder, len)
      for(let i = 0 ; i < len; i ++){
        signature[offset + i].toEncoder(this.restEncoder)
      }
    }
  }

  /**
   * @param {IntegrateSignature | null} signature An unsigned 8-bit integer
   */
  writeIntegrateSignature(signature){
    if(signature){
      signature.toEncoder(this.restEncoder)
    }
  }

  /**
   * Write len of a struct - well suited for Opt RLE encoder.
   *
   * @param {number} len
   */
  writeLen (len) {
    encoding.writeVarUint(this.restEncoder, len)
  }

  /**
   * @param {any} any
   */
  writeAny (any) {
    encoding.writeAny(this.restEncoder, any)
  }

  /**
   * @param {Uint8Array} buf
   */
  writeBuf (buf) {
    encoding.writeVarUint8Array(this.restEncoder, buf)
  }

  /**
   * @param {any} embed
   */
  writeJSON (embed) {
    encoding.writeVarString(this.restEncoder, JSON.stringify(embed))
  }

  /**
   * @param {string} key
   */
  writeKey (key) {
    encoding.writeVarString(this.restEncoder, key)
  }
}

export class DSEncoderV2 {
  constructor () {
    this.restEncoder = encoding.createEncoder() // encodes all the rest / non-optimized
    this.dsCurrVal = 0
  }

  toUint8Array () {
    return encoding.toUint8Array(this.restEncoder)
  }

  resetDsCurVal () {
    this.dsCurrVal = 0
  }

  /**
   * @param {number} clock
   */
  writeDsClock (clock) {
    const diff = clock - this.dsCurrVal
    this.dsCurrVal = clock
    encoding.writeVarUint(this.restEncoder, diff)
  }

  /**
   * @param {number} len
   */
  writeDsLen (len) {
    if (len === 0) {
      error.unexpectedCase()
    }
    encoding.writeVarUint(this.restEncoder, len - 1)
    this.dsCurrVal += len
  }

  /**
   * @param {Array<DeleteSignature | null> | null} signature 
   */
  writeDsSignature(signature){
    const haveSignature = !!signature
    encoding.writeVarUint(this.restEncoder, haveSignature ? 1 : 0)
    if(haveSignature){
      encoding.writeVarUint(this.restEncoder, signature.length)
      for(let signatureItem of signature){
        encoding.writeVarUint(this.restEncoder, signatureItem ? 1 : 0)
        if(signatureItem){
          signatureItem.toEncoder(this.restEncoder)
        }
      }
    }
  }

  /**
   * @param {Array<string> | null} hashs
   */
  writeDSHashs(hashs){
    const haveHashs = !!hashs
    encoding.writeVarUint(this.restEncoder, haveHashs ? 1 : 0)
    if(haveHashs){
      encoding.writeVarUint(this.restEncoder, hashs.length)
      for(let hash of hashs){
          encoding.writeVarString(this.restEncoder, hash)
      }
    }
  }
}

export class UpdateEncoderV2 extends DSEncoderV2 {
  constructor () {
    super()
    /**
     * @type {Map<string,number>}
     */
    this.keyMap = new Map()
    /**
     * Refers to the next uniqe key-identifier to me used.
     * See writeKey method for more information.
     *
     * @type {number}
     */
    this.keyClock = 0
    this.keyClockEncoder = new encoding.IntDiffOptRleEncoder()
    this.clientEncoder = new encoding.UintOptRleEncoder()

    this.clockEncoder = new encoding.IntDiffOptRleEncoder()

    this.leftClockEncoder = new encoding.IntDiffOptRleEncoder()
    this.leftHashEncoder = new encoding.StringEncoder()

    this.rightClockEncoder = new encoding.IntDiffOptRleEncoder()
    this.rightHashEncoder = new encoding.StringEncoder()

    this.infoEncoder = new encoding.RleEncoder(encoding.writeUint8)
    this.stringEncoder = new encoding.StringEncoder()
    this.parentInfoEncoder = new encoding.RleEncoder(encoding.writeUint8)
    this.typeRefEncoder = new encoding.UintOptRleEncoder()
    this.lenEncoder = new encoding.UintOptRleEncoder()
    
    this.haveSignatureEncoder = new encoding.RleEncoder(encoding.writeUint8)
    this.signatureLenEncoder = new encoding.UintOptRleEncoder()
    this.signatureEncoder = encoding.createEncoder()

    this.haveIntegrateSignatureEncoder = new encoding.RleEncoder(encoding.writeUint8)
    this.integrateSignatureEncoder = encoding.createEncoder()
  }

  toUint8Array () {
    const encoder = encoding.createEncoder()
    encoding.writeVarUint(encoder, 0) // this is a feature flag that we might use in the future
    encoding.writeVarUint8Array(encoder, this.keyClockEncoder.toUint8Array())
    encoding.writeVarUint8Array(encoder, this.clientEncoder.toUint8Array())

    encoding.writeVarUint8Array(encoder, this.clockEncoder.toUint8Array())

    encoding.writeVarUint8Array(encoder, this.leftClockEncoder.toUint8Array())
    encoding.writeVarUint8Array(encoder, this.leftHashEncoder.toUint8Array())

    encoding.writeVarUint8Array(encoder, this.rightClockEncoder.toUint8Array())
    encoding.writeVarUint8Array(encoder, this.rightHashEncoder.toUint8Array())

    encoding.writeVarUint8Array(encoder, encoding.toUint8Array(this.infoEncoder))
    encoding.writeVarUint8Array(encoder, this.stringEncoder.toUint8Array())
    encoding.writeVarUint8Array(encoder, encoding.toUint8Array(this.parentInfoEncoder))
    encoding.writeVarUint8Array(encoder, this.typeRefEncoder.toUint8Array())
    encoding.writeVarUint8Array(encoder, this.lenEncoder.toUint8Array())

    encoding.writeVarUint8Array(encoder, encoding.toUint8Array(this.haveSignatureEncoder))
    encoding.writeVarUint8Array(encoder, this.signatureLenEncoder.toUint8Array())
    encoding.writeVarUint8Array(encoder, encoding.toUint8Array(this.signatureEncoder))

    encoding.writeVarUint8Array(encoder, encoding.toUint8Array(this.haveIntegrateSignatureEncoder))
    encoding.writeVarUint8Array(encoder, encoding.toUint8Array(this.integrateSignatureEncoder))

    // @note The rest encoder is appended! (note the missing var)
    encoding.writeUint8Array(encoder, encoding.toUint8Array(this.restEncoder))
    return encoding.toUint8Array(encoder)
  }

  /**
   * 写入item的clock
   * @param {ID} id
   */
  writeClock(id){
    this.clockEncoder.write(id.clock)
  }

  /**
   * @param {ID} id
   */
  writeLeftID (id) {
    this.clientEncoder.write(id.client)
    this.leftClockEncoder.write(id.clock)
    this.leftHashEncoder.write(id.hash)
  }

  /**
   * @param {ID} id
   */
  writeRightID (id) {
    this.clientEncoder.write(id.client)
    this.rightClockEncoder.write(id.clock)
    this.rightHashEncoder.write(id.hash)
  }

  /**
   * @param {number} client
   */
  writeClient (client) {
    this.clientEncoder.write(client)
  }

  /**
   * @param {number} info An unsigned 8-bit integer
   */
  writeInfo (info) {
    this.infoEncoder.write(info)
  }

  /**
   * @param {string} s
   */
  writeString (s) {
    this.stringEncoder.write(s)
  }

  /**
   * @param {boolean} isYKey
   */
  writeParentInfo (isYKey) {
    this.parentInfoEncoder.write(isYKey ? 1 : 0)
  }

  /**
  * @param {boolean} haveSignature An unsigned 8-bit integer
  */
  writeHaveSignature(haveSignature){
    this.haveSignatureEncoder.write(haveSignature ? 1 : 0)
  }

  /**
   * @param {boolean} haveSignature An unsigned 8-bit integer
   */
  writeHaveIntegrateSignature(haveSignature){
    this.haveIntegrateSignatureEncoder.write(haveSignature ? 1 : 0)
  }

  /**
   * @param {Array<Signature>} signature
   * @param {number} offset
   */
  writeSignature(signature, offset){
    if(signature){
      const len = signature.length - offset
      this.signatureLenEncoder.write(len)
      for(let i = 0 ; i < len; i ++){
        signature[offset + i].toEncoder(this.signatureEncoder)
      }
    }
  }

  /**
   * @param {IntegrateSignature | null} signature An unsigned 8-bit integer
   */
  writeIntegrateSignature(signature){
    if(signature){
      signature.toEncoder(this.integrateSignatureEncoder)
    }
  }

  /**
   * @param {number} info An unsigned 8-bit integer
   */
  writeTypeRef (info) {
    this.typeRefEncoder.write(info)
  }

  /**
   * Write len of a struct - well suited for Opt RLE encoder.
   *
   * @param {number} len
   */
  writeLen (len) {
    this.lenEncoder.write(len)
  }

  /**
   * @param {any} any
   */
  writeAny (any) {
    encoding.writeAny(this.restEncoder, any)
  }

  /**
   * @param {Uint8Array} buf
   */
  writeBuf (buf) {
    encoding.writeVarUint8Array(this.restEncoder, buf)
  }

  /**
   * This is mainly here for legacy purposes.
   *
   * Initial we incoded objects using JSON. Now we use the much faster lib0/any-encoder. This method mainly exists for legacy purposes for the v1 encoder.
   *
   * @param {any} embed
   */
  writeJSON (embed) {
    encoding.writeAny(this.restEncoder, embed)
  }

  /**
   * Property keys are often reused. For example, in y-prosemirror the key `bold` might
   * occur very often. For a 3d application, the key `position` might occur very often.
   *
   * We cache these keys in a Map and refer to them via a unique number.
   *
   * @param {string} key
   */
  writeKey (key) {
    const clock = this.keyMap.get(key)
    if (clock === undefined) {
      /**
       * @todo uncomment to introduce this feature finally
       *
       * Background. The ContentFormat object was always encoded using writeKey, but the decoder used to use readString.
       * Furthermore, I forgot to set the keyclock. So everything was working fine.
       *
       * However, this feature here is basically useless as it is not being used (it actually only consumes extra memory).
       *
       * I don't know yet how to reintroduce this feature..
       *
       * Older clients won't be able to read updates when we reintroduce this feature. So this should probably be done using a flag.
       *
       */
      // this.keyMap.set(key, this.keyClock)
      this.keyClockEncoder.write(this.keyClock++)
      this.stringEncoder.write(key)
    } else {
      this.keyClockEncoder.write(clock)
    }
  }
}
