// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

//! FastScan: Faiss-style block layout (bbs=32) + vpshufb 32-way parallel lookup
//! for 4-bit PQ codes.
//!
//! Block layout: 32 vectors per block, codes interleaved by sub-quantizer pair.
//! Each block stores M/2 groups of 32 bytes (one byte per vector per sub-quant pair).
//!
//! Layout: [block0_pair0(32B), block0_pair1(32B), ..., block0_pairM/2(32B),
//!          block1_pair0(32B), ...]

/// Block size: 32 vectors per block (matches AVX2 register width).
pub const BBS: usize = 32;

/// Pack 4-bit codes from row-major [n][cs] into block layout.
/// Output layout: [num_blocks][cs][BBS] where cs = M/2.
/// Pads the last block with zeros if n is not a multiple of BBS.
pub fn pack_codes_block_layout(codes: &[u8], n: usize, cs: usize) -> Vec<u8> {
    let num_blocks = n.div_ceil(BBS);
    let block_size = cs * BBS; // bytes per block
    let mut packed = vec![0u8; num_blocks * block_size];

    for block in 0..num_blocks {
        let block_start = block * BBS;
        for pair in 0..cs {
            for vec_in_block in 0..BBS {
                let global_vec = block_start + vec_in_block;
                if global_vec < n {
                    packed[block * block_size + pair * BBS + vec_in_block] =
                        codes[global_vec * cs + pair];
                }
                // else: remains 0 (padding)
            }
        }
    }

    packed
}

/// Unpack block layout back to row-major [n][cs] (for compatibility).
pub fn unpack_codes_block_layout(packed: &[u8], n: usize, cs: usize) -> Vec<u8> {
    let num_blocks = n.div_ceil(BBS);
    let block_size = cs * BBS;
    let mut codes = vec![0u8; n * cs];

    for block in 0..num_blocks {
        let block_start = block * BBS;
        for pair in 0..cs {
            for vec_in_block in 0..BBS {
                let global_vec = block_start + vec_in_block;
                if global_vec < n {
                    codes[global_vec * cs + pair] =
                        packed[block * block_size + pair * BBS + vec_in_block];
                }
            }
        }
    }

    codes
}

/// Quantize a f32 distance table [M * 16] to u8 [M * 16].
/// Returns (qmin, qmax_used, quantized_table).
pub fn quantize_distance_table(table: &[f32], qmax_hint: f32) -> (f32, f32, Vec<u8>) {
    let qmin = table.iter().cloned().fold(f32::INFINITY, f32::min);
    let qmax = qmax_hint.max(table.iter().cloned().fold(f32::MIN, f32::max));
    let range = (qmax - qmin).max(1e-10);
    let factor = 255.0 / range;

    let qtable: Vec<u8> = table
        .iter()
        .map(|&d| ((d - qmin) * factor).clamp(0.0, 255.0) as u8)
        .collect();

    (qmin, qmax, qtable)
}

/// FastScan: scan block-layout 4-bit codes using SIMD.
/// codes: block layout [num_blocks][cs][BBS]
/// sim_table: [M * 16] f32 distance table
/// Returns f32 distances for `n` vectors.
pub fn fastscan_4bit(sim_table: &[f32], codes: &[u8], n: usize, m: usize, dists: &mut [f32]) {
    let cs = m / 2;

    // Step 1: f32 exact for first min(200, n) vectors as qmax calibration
    const FLAT_NUM: usize = 200;
    let flat_end = n.min(FLAT_NUM);
    let block_size = cs * BBS;

    for i in 0..flat_end {
        let block = i / BBS;
        let vec_in_block = i % BBS;
        let mut d = 0.0f32;
        for pair in 0..cs {
            let byte = codes[block * block_size + pair * BBS + vec_in_block];
            let lo = (byte & 0x0F) as usize;
            let hi = ((byte >> 4) & 0x0F) as usize;
            d += sim_table[(pair * 2) * 16 + lo];
            d += sim_table[(pair * 2 + 1) * 16 + hi];
        }
        dists[i] = d;
    }

    if n <= FLAT_NUM {
        return;
    }

    // Step 2: Quantize distance table using the table's own range
    let table_max = sim_table.iter().cloned().fold(f32::MIN, f32::max);
    let (qmin, _, qtable) = quantize_distance_table(sim_table, table_max);
    let range = (table_max - qmin).max(1e-10);

    // Step 3: Scan blocks using SIMD
    let num_blocks = n.div_ceil(BBS);
    let start_block = flat_end.div_ceil(BBS);

    #[cfg(target_arch = "x86_64")]
    {
        if is_x86_feature_detected!("avx2") {
            unsafe {
                fastscan_blocks_avx2(
                    &qtable,
                    codes,
                    cs,
                    start_block,
                    num_blocks,
                    block_size,
                    qmin,
                    range,
                    m,
                    dists,
                );
            }
            let partial_start = start_block * BBS;
            for i in flat_end..partial_start.min(n) {
                let block = i / BBS;
                let vec_in_block = i % BBS;
                let mut d = 0.0f32;
                for pair in 0..cs {
                    let byte = codes[block * block_size + pair * BBS + vec_in_block];
                    let lo = (byte & 0x0F) as usize;
                    let hi = ((byte >> 4) & 0x0F) as usize;
                    d += sim_table[(pair * 2) * 16 + lo];
                    d += sim_table[(pair * 2 + 1) * 16 + hi];
                }
                dists[i] = d;
            }
            return;
        }
    }

    #[cfg(target_arch = "aarch64")]
    {
        unsafe {
            fastscan_blocks_neon(
                &qtable,
                codes,
                cs,
                start_block,
                num_blocks,
                block_size,
                qmin,
                range,
                m,
                dists,
            );
        }
        let partial_start = start_block * BBS;
        for i in flat_end..partial_start.min(n) {
            let block = i / BBS;
            let vec_in_block = i % BBS;
            let mut d = 0.0f32;
            for pair in 0..cs {
                let byte = codes[block * block_size + pair * BBS + vec_in_block];
                let lo = (byte & 0x0F) as usize;
                let hi = ((byte >> 4) & 0x0F) as usize;
                d += sim_table[(pair * 2) * 16 + lo];
                d += sim_table[(pair * 2 + 1) * 16 + hi];
            }
            dists[i] = d;
        }
        return;
    }

    // Fallback: scalar scan on blocks (used when no SIMD available)
    #[allow(unreachable_code)]
    for block in start_block..num_blocks {
        let base_vec = block * BBS;
        let vecs_in_block = BBS.min(n - base_vec);

        let mut q_dists = [0u16; BBS];

        for pair in 0..cs {
            let qtab_lo = &qtable[(pair * 2) * 16..(pair * 2 + 1) * 16];
            let qtab_hi = &qtable[(pair * 2 + 1) * 16..(pair * 2 + 2) * 16];
            let block_codes = &codes[block * block_size + pair * BBS..];

            for v in 0..vecs_in_block {
                let byte = block_codes[v];
                let lo = (byte & 0x0F) as usize;
                let hi = ((byte >> 4) & 0x0F) as usize;
                q_dists[v] += qtab_lo[lo] as u16 + qtab_hi[hi] as u16;
            }
        }

        // Dequantize
        let inv_factor = range / 255.0;
        let base_dist = qmin * m as f32;
        for v in 0..vecs_in_block {
            dists[base_vec + v] = q_dists[v] as f32 * inv_factor + base_dist;
        }
    }

    // Fill gap between flat_end and start_block*BBS with exact computation
    let partial_start = start_block * BBS;
    for i in flat_end..partial_start.min(n) {
        let block = i / BBS;
        let vec_in_block = i % BBS;
        let mut d = 0.0f32;
        for pair in 0..cs {
            let byte = codes[block * block_size + pair * BBS + vec_in_block];
            let lo = (byte & 0x0F) as usize;
            let hi = ((byte >> 4) & 0x0F) as usize;
            d += sim_table[(pair * 2) * 16 + lo];
            d += sim_table[(pair * 2 + 1) * 16 + hi];
        }
        dists[i] = d;
    }
}

/// AVX2 block scan using vpshufb for 32-way parallel 4-bit lookup.
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
unsafe fn fastscan_blocks_avx2(
    qtable: &[u8],
    codes: &[u8],
    cs: usize,
    start_block: usize,
    num_blocks: usize,
    block_size: usize,
    qmin: f32,
    range: f32,
    m: usize,
    dists: &mut [f32],
) {
    use std::arch::x86_64::*;

    let inv_factor = range / 255.0;
    let base_dist = qmin * m as f32;

    for block in start_block..num_blocks {
        let base_vec = block * BBS;

        // u16 accumulators: 2 × __m256i = 32 × u16 values
        let mut accu_lo = _mm256_setzero_si256(); // vecs 0-15
        let mut accu_hi = _mm256_setzero_si256(); // vecs 16-31

        for pair in 0..cs {
            // Load 32-byte quantized LUT for this pair (lo + hi sub-quantizers)
            // LUT layout: [16 bytes for sub_lo, 16 bytes for sub_hi]
            let lut_lo_ptr = qtable.as_ptr().add((pair * 2) * 16);
            let lut_hi_ptr = qtable.as_ptr().add((pair * 2 + 1) * 16);

            // Broadcast 16-byte LUTs into 256-bit registers (same 16-byte table in both lanes)
            let lut_lo = _mm256_broadcastsi128_si256(_mm_loadu_si128(lut_lo_ptr as *const __m128i));
            let lut_hi = _mm256_broadcastsi128_si256(_mm_loadu_si128(lut_hi_ptr as *const __m128i));

            // Load 32 code bytes for this pair in this block
            let code_ptr = codes.as_ptr().add(block * block_size + pair * BBS);
            let code_vec = _mm256_loadu_si256(code_ptr as *const __m256i);

            // Split nibbles
            let mask = _mm256_set1_epi8(0x0F);
            let lo_nibbles = _mm256_and_si256(code_vec, mask);
            let hi_nibbles = _mm256_and_si256(_mm256_srli_epi16(code_vec, 4), mask);

            // vpshufb: 32-way parallel lookup
            let dist_lo = _mm256_shuffle_epi8(lut_lo, lo_nibbles);
            let dist_hi = _mm256_shuffle_epi8(lut_hi, hi_nibbles);

            // Widen each to u16 separately then add (avoids u8 saturation overflow)
            let zero = _mm256_setzero_si256();
            let dlo_lo = _mm256_unpacklo_epi8(dist_lo, zero);
            let dlo_hi = _mm256_unpackhi_epi8(dist_lo, zero);
            let dhi_lo = _mm256_unpacklo_epi8(dist_hi, zero);
            let dhi_hi = _mm256_unpackhi_epi8(dist_hi, zero);

            accu_lo = _mm256_add_epi16(accu_lo, _mm256_add_epi16(dlo_lo, dhi_lo));
            accu_hi = _mm256_add_epi16(accu_hi, _mm256_add_epi16(dlo_hi, dhi_hi));
        }

        // Extract u16 values and dequantize to f32.
        // _mm256_unpacklo/hi_epi8 operates per 128-bit lane, so:
        //   accu_lo = [v0..v7 | v16..v23], accu_hi = [v8..v15 | v24..v31]
        // Reassemble correct order with cross-lane permute.
        let result_lo = _mm256_permute2x128_si256(accu_lo, accu_hi, 0x20);
        let result_hi = _mm256_permute2x128_si256(accu_lo, accu_hi, 0x31);
        let mut q_vals = [0u16; BBS];
        _mm256_storeu_si256(q_vals.as_mut_ptr() as *mut __m256i, result_lo);
        _mm256_storeu_si256(q_vals.as_mut_ptr().add(16) as *mut __m256i, result_hi);

        for v in 0..BBS {
            let idx = base_vec + v;
            if idx < dists.len() {
                dists[idx] = q_vals[v] as f32 * inv_factor + base_dist;
            }
        }
    }
}

/// ARM NEON block scan (16-way per instruction, 2 passes per block).
#[cfg(target_arch = "aarch64")]
#[target_feature(enable = "neon")]
unsafe fn fastscan_blocks_neon(
    qtable: &[u8],
    codes: &[u8],
    cs: usize,
    start_block: usize,
    num_blocks: usize,
    block_size: usize,
    qmin: f32,
    range: f32,
    m: usize,
    dists: &mut [f32],
) {
    use std::arch::aarch64::*;

    let inv_factor = range / 255.0;
    let base_dist = qmin * m as f32;

    for block in start_block..num_blocks {
        let base_vec = block * BBS;

        // u16 accumulators: 4 × uint16x8_t = 32 × u16
        let mut accu = [vdupq_n_u16(0); 4];

        for pair in 0..cs {
            let lut_lo = vld1q_u8(qtable.as_ptr().add((pair * 2) * 16));
            let lut_hi = vld1q_u8(qtable.as_ptr().add((pair * 2 + 1) * 16));

            let code_ptr = codes.as_ptr().add(block * block_size + pair * BBS);

            // Process 16 vectors at a time (NEON = 128-bit)
            for half in 0..2 {
                let code_vec = vld1q_u8(code_ptr.add(half * 16));

                let mask = vdupq_n_u8(0x0F);
                let lo_nib = vandq_u8(code_vec, mask);
                let hi_nib = vshrq_n_u8(code_vec, 4);

                // tbl: 16-way lookup
                let dist_lo = vqtbl1q_u8(lut_lo, lo_nib);
                let dist_hi = vqtbl1q_u8(lut_hi, hi_nib);

                // Widen each to u16 separately then add (avoids u8 saturation overflow)
                accu[half * 2] = vaddq_u16(accu[half * 2], vmovl_u8(vget_low_u8(dist_lo)));
                accu[half * 2] = vaddq_u16(accu[half * 2], vmovl_u8(vget_low_u8(dist_hi)));
                accu[half * 2 + 1] = vaddq_u16(accu[half * 2 + 1], vmovl_u8(vget_high_u8(dist_lo)));
                accu[half * 2 + 1] = vaddq_u16(accu[half * 2 + 1], vmovl_u8(vget_high_u8(dist_hi)));
            }
        }

        // Extract and dequantize
        let mut q_vals = [0u16; BBS];
        for i in 0..4 {
            vst1q_u16(q_vals.as_mut_ptr().add(i * 8), accu[i]);
        }

        for v in 0..BBS {
            let idx = base_vec + v;
            if idx < dists.len() {
                dists[idx] = q_vals[v] as f32 * inv_factor + base_dist;
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_pack_unpack_roundtrip() {
        let n = 100;
        let cs = 4; // m/2 = 4, i.e. m=8
        let codes: Vec<u8> = (0..n * cs).map(|i| (i % 256) as u8).collect();

        let packed = pack_codes_block_layout(&codes, n, cs);
        let unpacked = unpack_codes_block_layout(&packed, n, cs);

        assert_eq!(codes, unpacked);
    }

    #[test]
    fn test_fastscan_correctness() {
        let m = 8;
        let cs = m / 2;
        let n = 100;

        // Random-ish codes
        let codes_row: Vec<u8> = (0..n * cs).map(|i| ((i * 7 + 3) % 256) as u8).collect();

        // Random-ish distance table [M * 16]
        let sim_table: Vec<f32> = (0..m * 16).map(|i| (i as f32) * 0.1 + 0.5).collect();

        // Compute ground truth with scalar
        let mut expected = vec![0.0f32; n];
        for i in 0..n {
            for pair in 0..cs {
                let byte = codes_row[i * cs + pair];
                let lo = (byte & 0x0F) as usize;
                let hi = ((byte >> 4) & 0x0F) as usize;
                expected[i] += sim_table[(pair * 2) * 16 + lo];
                expected[i] += sim_table[(pair * 2 + 1) * 16 + hi];
            }
        }

        // Pack into block layout and scan
        let packed = pack_codes_block_layout(&codes_row, n, cs);
        let mut result = vec![0.0f32; n];
        fastscan_4bit(&sim_table, &packed, n, m, &mut result);

        // Check first 200 (f32 exact) — should match perfectly
        for i in 0..n.min(200) {
            assert!(
                (result[i] - expected[i]).abs() < 1e-5,
                "Mismatch at {}: {} vs {}",
                i,
                result[i],
                expected[i]
            );
        }
    }

    #[test]
    fn test_fastscan_large() {
        let m = 16;
        let cs = m / 2;
        let n = 1000; // > 200, exercises quantized path

        let codes_row: Vec<u8> = (0..n * cs).map(|i| ((i * 13 + 7) % 256) as u8).collect();
        let sim_table: Vec<f32> = (0..m * 16).map(|i| (i as f32) * 0.05 + 1.0).collect();

        // Compute scalar reference
        let mut expected = vec![0.0f32; n];
        for i in 0..n {
            for pair in 0..cs {
                let byte = codes_row[i * cs + pair];
                let lo = (byte & 0x0F) as usize;
                let hi = ((byte >> 4) & 0x0F) as usize;
                expected[i] += sim_table[(pair * 2) * 16 + lo];
                expected[i] += sim_table[(pair * 2 + 1) * 16 + hi];
            }
        }

        let packed = pack_codes_block_layout(&codes_row, n, cs);
        let mut result = vec![0.0f32; n];
        fastscan_4bit(&sim_table, &packed, n, m, &mut result);

        // First 200 are computed with f32 exact — should match perfectly
        for i in 0..200 {
            assert!(
                (result[i] - expected[i]).abs() < 1e-5,
                "Exact mismatch at {}: got {}, expected {}",
                i,
                result[i],
                expected[i]
            );
        }

        // Beyond 200: quantized path — allow quantization tolerance
        let max_expected = expected.iter().cloned().fold(f32::MIN, f32::max);
        let tolerance = max_expected * 0.02; // 2% relative tolerance for u8 quantization
        for i in 200..n {
            assert!(
                (result[i] - expected[i]).abs() <= tolerance,
                "SIMD mismatch at {}: got {}, expected {}, diff {}",
                i,
                result[i],
                expected[i],
                (result[i] - expected[i]).abs()
            );
        }
    }
}
