//! Base-62 decode plus LZSS expand.
//!
//! The decompressor seeds its output with the 1024-space window, reads the
//! base-62 stream token by token, and reconstructs literals and matches. Any
//! input character outside the alphabet is skipped. The leading window is
//! dropped before returning.
//!
//! Malformed input never panics. The JS source reads token bytes with
//! `table[data.charAt(++offset)]`, which yields `undefined` past the end or on a
//! stray character. That `undefined` flows into arithmetic as `NaN`. This module
//! tracks those reads as `None` and reproduces the `NaN` arithmetic so corrupt
//! streams decode to the exact same bytes.

use crate::config;

/// Build the reverse lookup: base-62 byte to its table index, or 255 for a byte
/// outside the alphabet.
fn build_table() -> [u8; 256] {
    let mut table = [255u8; 256];
    for (i, &b) in config::BASE62TABLE.iter().enumerate() {
        table[b as usize] = i as u8;
    }
    table
}

/// Look up a code unit in the alphabet. Returns `None` when the unit is not one
/// of the 62 symbols, which the decoder treats as a no-op separator.
#[inline]
fn lookup(table: &[u8; 256], unit: u16) -> Option<usize> {
    if unit > 0xFF {
        return None;
    }
    let v = table[unit as usize];
    if v == 255 {
        None
    } else {
        Some(v as usize)
    }
}

/// Index state for literal decoding.
///
/// JS starts `index` at `null`, sets it from start tags, and resets it to `null`
/// after a match. A failed read leaves it `NaN`. `Null` coerces to 0 in
/// arithmetic, while `NaN` poisons the result.
#[derive(Clone, Copy)]
enum Index {
    /// `null`, behaves as 0 in arithmetic.
    Null,
    /// A finite value set by a start tag.
    Value(i64),
    /// `NaN`, from a failed read.
    Nan,
}

/// Decode `data` (UTF-16 code units of a base-62 string) into output code units.
///
/// The returned vector has the leading window already stripped.
pub fn decompress(data: &[u16]) -> Vec<u16> {
    let table = build_table();
    let mut result: Vec<u16> = config::create_window_internal();

    let mut out = false;
    let mut index = Index::Null;
    let len = data.len();
    let mut offset = 0usize;

    // Read the next token byte and advance the cursor. Returns `None` for a read
    // past the end or a non-alphabet byte, matching `table[undefined]` in JS.
    let next = |offset: &mut usize| -> Option<usize> {
        *offset += 1;
        if *offset >= len {
            return None;
        }
        lookup(&table, data[*offset])
    };

    while offset < len {
        let c = match lookup(&table, data[offset]) {
            Some(c) => c,
            None => {
                offset += 1;
                continue;
            }
        };

        if c < config::DECODE_MAX {
            // Literal payload digit. `None` results push 0, matching
            // `String.fromCharCode(NaN) === '\0'`.
            let code: Option<i64> = if !out {
                match index {
                    Index::Null => Some(c as i64),
                    Index::Value(v) => Some(v * config::UNICODE_CHAR_MAX as i64 + c as i64),
                    Index::Nan => None,
                }
            } else {
                let c3 = next(&mut offset);
                match (c3, index) {
                    (Some(c3), Index::Null) => {
                        Some(c3 as i64 * config::UNICODE_CHAR_MAX as i64 + c as i64)
                    }
                    (Some(c3), Index::Value(v)) => Some(
                        c3 as i64 * config::UNICODE_CHAR_MAX as i64
                            + c as i64
                            + config::UNICODE_BUFFER_MAX as i64 * v,
                    ),
                    _ => None,
                }
            };
            result.push(code.unwrap_or(0) as u16);
        } else if c < config::LATIN_DECODE_MAX {
            index = Index::Value((c - config::DECODE_MAX) as i64);
            out = false;
        } else if c == config::CHAR_START {
            index = match next(&mut offset) {
                Some(c2) => Index::Value(c2 as i64 - 5),
                None => Index::Nan,
            };
            out = true;
        } else if c < config::COMPRESS_INDEX {
            let c2 = next(&mut offset);
            // `pos` is `None` (NaN) when `c2` failed to read. `length` is `None`
            // when its read failed. The two token classes differ only in their
            // base tag and whether a length byte follows.
            let (base_tag, length): (usize, Option<usize>) = if c < config::COMPRESS_FIXED_START {
                (config::COMPRESS_START, next(&mut offset))
            } else {
                (config::COMPRESS_FIXED_START, Some(2))
            };
            // `pos` is signed. Token byte `c` can sit below `base_tag` (index 47
            // routes here), giving a negative distance that `slice(-pos)` reads
            // from the front of the output instead of the back.
            let pos: Option<i64> =
                c2.map(|v| (c as i64 - base_tag as i64) * config::BUFFER_MAX as i64 + v as i64);

            expand_match(&mut result, pos, length);
            index = Index::Null;
        }

        offset += 1;
    }

    result.drain(..config::WINDOW_MAX);
    result
}

/// Reconstruct a match into `result`.
///
/// `pos` is the signed back-distance and `length` the copy count. Both are
/// `None` when the corresponding token byte failed to read (JS `NaN`). The
/// arithmetic mirrors `slice(-pos)`, the `sub.length = length` truncation, and
/// the RLE-style expansion loop, including the `NaN` short-circuits.
fn expand_match(result: &mut Vec<u16>, pos: Option<i64>, length: Option<usize>) {
    // Reproduce `result.slice(-pos)`. JS computes the start as `-pos`, then
    // resolves it against the array length: a negative start counts from the
    // end and clamps to 0, a non-negative start clamps to the length.
    //   pos  > 0: take the last `pos` units (whole array when pos >= len).
    //   pos == 0: `slice(0)` returns the whole array.
    //   pos  < 0: start at index `-pos`, reading from the front (empty when
    //             `-pos >= len`).
    // A `NaN` pos makes `slice(-NaN) === slice(0)`, again the whole array.
    let len = result.len() as i64;
    let start = match pos {
        Some(p) => {
            let s = -p;
            if s < 0 {
                (len + s).max(0)
            } else {
                s.min(len)
            }
        }
        None => 0,
    };
    let mut sub: Vec<u16> = result[start as usize..].to_vec();

    // `sub.length = length` truncates only when sub is longer. With a `NaN`
    // length the comparison `sub.length > NaN` is false, so no truncation.
    if let Some(length) = length {
        if sub.len() > length {
            sub.truncate(length);
        }
    }
    let sub_len = sub.len();
    if sub_len == 0 {
        return;
    }

    // `while (expandLen < length)`: a `NaN` length makes `0 < NaN` false, so the
    // loop never runs and nothing is appended.
    let length = match length {
        Some(l) => l,
        None => return,
    };
    let mut expand_len = 0;
    'outer: while expand_len < length {
        for &unit in &sub {
            result.push(unit);
            expand_len += 1;
            if expand_len >= length {
                break 'outer;
            }
        }
    }
}
