/* eslint-disable no-var */
// @ts-nocheck

/*
 * Copyright (c) 2019 Genome Research Ltd.
 * Author(s): James Bonfield
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 *
 *    2. Redistributions in binary form must reproduce the above
 *       copyright notice, this list of conditions and the following
 *       disclaimer in the documentation and/or other materials provided
 *       with the distribution.
 *
 *    3. Neither the names Genome Research Ltd and Wellcome Trust Sanger
 *       Institute nor the names of its contributors may be used to endorse
 *       or promote products derived from this software without specific
 *       prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY GENOME RESEARCH LTD AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GENOME RESEARCH
 * LTD OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

import RangeCoder from './arith_sh'
import ByteModel from './byte_model'
import IOStream from './iostream'

// ----------------------------------------------------------------------
// Main arithmetic entry function: decodes a compressed src and
// returns the uncompressed buffer.

function read_array(src, tab, size) {
  let j = 0 // array value
  let z = 0 // array index: tab[j]
  let last = -1

  // Remove first level of run-length encoding
  const R = new Array(1024) // runs
  while (z < size) {
    const run = src.ReadByte()
    R[j++] = run
    z += run

    if (run == last) {
      let copy = src.ReadByte()
      z += run * copy
      while (copy--) {
        R[j++] = run
      }
    }
    last = run
  }

  // Now expand runs in R to tab, noting 255 is max run
  let i = 0
  j = 0
  z = 0
  while (z < size) {
    let run_len = 0
    do {
      var part = R[j++]
      run_len += part
    } while (part == 255)

    while (run_len--) {
      tab[z++] = i
    }
    i++
  }
}

const QMAX = 256

const FLAG_DEDUP = 2
const FLAG_FLEN = 4
const FLAG_SEL = 8 // whether selector is used in context
const FLAG_QMAP = 16
const FLAG_PTAB = 32
const FLAG_DTAB = 64
const FLAG_QTAB = 128

const GFLAG_MULTI_PARAM = 1
const GFLAG_HAVE_STAB = 2
const GFLAG_DO_REV = 4

// Compute a new context from our current state and qual q
function fqz_update_ctx(params, state, q) {
  let last = params.context
  state.qctx = (state.qctx << params.qshift) + params.qtab[q] // >>> 0
  last += (state.qctx & ((1 << params.qbits) - 1)) << params.qloc // >>> 0

  if (params.do_pos) {
    last += params.ptab[Math.min(state.p, 1023)] << params.ploc
  }

  if (params.do_delta) {
    last += params.dtab[Math.min(state.delta, 255)] << params.dloc
    // Is it better to use q here or qtab[q]?
    // If qtab[q] we can map eg [a-z0-9A-Z]->0 ,->1 and have
    // delta being a token number count into comma separated lists?
    state.delta += state.prevq != q ? 1 : 0
    state.prevq = q
  }

  if (params.do_sel) {
    last += state.s << params.sloc
  }

  state.p--

  return last & 0xffff
}

function decode_fqz_single_param(src) {
  const p = {} // params

  // Load FQZ parameters
  p.context = src.ReadUint16()
  p.pflags = src.ReadByte()

  p.do_dedup = p.pflags & FLAG_DEDUP
  p.fixed_len = p.pflags & FLAG_FLEN
  p.do_sel = p.pflags & FLAG_SEL
  p.do_qmap = p.pflags & FLAG_QMAP
  p.do_pos = p.pflags & FLAG_PTAB
  p.do_delta = p.pflags & FLAG_DTAB
  p.do_qtab = p.pflags & FLAG_QTAB

  p.max_sym = src.ReadByte()

  let x = src.ReadByte()
  p.qbits = x >> 4
  p.qshift = x & 15
  x = src.ReadByte()
  p.qloc = x >> 4
  p.sloc = x & 15
  x = src.ReadByte()
  p.ploc = x >> 4
  p.dloc = x & 15

  // Qual map, eg to "unbin" Illumina qualities
  p.qmap = new Array(256)
  if (p.pflags & FLAG_QMAP) {
    for (var i = 0; i < p.max_sym; i++) {
      p.qmap[i] = src.ReadByte()
    }
  } else {
    // Useful optimisation to speed up main loop
    for (var i = 0; i < 256; i++) {
      p.qmap[i] = i
    } // NOP
  }

  // Read tables
  p.qtab = new Array(1024)
  if (p.qbits > 0 && p.pflags & FLAG_QTAB) {
    read_array(src, p.qtab, 256)
  } else {
    // Useful optimisation to speed up main loop
    for (var i = 0; i < 256; i++) {
      p.qtab[i] = i
    } // NOP
  }

  p.ptab = new Array(1024)
  if (p.pflags & FLAG_PTAB) {
    read_array(src, p.ptab, 1024)
  }

  p.dtab = new Array(256)
  if (p.pflags & FLAG_DTAB) {
    read_array(src, p.dtab, 256)
  }

  return p
}

function decode_fqz_params(src) {
  const gparams = {
    max_sym: 0,
  }

  // Check fqz format version
  const vers = src.ReadByte()
  if (vers != 5) {
    console.error('Invalid FQZComp version number')
    return
  }

  const gflags = src.ReadByte()
  const nparam = gflags & GFLAG_MULTI_PARAM ? src.ReadByte() : 1
  let max_sel = gflags.nparam > 1 ? gflags.nparam - 1 : 0 // Note max_sel, not num_sel

  const stab = new Array(256)
  if (gflags & GFLAG_HAVE_STAB) {
    max_sel = src.ReadByte()
    read_array(src, stab, 256)
  } else {
    for (var i = 0; i < nparam; i++) {
      stab[i] = i
    }
    for (; i < 256; i++) {
      stab[i] = nparam - 1
    }
  }
  gparams.do_rev = gflags & GFLAG_DO_REV
  gparams.stab = stab
  gparams.max_sel = max_sel

  gparams.params = new Array(gparams.nparam)
  for (let p = 0; p < nparam; p++) {
    gparams.params[p] = decode_fqz_single_param(src)
    if (gparams.max_sym < gparams.params[p].max_sym) {
      gparams.max_sym = gparams.params[p].max_sym
    }
  }

  return gparams
}

function fqz_create_models(gparams) {
  const model = {}

  model.qual = new Array(1 << 16)
  for (var i = 0; i < 1 << 16; i++) {
    model.qual[i] = new ByteModel(gparams.max_sym + 1)
  } // +1 as max value not num. values

  model.len = new Array(4)
  for (var i = 0; i < 4; i++) {
    model.len[i] = new ByteModel(256)
  }

  model.rev = new ByteModel(2)
  model.dup = new ByteModel(2)

  if (gparams.max_sel > 0) {
    model.sel = new ByteModel(gparams.max_sel + 1)
  } // +1 as max value not num. values

  return model
}

// Initialise a new record, updating state.
// Returns 1 if dup, otherwise 0
function decode_fqz_new_record(src, rc, gparams, model, state, rev) {
  // Parameter selector
  state.s = gparams.max_sel > 0 ? model.sel.ModelDecode(src, rc) : 0
  state.x = gparams.stab[state.s]

  const params = gparams.params[state.x]

  // Reset contexts at the start of each new record
  if (params.fixed_len >= 0) {
    // Not fixed or fixed but first record
    var len = model.len[0].ModelDecode(src, rc)
    len |= model.len[1].ModelDecode(src, rc) << 8
    len |= model.len[2].ModelDecode(src, rc) << 16
    len |= model.len[3].ModelDecode(src, rc) << 24
    if (params.fixed_len > 0) {
      params.fixed_len = -len
    }
  } else {
    len = -params.fixed_len
  }
  state.len = len

  if (gparams.do_rev) {
    rev[state.rec] = model.rev.ModelDecode(src, rc)
  }

  state.is_dup = 0
  if (params.pflags & FLAG_DEDUP) {
    if (model.dup.ModelDecode(src, rc)) {
      state.is_dup = 1
    }
  }

  state.p = len // number of remaining bytes in this record
  state.delta = 0
  state.qctx = 0
  state.prevq = 0
  state.rec++
}

function decode_fqz(src: IOStream, q_lens: number) {
  // Decode parameter block
  const n_out = src.ReadUint7()
  const gparams = decode_fqz_params(src)
  if (!gparams) {
    return
  }
  var params = gparams.params
  const rev = new Array(q_lens.length)

  // Create initial models
  const model = fqz_create_models(gparams)

  // Create our entropy encoder and output buffers
  const rc = new RangeCoder(src)
  rc.RangeStartDecode(src)
  const output = new Uint8Array(n_out)

  // Internal FQZ state
  const state = {
    qctx: 0, // Qual-only sub-context
    prevq: 0, // Previous quality value
    delta: 0, // Running delta (q vs prevq)
    p: 0, // Number of bases left in current record
    s: 0, // Current parameter selector value (0 if unused)
    x: 0, // "stab" tabulated copy of s
    len: 0, // Length of current string
    is_dup: 0, // This string is a duplicate of last
    rec: 0, // Record number
  }

  // The main decode loop itself
  let i = 0 // position in output buffer
  while (i < n_out) {
    if (state.p == 0) {
      decode_fqz_new_record(src, rc, gparams, model, state, rev)
      if (state.is_dup > 0) {
        if (model.dup.ModelDecode(src, rc)) {
          // Duplicate of last line
          for (let x = 0; x < len; x++) {
            output[i + x] = output[i + x - state.len]
          }
          i += state.len
          state.p = 0
          continue
        }
      }
      q_lens.push(state.len)

      var params = gparams.params[state.x]
      var last = params.context
    }

    // Decode the current quality (possibly mapped via qmap)
    const Q = model.qual[last].ModelDecode(src, rc)

    // if (params.do_qmap)
    //    output[i++] = params.qmap[Q];
    // else
    //    output[i++] = Q
    output[i++] = params.qmap[Q] // optimised version of above
    last = fqz_update_ctx(params, state, Q)
  }

  if (gparams.do_rev) {
    reverse_qualities(output, n_out, rev, q_lens)
  }

  return output
}

function reverse_qualities(qual, qual_len, rev, len) {
  let rec = 0
  let i = 0
  while (i < qual_len) {
    if (rev[rec]) {
      let j = 0
      let k = len[rec] - 1
      while (j < k) {
        const tmp = qual[i + j]
        qual[i + j] = qual[i + k]
        qual[i + k] = tmp
        j++
        k--
      }
    }

    i += len[rec++]
  }
}

export function decode(src: Uint8Array, q_lens) {
  const stream = new IOStream(src)

  return decode_fqz(stream, q_lens)
}
