//! The x86_64 microkernels of the narrow convolutions and the inverse GDN, on AVX2 and
//! AVX-512 (`std::arch`), each selected only where the processor reports its features
//! ([`super::Isa::supported`]).
//!
//! Convolution: two input channels share a 32-bit word, and a tap's weights for one
//! output channel are the matching pair. `vpmaddwd` (AVX2, AVX-512BW) multiplies the
//! two 16-bit pairs of each 32-bit lane and adds both products into it; `vpdpwssd`
//! (AVX-VNNI, AVX512-VNNI) does the same and adds the lane's accumulator, wrapping. A
//! lane accumulates one output element's products over a block of at most `kblk` taps,
//! which [`super::narrow`] chose so that no partial sum leaves `i32`; so neither
//! instruction wraps, and each lane holds the exact sum of its block's products. Each
//! block's sums are then sign-extended to 64 bits, shifted by the limb's power of two
//! and added into the output's 64-bit sum.
//!
//! Inverse GDN: `vpmuldq` multiplies the low signed 32 bits of each 64-bit lane (`γ` and
//! `x²`, each within `i32` on this path) into an exact 64-bit product; it is then
//! shifted right arithmetically by `gf` (`vpsraq` on AVX-512; on AVX2, which has no
//! 64-bit arithmetic shift, a logical shift where every product is non-negative, else
//! the logical shift with the sign bits put back) and added into the norm, which the
//! caller has proved cannot leave `i64`. On AVX-512 (with DQ) the outputs are formed in
//! lanes too: each root seeded from the IEEE-754 square root, corrected and verified in
//! integers, then `(x · root) >> frac` by `vpmullq`, exact for `|x| < 2^31`.

use std::arch::x86_64::*;

/// Which x86 kernels run.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum Kind {
    Avx2,
    Avx2Vnni,
    Avx512,
    Avx512Vnni,
}

impl Kind {
    /// Output channels a convolution tile holds in registers.
    pub(super) fn block(self) -> usize {
        match self {
            Kind::Avx2 | Kind::Avx2Vnni => 4,
            Kind::Avx512 | Kind::Avx512Vnni => 8,
        }
    }
}

/// `acc[j·n + e] += (Σ_t pairdot(ws[t·NB + j], x[offs_t + base + e])) · 2^sh` for
/// `e < n`, `j < NB`, in blocks of at most `kblk` taps per 32-bit partial sum.
///
/// # Safety
/// The processor runs `kind`; `offs_t + base + n ≤ x.len()` for every tap `t`;
/// `ws.len() = NB · offs.len()`; `acc.len() ≥ NB · n`; and no block's partial sum of
/// any element leaves `i32` (`kblk` from [`super::narrow`]'s plan).
#[allow(clippy::too_many_arguments)]
pub(super) unsafe fn sums<const NB: usize>(kind: Kind, ws: &[i32], offs: &[usize], x: &[u32], base: usize, n: usize, kblk: usize, sh: u32, acc: &mut [i64]) {
    debug_assert_eq!(ws.len(), NB * offs.len());
    debug_assert!(acc.len() >= NB * n);
    // SAFETY: forwarded from the caller.
    unsafe {
        match kind {
            Kind::Avx2 => avx2_madd::<NB>(ws, offs, x, base, n, kblk, sh, acc),
            Kind::Avx2Vnni => avx2_vnni::<NB>(ws, offs, x, base, n, kblk, sh, acc),
            Kind::Avx512 => avx512_madd::<NB>(ws, offs, x, base, n, kblk, sh, acc),
            Kind::Avx512Vnni => avx512_vnni::<NB>(ws, offs, x, base, n, kblk, sh, acc),
        }
    }
}

/// The multiply-add of one tap into a 32-bit accumulator: `acc + lo(x)·lo(w) + hi(x)·hi(w)`.
trait Dot256 {
    unsafe fn dot(acc: __m256i, x: __m256i, w: __m256i) -> __m256i;
}
struct Madd256;
impl Dot256 for Madd256 {
    #[inline(always)]
    unsafe fn dot(acc: __m256i, x: __m256i, w: __m256i) -> __m256i {
        // SAFETY: called only inside functions compiled with AVX2.
        unsafe { _mm256_add_epi32(acc, _mm256_madd_epi16(x, w)) }
    }
}
struct Vnni256;
impl Dot256 for Vnni256 {
    #[inline(always)]
    unsafe fn dot(acc: __m256i, x: __m256i, w: __m256i) -> __m256i {
        // SAFETY: called only inside functions compiled with AVX-VNNI.
        unsafe { _mm256_dpwssd_avx_epi32(acc, x, w) }
    }
}

trait Dot512 {
    unsafe fn dot(acc: __m512i, x: __m512i, w: __m512i) -> __m512i;
}
struct Madd512;
impl Dot512 for Madd512 {
    #[inline(always)]
    unsafe fn dot(acc: __m512i, x: __m512i, w: __m512i) -> __m512i {
        // SAFETY: called only inside functions compiled with AVX-512BW.
        unsafe { _mm512_add_epi32(acc, _mm512_madd_epi16(x, w)) }
    }
}
struct Vnni512;
impl Dot512 for Vnni512 {
    #[inline(always)]
    unsafe fn dot(acc: __m512i, x: __m512i, w: __m512i) -> __m512i {
        // SAFETY: called only inside functions compiled with AVX512-VNNI.
        unsafe { _mm512_dpwssd_epi32(acc, x, w) }
    }
}

#[target_feature(enable = "avx2")]
#[allow(clippy::too_many_arguments)]
unsafe fn avx2_madd<const NB: usize>(ws: &[i32], offs: &[usize], x: &[u32], base: usize, n: usize, kblk: usize, sh: u32, acc: &mut [i64]) {
    // SAFETY: forwarded.
    unsafe { body256::<Madd256, NB>(ws, offs, x, base, n, kblk, sh, acc) }
}

#[target_feature(enable = "avx2,avxvnni")]
#[allow(clippy::too_many_arguments)]
unsafe fn avx2_vnni<const NB: usize>(ws: &[i32], offs: &[usize], x: &[u32], base: usize, n: usize, kblk: usize, sh: u32, acc: &mut [i64]) {
    // SAFETY: forwarded.
    unsafe { body256::<Vnni256, NB>(ws, offs, x, base, n, kblk, sh, acc) }
}

#[target_feature(enable = "avx512f,avx512bw")]
#[allow(clippy::too_many_arguments)]
unsafe fn avx512_madd<const NB: usize>(ws: &[i32], offs: &[usize], x: &[u32], base: usize, n: usize, kblk: usize, sh: u32, acc: &mut [i64]) {
    // SAFETY: forwarded.
    unsafe { body512::<Madd512, NB>(ws, offs, x, base, n, kblk, sh, acc) }
}

#[target_feature(enable = "avx512f,avx512bw,avx512vnni")]
#[allow(clippy::too_many_arguments)]
unsafe fn avx512_vnni<const NB: usize>(ws: &[i32], offs: &[usize], x: &[u32], base: usize, n: usize, kblk: usize, sh: u32, acc: &mut [i64]) {
    // SAFETY: forwarded.
    unsafe { body512::<Vnni512, NB>(ws, offs, x, base, n, kblk, sh, acc) }
}

/// Add the eight 32-bit sums of `a`, sign-extended and shifted left by `cnt`, into
/// `d[0..8]`.
#[inline(always)]
unsafe fn flush256(a: __m256i, cnt: __m128i, d: *mut i64) {
    // SAFETY: `d[0..8]` is the caller's; AVX2 is enabled in every caller.
    unsafe {
        let lo = _mm256_sll_epi64(_mm256_cvtepi32_epi64(_mm256_castsi256_si128(a)), cnt);
        let hi = _mm256_sll_epi64(_mm256_cvtepi32_epi64(_mm256_extracti128_si256::<1>(a)), cnt);
        _mm256_storeu_si256(d as *mut __m256i, _mm256_add_epi64(_mm256_loadu_si256(d as *const __m256i), lo));
        _mm256_storeu_si256(d.add(4) as *mut __m256i, _mm256_add_epi64(_mm256_loadu_si256(d.add(4) as *const __m256i), hi));
    }
}

/// AVX2: `NB ≤ 4` channels × 16 elements (two vectors of eight 32-bit lanes), then 8,
/// then the scalar tail.
#[inline(always)]
#[allow(clippy::too_many_arguments)]
unsafe fn body256<D: Dot256, const NB: usize>(ws: &[i32], offs: &[usize], x: &[u32], base: usize, n: usize, kblk: usize, sh: u32, acc: &mut [i64]) {
    let taps = offs.len();
    let xp = x.as_ptr();
    let ap = acc.as_mut_ptr();
    // SAFETY (every raw access): reads `x[offs_t + base + e .. + 8·V]` with
    // `e + 8·V ≤ n`, in bounds by the contract of [`sums`]; reads `ws[t·NB + j]`, and
    // writes `acc[j·n + e .. + 8·V]`, both within their slices.
    unsafe {
        let cnt = _mm_cvtsi32_si128(sh as i32);
        // Tap blocks outermost (see `narrow::TAPS_PER_PASS`); in each, the row in steps of
        // 16 elements, then one of 8; the last `n mod 8` in scalar arithmetic after.
        let blk = kblk.min(super::narrow::TAPS_PER_PASS);
        let (n16, n8) = (n - n % 16, n - n % 8);
        let mut t0 = 0;
        while t0 < taps {
            let t1 = t0 + blk.min(taps - t0);
            macro_rules! step {
                ($v:literal, $e:expr) => {{
                    let e = $e;
                    let mut a = [[_mm256_setzero_si256(); $v]; NB];
                    for t in t0..t1 {
                        let p = xp.add(*offs.get_unchecked(t) + base + e);
                        let mut xv = [_mm256_setzero_si256(); $v];
                        for (k, v) in xv.iter_mut().enumerate() {
                            *v = _mm256_loadu_si256(p.add(8 * k) as *const __m256i);
                        }
                        let w = ws.as_ptr().add(t * NB);
                        for (j, aj) in a.iter_mut().enumerate() {
                            let wj = _mm256_set1_epi32(*w.add(j));
                            for (av, &xk) in aj.iter_mut().zip(&xv) {
                                *av = D::dot(*av, xk, wj);
                            }
                        }
                    }
                    for (j, aj) in a.iter().enumerate() {
                        for (k, &av) in aj.iter().enumerate() {
                            flush256(av, cnt, ap.add(j * n + e + 8 * k));
                        }
                    }
                }};
            }
            let mut e = 0;
            while e < n16 {
                step!(2, e);
                e += 16;
            }
            if n8 > n16 {
                step!(1, n16);
            }
            t0 = t1;
        }
        super::narrow::tail_pairs::<NB>(ws, offs, x, base, n8, n, sh, acc);
    }
}

/// Add the sixteen 32-bit sums of `a` selected by `m` (bit `i` for element `i`),
/// sign-extended and shifted left by `cnt`, into `d[0..16]`.
#[inline(always)]
unsafe fn flush512(a: __m512i, cnt: __m128i, m: u16, d: *mut i64) {
    // SAFETY: `d[i]` for every selected `i` is the caller's; masked loads and stores
    // touch no other element. AVX-512F is enabled in every caller.
    unsafe {
        let lo = _mm512_sll_epi64(_mm512_cvtepi32_epi64(_mm512_castsi512_si256(a)), cnt);
        let hi = _mm512_sll_epi64(_mm512_cvtepi32_epi64(_mm512_extracti64x4_epi64::<1>(a)), cnt);
        let (ml, mh) = (m as u8, (m >> 8) as u8);
        _mm512_mask_storeu_epi64(d, ml, _mm512_add_epi64(_mm512_maskz_loadu_epi64(ml, d), lo));
        _mm512_mask_storeu_epi64(d.add(8), mh, _mm512_add_epi64(_mm512_maskz_loadu_epi64(mh, d.add(8)), hi));
    }
}

/// AVX-512: `NB ≤ 8` channels × 32 elements (two vectors of sixteen 32-bit lanes), then
/// 16, then the last `n mod 16` under a lane mask.
#[inline(always)]
#[allow(clippy::too_many_arguments)]
unsafe fn body512<D: Dot512, const NB: usize>(ws: &[i32], offs: &[usize], x: &[u32], base: usize, n: usize, kblk: usize, sh: u32, acc: &mut [i64]) {
    let taps = offs.len();
    let xp = x.as_ptr();
    let ap = acc.as_mut_ptr();
    // SAFETY (every raw access): reads `x[offs_t + base + e + i]` only for `e + i < n`
    // (whole vectors where `e + 16·V ≤ n`, masked lanes after), in bounds by the contract
    // of [`sums`]; reads `ws[t·NB + j]`; writes `acc[j·n + e + i]` only for `e + i < n`.
    unsafe {
        let cnt = _mm_cvtsi32_si128(sh as i32);
        // Tap blocks outermost (see `narrow::TAPS_PER_PASS`); in each, the row in steps of
        // 32 elements, then one of 16, then the last `n mod 16` under a lane mask.
        let blk = kblk.min(super::narrow::TAPS_PER_PASS);
        let (n32, n16) = (n - n % 32, n - n % 16);
        let mut t0 = 0;
        while t0 < taps {
            let t1 = t0 + blk.min(taps - t0);
            macro_rules! step {
                ($v:literal, $e:expr, $mask:expr) => {{
                    let (e, m): (usize, u16) = ($e, $mask);
                    let mut a = [[_mm512_setzero_si512(); $v]; NB];
                    for t in t0..t1 {
                        let p = xp.add(*offs.get_unchecked(t) + base + e);
                        let mut xv = [_mm512_setzero_si512(); $v];
                        for (k, v) in xv.iter_mut().enumerate() {
                            *v = if m == u16::MAX { _mm512_loadu_si512(p.add(16 * k) as *const __m512i) } else { _mm512_maskz_loadu_epi32(m, p.add(16 * k) as *const i32) };
                        }
                        let w = ws.as_ptr().add(t * NB);
                        for (j, aj) in a.iter_mut().enumerate() {
                            let wj = _mm512_set1_epi32(*w.add(j));
                            for (av, &xk) in aj.iter_mut().zip(&xv) {
                                *av = D::dot(*av, xk, wj);
                            }
                        }
                    }
                    for (j, aj) in a.iter().enumerate() {
                        for (k, &av) in aj.iter().enumerate() {
                            flush512(av, cnt, m, ap.add(j * n + e + 16 * k));
                        }
                    }
                }};
            }
            let mut e = 0;
            while e < n32 {
                step!(2, e, u16::MAX);
                e += 32;
            }
            if n16 > n32 {
                step!(1, n32, u16::MAX);
            }
            if n > n16 {
                step!(1, n16, ((1u32 << (n - n16)) - 1) as u16);
            }
            t0 = t1;
        }
    }
}

// ─────────────────────────────────────────────────────────────────────
// Inverse GDN norms
// ─────────────────────────────────────────────────────────────────────

/// Channels per block of [`igdn_norms`] for `kind`.
pub(super) fn igdn_block(kind: Kind) -> usize {
    match kind {
        Kind::Avx2 | Kind::Avx2Vnni => 4,
        Kind::Avx512 | Kind::Avx512Vnni => 8,
    }
}

/// IGDN norms of `B = igdn_block(kind)` output channels: `norms[k·np + p] = beta[k] +
/// Σ_j (gt[j·B + k] · x2[j·np + p]) >> gf` for positions `p` in a leading run (whose
/// length is returned; the caller computes the rest), terms in `j` order from `beta`.
/// `nonneg`: every `gt` is `≥ 0`, so every product is (each `x2 ≥ 0`).
///
/// # Safety
/// The processor runs `kind`; `x2.len() ≥ c·np`, `gt.len() = c·B`, `beta.len() = B`,
/// `norms.len() ≥ B·np`, `gf ≤ 63`; and no partial sum leaves `i64` (the caller's bound).
#[allow(clippy::too_many_arguments)]
pub(super) unsafe fn igdn_norms(kind: Kind, x2: &[i32], np: usize, gt: &[i32], beta: &[i64], gf: u32, nonneg: bool, norms: &mut [i64]) -> usize {
    // SAFETY: forwarded from the caller.
    unsafe {
        match (kind, nonneg) {
            (Kind::Avx512 | Kind::Avx512Vnni, _) => igdn_avx512(x2, np, gt, beta, gf, norms),
            (Kind::Avx2 | Kind::Avx2Vnni, true) => igdn_avx2::<true>(x2, np, gt, beta, gf, norms),
            (Kind::Avx2 | Kind::Avx2Vnni, false) => igdn_avx2::<false>(x2, np, gt, beta, gf, norms),
        }
    }
}

/// AVX-512: 8 channels × 16 positions (two vectors of eight 64-bit lanes per channel).
#[target_feature(enable = "avx512f")]
unsafe fn igdn_avx512(x2: &[i32], np: usize, gt: &[i32], beta: &[i64], gf: u32, norms: &mut [i64]) -> usize {
    const B: usize = 8;
    let c = gt.len() / B;
    let cnt = _mm_cvtsi32_si128(gf as i32);
    let mut p = 0;
    // SAFETY (every raw access): reads `x2[j·np + p .. + 16]` with `p + 16 ≤ np`,
    // `gt[j·B + k]` and `beta[k]`, and writes `norms[k·np + p .. + 16]`, all within the
    // slices by the contract of [`igdn_norms`].
    unsafe {
        while p + 16 <= np {
            let mut a = [[_mm512_setzero_si512(); 2]; B];
            for (k, ak) in a.iter_mut().enumerate() {
                let b = _mm512_set1_epi64(beta[k]);
                *ak = [b, b];
            }
            for j in 0..c {
                let xs = _mm512_loadu_si512(x2.as_ptr().add(j * np + p) as *const __m512i);
                let x0 = _mm512_cvtepi32_epi64(_mm512_castsi512_si256(xs));
                let x1 = _mm512_cvtepi32_epi64(_mm512_extracti64x4_epi64::<1>(xs));
                for (k, ak) in a.iter_mut().enumerate() {
                    let g = _mm512_set1_epi64(*gt.get_unchecked(j * B + k) as i64);
                    ak[0] = _mm512_add_epi64(ak[0], _mm512_sra_epi64(_mm512_mul_epi32(x0, g), cnt));
                    ak[1] = _mm512_add_epi64(ak[1], _mm512_sra_epi64(_mm512_mul_epi32(x1, g), cnt));
                }
            }
            for (k, ak) in a.iter().enumerate() {
                let d = norms.as_mut_ptr().add(k * np + p);
                _mm512_storeu_si512(d as *mut __m512i, ak[0]);
                _mm512_storeu_si512(d.add(8) as *mut __m512i, ak[1]);
            }
            p += 16;
        }
    }
    p
}

/// `⌊v / 2^gf⌋` in each 64-bit lane (`cnt` = gf, `back` = 64 − gf): the logical shift,
/// with the gf top bits set again in a negative lane (a shift left by 64 of all ones
/// gives 0, so gf = 0 is `v` itself). With `NONNEG` every lane is non-negative and the
/// logical shift is the floor.
#[inline(always)]
unsafe fn sra256<const NONNEG: bool>(v: __m256i, cnt: __m128i, back: __m128i) -> __m256i {
    // SAFETY: called only inside functions compiled with AVX2.
    unsafe {
        if NONNEG {
            _mm256_srl_epi64(v, cnt)
        } else {
            let neg = _mm256_cmpgt_epi64(_mm256_setzero_si256(), v);
            _mm256_or_si256(_mm256_srl_epi64(v, cnt), _mm256_sll_epi64(neg, back))
        }
    }
}

/// AVX2: 4 channels × 8 positions (two vectors of four 64-bit lanes per channel).
#[target_feature(enable = "avx2")]
unsafe fn igdn_avx2<const NONNEG: bool>(x2: &[i32], np: usize, gt: &[i32], beta: &[i64], gf: u32, norms: &mut [i64]) -> usize {
    const B: usize = 4;
    let c = gt.len() / B;
    let cnt = _mm_cvtsi32_si128(gf as i32);
    // the sign bits a logical shift by gf clears: shifted in from the left by 64 − gf
    let back = _mm_cvtsi32_si128(64 - gf as i32);
    let zero = _mm256_setzero_si256();
    let mut p = 0;
    // SAFETY (every raw access): as in `igdn_avx512`, with 8 positions per step.
    unsafe {
        while p + 8 <= np {
            let mut a = [[zero; 2]; B];
            for (k, ak) in a.iter_mut().enumerate() {
                let b = _mm256_set1_epi64x(beta[k]);
                *ak = [b, b];
            }
            for j in 0..c {
                let xs = _mm256_loadu_si256(x2.as_ptr().add(j * np + p) as *const __m256i);
                let x0 = _mm256_cvtepi32_epi64(_mm256_castsi256_si128(xs));
                let x1 = _mm256_cvtepi32_epi64(_mm256_extracti128_si256::<1>(xs));
                for (k, ak) in a.iter_mut().enumerate() {
                    let g = _mm256_set1_epi64x(*gt.get_unchecked(j * B + k) as i64);
                    ak[0] = _mm256_add_epi64(ak[0], sra256::<NONNEG>(_mm256_mul_epi32(x0, g), cnt, back));
                    ak[1] = _mm256_add_epi64(ak[1], sra256::<NONNEG>(_mm256_mul_epi32(x1, g), cnt, back));
                }
            }
            for (k, ak) in a.iter().enumerate() {
                let d = norms.as_mut_ptr().add(k * np + p);
                _mm256_storeu_si256(d as *mut __m256i, ak[0]);
                _mm256_storeu_si256(d.add(4) as *mut __m256i, ak[1]);
            }
            p += 8;
        }
    }
    p
}

/// The inverse GDN's outputs from its norms on AVX-512 (F and DQ): `dst[p] = (x[p] ·
/// ⌊√max(0, norm[p])⌋) >> frac` for `p` in a leading multiple of 8 (returned). Each root is
/// taken digit by digit in integer lanes (the binary restoring method: 32 steps, one per
/// digit of a root of `n < 2^63`), with no floating point, then held to `s² ≤ n < (s +
/// 1)²` in integers; a step whose roots do not all meet it is left to the caller (none is
/// expected). The product is `vpmullq`, exact because every `|x| < 2^31` and `s < 2^32`.
///
/// # Safety
/// The processor runs AVX-512 F and DQ; `norm`, `x` and `dst` hold at least `n` values;
/// every `|x| ≤ i32::MAX`; `frac ≤ 63`.
#[target_feature(enable = "avx512f,avx512dq")]
pub(super) unsafe fn igdn_emit_avx512(norm: &[i64], x: &[i64], frac: u32, n: usize, dst: &mut [i64]) -> usize {
    let cnt = _mm_cvtsi32_si128(frac as i32);
    let one = _mm512_set1_epi64(1);
    let cap = _mm512_set1_epi64(u32::MAX as i64);
    let zero = _mm512_setzero_si512();
    let mut p = 0;
    // SAFETY (every raw access): reads `norm[p .. p + 8]` and `x[p .. p + 8]` and writes
    // `dst[p .. p + 8]` with `p + 8 ≤ n`, within the slices by the contract.
    unsafe {
        while p + 8 <= n {
            let nv = _mm512_max_epi64(_mm512_loadu_si512(norm.as_ptr().add(p) as *const __m512i), zero);
            // ⌊√n⌋ digit by digit: `bit` runs over the powers of four from 2^62 down; where
            // the remainder holds `s + bit`, it is subtracted and the digit is set.
            // `s + bit` stays below 2^64 (before each step `s ≤ 2√(n·bit)`, with n < 2^63
            // and `bit ≤ 2^60` once `s` is nonzero), and the remainder never goes negative.
            let mut rem = nv;
            let mut s = zero;
            for d in 0..32 {
                let bit = _mm512_set1_epi64(1i64 << (62 - 2 * d));
                let t = _mm512_add_epi64(s, bit);
                let take = _mm512_cmpge_epu64_mask(rem, t);
                rem = _mm512_mask_sub_epi64(rem, take, rem, t);
                s = _mm512_srli_epi64::<1>(s);
                s = _mm512_mask_add_epi64(s, take, s, bit);
            }
            // s² ≤ n, and (s + 1)² > n wherever s + 1 has a square in u64
            let s1 = _mm512_add_epi64(s, one);
            let low = _mm512_cmple_epu64_mask(_mm512_mullo_epi64(s, s), nv);
            let high = _mm512_cmpgt_epu64_mask(_mm512_mullo_epi64(s1, s1), nv) | !_mm512_cmplt_epu64_mask(s, cap);
            if low & high != 0xff {
                break;
            }
            let xv = _mm512_loadu_si512(x.as_ptr().add(p) as *const __m512i);
            _mm512_storeu_si512(dst.as_mut_ptr().add(p) as *mut __m512i, _mm512_sra_epi64(_mm512_mullo_epi64(xv, s), cnt));
            p += 8;
        }
    }
    p
}
