//! LZSS search plus base-62 emit.
//!
//! The compressor prepends a 1024-space window to the input, then walks the data
//! looking for the longest back-reference within a 304-unit horizon. Each step
//! emits either a literal token or a match token in the base-62 alphabet.

use crate::config;
use crate::Sink;

/// Search the slice for the first occurrence of `needle` and return its start
/// index, mirroring `String.prototype.indexOf`.
fn index_of(haystack: &[u16], needle: &[u16]) -> Option<usize> {
    if needle.len() > haystack.len() {
        return None;
    }
    (0..=haystack.len() - needle.len()).find(|&i| &haystack[i..i + needle.len()] == needle)
}

/// Search the slice for the last occurrence of `needle` whose start index is at
/// most `limit`, mirroring `String.prototype.lastIndexOf(needle, limit)`.
///
/// A negative `limit` clamps to 0. A `limit` past the end clamps to the last
/// valid start.
fn last_index_of(haystack: &[u16], needle: &[u16], limit: i64) -> Option<usize> {
    let hlen = haystack.len();
    let nlen = needle.len();
    if nlen > hlen {
        return None;
    }
    let max_start = (hlen - nlen) as i64;
    let start = if limit < 0 { -1 } else { limit.min(max_start) };
    if start < 0 {
        return None;
    }
    (0..=start as usize)
        .rev()
        .find(|&i| &haystack[i..i + nlen] == needle)
}

/// Read a code unit from `data`, returning `None` for an out-of-range index.
///
/// JS `charCodeAt` yields `NaN` past the end, and `NaN` compares unequal to
/// everything. Treating the read as `None` reproduces that: the match extension
/// stops at the data boundary.
#[inline]
fn char_code_at(data: &[u16], i: i64) -> Option<u16> {
    if i < 0 {
        return None;
    }
    data.get(i as usize).copied()
}

/// Encoder state and output sink.
pub struct Compressor<'a, 'b> {
    data: Vec<u16>,
    data_len: usize,
    offset: usize,
    index: usize,
    length: usize,
    sink: &'b mut Sink<'a>,
}

impl<'a, 'b> Compressor<'a, 'b> {
    /// Compress `input` (UTF-16 code units) and deliver base-62 output to `sink`.
    pub fn run(input: &[u16], sink: &'b mut Sink<'a>) {
        if input.is_empty() {
            return;
        }
        let mut data = config::create_window_internal();
        data.extend_from_slice(input);
        let data_len = data.len();
        let mut c = Compressor {
            data,
            data_len,
            offset: config::WINDOW_MAX,
            index: 0,
            length: 0,
            sink,
        };
        c.compress();
    }

    /// Find the longest match at the current offset within the search horizon.
    ///
    /// Returns true and sets `self.index`/`self.length` when a match of length at
    /// least 2 exists. Returns false otherwise. The arithmetic mirrors the JS
    /// search step for step so the emitted distance and length match exactly.
    fn search(&mut self) -> bool {
        let data = &self.data;
        let offset = self.offset;
        let mut i: usize = 2;
        let mut len = config::BUFFER_MAX;
        if self.data_len - offset < len {
            len = self.data_len - offset;
        }
        if i > len {
            return false;
        }

        // `pos` may be negative early in the stream. `win` is the clamped slice,
        // but `pos`, `limit`, and `j` keep the signed value, matching JS.
        let pos: i64 = offset as i64 - config::WINDOW_BUFFER_MAX as i64;
        let win_start = pos.max(0) as usize;
        let win_end = (offset + len).min(self.data_len);
        let win = &data[win_start..win_end];
        // `win` indexing starts at `win_start`, but JS coordinates start at `pos`.
        // Translate a JS window index to a `win` index by subtracting the gap.
        let gap = win_start as i64 - pos; // 0 when pos >= 0, else -pos
        let limit: i64 = offset as i64 + i as i64 - 3 - pos;

        let mut s: Vec<u16> = Vec::new();
        let mut index_match: i64 = -1;
        let mut best_index: i64 = -1;

        loop {
            if i == 2 {
                s = vec![
                    char_code_at(data, offset as i64).unwrap_or(0),
                    char_code_at(data, offset as i64 + 1).unwrap_or(0),
                ];
                // `indexOf` in JS window coordinates: result + gap into `win`.
                let found = index_of(win, &s).map(|k| k as i64 - gap);
                match found {
                    None => break,
                    Some(idx) => {
                        index_match = idx;
                        if idx > limit {
                            break;
                        }
                    }
                }
            } else if i == 3 {
                s.push(char_code_at(data, offset as i64 + 2).unwrap_or(0));
            } else {
                s = (0..i)
                    .map(|k| char_code_at(data, offset as i64 + k as i64).unwrap_or(0))
                    .collect();
            }

            // `lastIndexOf` in JS window coordinates. Shift `limit` into `win`
            // coordinates by adding the gap.
            let last_index = last_index_of(win, &s, limit + gap).map(|k| k as i64 - gap);
            let last_index = match last_index {
                None => break,
                Some(li) => li,
            };

            best_index = last_index;
            let j = pos + last_index;
            loop {
                if char_code_at(data, offset as i64 + i as i64) != char_code_at(data, j + i as i64)
                {
                    break;
                }
                i += 1;
                if i >= len {
                    break;
                }
            }

            if index_match == last_index {
                i += 1;
                break;
            }
            i += 1;
            if i >= len {
                break;
            }
        }

        if i == 2 {
            return false;
        }

        self.index = (config::WINDOW_BUFFER_MAX as i64 - best_index) as usize;
        self.length = i - 1;
        true
    }

    /// Walk the data, emitting a literal or match token per step.
    fn compress(&mut self) {
        let mut buffer: Vec<u8> = Vec::with_capacity(config::COMPRESS_CHUNK_SIZE);
        let table = config::BASE62TABLE;
        let mut last_index: i64 = -1;

        while self.offset < self.data_len {
            if !self.search() {
                let c = u32::from(self.data[self.offset]);
                self.offset += 1;
                if c < config::LATIN_BUFFER_MAX {
                    let (c1, index) = if c < config::UNICODE_CHAR_MAX {
                        (c, config::LATIN_INDEX)
                    } else {
                        let c1 = c % config::UNICODE_CHAR_MAX;
                        let c2 = c / config::UNICODE_CHAR_MAX;
                        (c1, c2 + config::LATIN_INDEX)
                    };
                    let index = index as i64;
                    if last_index == index {
                        buffer.push(table[c1 as usize]);
                    } else {
                        buffer.push(table[(index as u32 - config::LATIN_INDEX_START) as usize]);
                        buffer.push(table[c1 as usize]);
                        last_index = index;
                    }
                } else {
                    let (c1, index) = if c < config::UNICODE_BUFFER_MAX {
                        (c, config::UNICODE_INDEX)
                    } else {
                        let c1 = c % config::UNICODE_BUFFER_MAX;
                        let c2 = c / config::UNICODE_BUFFER_MAX;
                        (c1, c2 + config::UNICODE_INDEX)
                    };
                    let (c3, c4) = if c1 < config::UNICODE_CHAR_MAX {
                        (c1, 0)
                    } else {
                        (c1 % config::UNICODE_CHAR_MAX, c1 / config::UNICODE_CHAR_MAX)
                    };
                    let index = index as i64;
                    if last_index == index {
                        buffer.push(table[c3 as usize]);
                        buffer.push(table[c4 as usize]);
                    } else {
                        buffer.push(table[config::CHAR_START]);
                        buffer.push(table[(index as usize) - config::TABLE_LENGTH]);
                        buffer.push(table[c3 as usize]);
                        buffer.push(table[c4 as usize]);
                        last_index = index;
                    }
                }
            } else {
                let (c1, c2) = if self.index < config::BUFFER_MAX {
                    (self.index, 0)
                } else {
                    (
                        self.index % config::BUFFER_MAX,
                        self.index / config::BUFFER_MAX,
                    )
                };
                if self.length == 2 {
                    buffer.push(table[c2 + config::COMPRESS_FIXED_START]);
                    buffer.push(table[c1]);
                } else {
                    buffer.push(table[c2 + config::COMPRESS_START]);
                    buffer.push(table[c1]);
                    buffer.push(table[self.length]);
                }
                self.offset += self.length;
                last_index = -1;
            }

            if buffer.len() >= config::COMPRESS_CHUNK_MAX {
                self.sink.push(&buffer);
                buffer.clear();
            }
        }

        if !buffer.is_empty() {
            self.sink.push(&buffer);
        }
        self.sink.end();
    }
}
