/**
 * ARM NEON convolution kernels.
 * Optimized for ARM64 (Apple Silicon, AWS Graviton, etc.)
 *
 * NEON provides 128-bit vectors (4 x float32) with FMA support on ARMv8.
 *
 * On Apple platforms, multi-filter convolution uses vDSP from Accelerate
 * framework for optimal performance.
 */

#if defined(__aarch64__) || defined(_M_ARM64)

#include <arm_neon.h>
#include <stdlib.h>  /* for malloc/free */
#include <stdatomic.h>  /* for thread-safe buffer management */
#include "../dispatch.h"

#ifdef __APPLE__
#include <Accelerate/Accelerate.h>
#endif

/*
 * Thread-local buffer pool for im2col to avoid repeated malloc/free.
 * This significantly improves performance when correlate is called
 * repeatedly in a loop (e.g., SGNL's ~10k calls per run).
 *
 * Uses 64-byte alignment for optimal cache line and SIMD performance.
 */
#define IM2COL_POOL_SIZE (32 * 1024 * 1024)  /* 32 MB max buffer */
#define CACHE_LINE_SIZE 64

static _Thread_local float *tl_im2col_buffer = NULL;
static _Thread_local size_t tl_im2col_capacity = 0;

/* Portable aligned allocation */
static inline float *aligned_alloc_floats(size_t num_floats) {
    size_t bytes = num_floats * sizeof(float);
    /* Round up to multiple of alignment for aligned_alloc */
    size_t aligned_bytes = ((bytes + CACHE_LINE_SIZE - 1) / CACHE_LINE_SIZE) * CACHE_LINE_SIZE;
#if defined(_WIN32)
    return (float *)_aligned_malloc(aligned_bytes, CACHE_LINE_SIZE);
#else
    return (float *)aligned_alloc(CACHE_LINE_SIZE, aligned_bytes);
#endif
}

static inline void aligned_free_floats(float *ptr) {
#if defined(_WIN32)
    _aligned_free(ptr);
#else
    free(ptr);
#endif
}

static float *get_im2col_buffer(size_t required_floats) {
    size_t required_bytes = required_floats * sizeof(float);

    /* If buffer is too small, reallocate */
    if (required_bytes > tl_im2col_capacity) {
        /* Cap at pool size to avoid excessive memory use */
        if (required_bytes > IM2COL_POOL_SIZE) {
            /* Too large for pool, caller should use malloc */
            return NULL;
        }

        aligned_free_floats(tl_im2col_buffer);
        tl_im2col_buffer = aligned_alloc_floats(required_floats);
        if (tl_im2col_buffer) {
            tl_im2col_capacity = required_bytes;
        } else {
            tl_im2col_capacity = 0;
        }
    }

    return tl_im2col_buffer;
}

/**
 * Convolution for standard (time, channels) layout using NEON.
 *
 * Vectorizes across channels: processes 4 channels per NEON operation.
 */
void convolve_neon(
    float * __restrict__ output,
    const float * __restrict__ kernel,
    const float * __restrict__ input,
    unsigned int kernel_len,
    unsigned int channels,
    unsigned int n_out)
{
    for (unsigned int s = 0; s < n_out; s++) {
        float *out_row = output + s * channels;

        /* Process 4 channels at a time with NEON */
        unsigned int ch = 0;
        for (; ch + 4 <= channels; ch += 4) {
            float32x4_t acc = vdupq_n_f32(0.0f);

            for (unsigned int k = 0; k < kernel_len; k++) {
                float32x4_t kval = vdupq_n_f32(kernel[k]);
                float32x4_t inp = vld1q_f32(&input[(s + k) * channels + ch]);
                acc = vfmaq_f32(acc, kval, inp);  /* FMA: acc += kval * inp */
            }

            vst1q_f32(&out_row[ch], acc);
        }

        /* Handle remaining channels with scalar code */
        for (; ch < channels; ch++) {
            float sum = 0.0f;
            for (unsigned int k = 0; k < kernel_len; k++) {
                sum += kernel[k] * input[(s + k) * channels + ch];
            }
            out_row[ch] = sum;
        }
    }
}

/**
 * Convolution for transposed (channels, time) layout using NEON.
 *
 * On Apple platforms, uses vDSP_conv from Accelerate framework for
 * optimal performance leveraging Apple's optimized signal processing.
 *
 * On other ARM platforms, uses heavily optimized NEON:
 * - Processes 32 output samples per iteration (8 NEON registers)
 * - Uses software prefetching for better cache utilization
 * - Kernel loop unrolled by 2 for better instruction scheduling
 */
void convolve_transposed_neon(
    float * __restrict__ output,
    const float * __restrict__ kernel,
    const float * __restrict__ input,
    unsigned int kernel_len,
    unsigned int n_out)
{
#ifdef __APPLE__
    /*
     * Use vDSP_conv for single-filter convolution.
     * vDSP_conv computes: output[n] = sum_k(input[n+k] * kernel[k])
     * which is exactly the correlation we need.
     *
     * Parameters:
     * - input: signal array
     * - 1: input stride
     * - kernel: filter array
     * - 1: filter stride (positive = no reversal = correlation)
     * - output: result array
     * - 1: output stride
     * - n_out: number of output samples
     * - kernel_len: filter length
     */
    vDSP_conv(input, 1, kernel, 1, output, 1, n_out, kernel_len);
#else
    /* Process 32 output samples at a time with 8 NEON registers */
    unsigned int s = 0;
    for (; s + 32 <= n_out; s += 32) {
        float32x4_t acc0 = vdupq_n_f32(0.0f);
        float32x4_t acc1 = vdupq_n_f32(0.0f);
        float32x4_t acc2 = vdupq_n_f32(0.0f);
        float32x4_t acc3 = vdupq_n_f32(0.0f);
        float32x4_t acc4 = vdupq_n_f32(0.0f);
        float32x4_t acc5 = vdupq_n_f32(0.0f);
        float32x4_t acc6 = vdupq_n_f32(0.0f);
        float32x4_t acc7 = vdupq_n_f32(0.0f);

        /* Prefetch ahead */
        __builtin_prefetch(&input[s + kernel_len + 64], 0, 3);
        __builtin_prefetch(&input[s + kernel_len + 128], 0, 3);

        /* Process kernel with unrolling by 2 */
        unsigned int k = 0;
        for (; k + 2 <= kernel_len; k += 2) {
            float32x4_t kval0 = vdupq_n_f32(kernel[k]);
            float32x4_t kval1 = vdupq_n_f32(kernel[k + 1]);
            const float *in_ptr0 = &input[s + k];
            const float *in_ptr1 = &input[s + k + 1];

            /* First kernel tap */
            acc0 = vfmaq_f32(acc0, kval0, vld1q_f32(in_ptr0));
            acc1 = vfmaq_f32(acc1, kval0, vld1q_f32(in_ptr0 + 4));
            acc2 = vfmaq_f32(acc2, kval0, vld1q_f32(in_ptr0 + 8));
            acc3 = vfmaq_f32(acc3, kval0, vld1q_f32(in_ptr0 + 12));
            acc4 = vfmaq_f32(acc4, kval0, vld1q_f32(in_ptr0 + 16));
            acc5 = vfmaq_f32(acc5, kval0, vld1q_f32(in_ptr0 + 20));
            acc6 = vfmaq_f32(acc6, kval0, vld1q_f32(in_ptr0 + 24));
            acc7 = vfmaq_f32(acc7, kval0, vld1q_f32(in_ptr0 + 28));

            /* Second kernel tap */
            acc0 = vfmaq_f32(acc0, kval1, vld1q_f32(in_ptr1));
            acc1 = vfmaq_f32(acc1, kval1, vld1q_f32(in_ptr1 + 4));
            acc2 = vfmaq_f32(acc2, kval1, vld1q_f32(in_ptr1 + 8));
            acc3 = vfmaq_f32(acc3, kval1, vld1q_f32(in_ptr1 + 12));
            acc4 = vfmaq_f32(acc4, kval1, vld1q_f32(in_ptr1 + 16));
            acc5 = vfmaq_f32(acc5, kval1, vld1q_f32(in_ptr1 + 20));
            acc6 = vfmaq_f32(acc6, kval1, vld1q_f32(in_ptr1 + 24));
            acc7 = vfmaq_f32(acc7, kval1, vld1q_f32(in_ptr1 + 28));
        }

        /* Handle odd kernel length */
        for (; k < kernel_len; k++) {
            float32x4_t kval = vdupq_n_f32(kernel[k]);
            const float *in_ptr = &input[s + k];
            acc0 = vfmaq_f32(acc0, kval, vld1q_f32(in_ptr));
            acc1 = vfmaq_f32(acc1, kval, vld1q_f32(in_ptr + 4));
            acc2 = vfmaq_f32(acc2, kval, vld1q_f32(in_ptr + 8));
            acc3 = vfmaq_f32(acc3, kval, vld1q_f32(in_ptr + 12));
            acc4 = vfmaq_f32(acc4, kval, vld1q_f32(in_ptr + 16));
            acc5 = vfmaq_f32(acc5, kval, vld1q_f32(in_ptr + 20));
            acc6 = vfmaq_f32(acc6, kval, vld1q_f32(in_ptr + 24));
            acc7 = vfmaq_f32(acc7, kval, vld1q_f32(in_ptr + 28));
        }

        vst1q_f32(&output[s], acc0);
        vst1q_f32(&output[s + 4], acc1);
        vst1q_f32(&output[s + 8], acc2);
        vst1q_f32(&output[s + 12], acc3);
        vst1q_f32(&output[s + 16], acc4);
        vst1q_f32(&output[s + 20], acc5);
        vst1q_f32(&output[s + 24], acc6);
        vst1q_f32(&output[s + 28], acc7);
    }

    /* Process 16 output samples at a time */
    for (; s + 16 <= n_out; s += 16) {
        float32x4_t acc0 = vdupq_n_f32(0.0f);
        float32x4_t acc1 = vdupq_n_f32(0.0f);
        float32x4_t acc2 = vdupq_n_f32(0.0f);
        float32x4_t acc3 = vdupq_n_f32(0.0f);

        for (unsigned int k = 0; k < kernel_len; k++) {
            float32x4_t kval = vdupq_n_f32(kernel[k]);
            const float *in_ptr = &input[s + k];
            acc0 = vfmaq_f32(acc0, kval, vld1q_f32(in_ptr));
            acc1 = vfmaq_f32(acc1, kval, vld1q_f32(in_ptr + 4));
            acc2 = vfmaq_f32(acc2, kval, vld1q_f32(in_ptr + 8));
            acc3 = vfmaq_f32(acc3, kval, vld1q_f32(in_ptr + 12));
        }

        vst1q_f32(&output[s], acc0);
        vst1q_f32(&output[s + 4], acc1);
        vst1q_f32(&output[s + 8], acc2);
        vst1q_f32(&output[s + 12], acc3);
    }

    /* Process 4 output samples at a time for remainder */
    for (; s + 4 <= n_out; s += 4) {
        float32x4_t acc = vdupq_n_f32(0.0f);

        for (unsigned int k = 0; k < kernel_len; k++) {
            float32x4_t kval = vdupq_n_f32(kernel[k]);
            float32x4_t inp = vld1q_f32(&input[s + k]);
            acc = vfmaq_f32(acc, kval, inp);
        }

        vst1q_f32(&output[s], acc);
    }

    /* Handle remaining samples with scalar code */
    for (; s < n_out; s++) {
        float sum = 0.0f;
        for (unsigned int k = 0; k < kernel_len; k++) {
            sum += kernel[k] * input[s + k];
        }
        output[s] = sum;
    }
#endif  /* __APPLE__ */
}

/**
 * Multi-filter convolution for groups=1 case.
 *
 * Applies multiple filters to a single input channel.
 *
 * On Apple platforms, uses im2col + cblas_sgemm for optimal performance.
 * This leverages Apple's AMX (Apple Matrix coprocessor) for matrix multiplication.
 *
 * On other ARM platforms, uses NEON intrinsics.
 *
 * filters: (n_filters, filter_len) - row-major
 * input: (n_samples,) - single channel
 * output: (n_filters, n_out) - row-major
 */
/*
 * Direct NEON convolution - processes multiple filters simultaneously.
 *
 * NOTE: For groups=1 (standard convolution), BLAS SGEMM with Apple's AMX
 * coprocessor is ~10x faster than direct NEON. This function is kept for
 * potential future use with depthwise convolution or smaller problem sizes.
 *
 * For each output position, we load input once and apply it to 4 filters.
 */
static void convolve_multi_filter_direct_neon(
    float * __restrict__ output,
    const float * __restrict__ filters,
    const float * __restrict__ input,
    unsigned int n_filters,
    unsigned int filter_len,
    unsigned int n_out)
{
    /* Process each filter, using NEON for 4 output positions at a time */
    for (unsigned int f = 0; f < n_filters; f++) {
        const float *filter = &filters[f * filter_len];
        float *out_row = &output[f * n_out];

        unsigned int s = 0;
        for (; s + 4 <= n_out; s += 4) {
            float32x4_t acc = vdupq_n_f32(0.0f);

            for (unsigned int k = 0; k < filter_len; k++) {
                float32x4_t in_vec = vld1q_f32(&input[s + k]);
                acc = vfmaq_n_f32(acc, in_vec, filter[k]);
            }

            vst1q_f32(&out_row[s], acc);
        }

        /* Handle remaining samples */
        for (; s < n_out; s++) {
            float sum = 0.0f;
            for (unsigned int k = 0; k < filter_len; k++) {
                sum += filter[k] * input[s + k];
            }
            out_row[s] = sum;
        }
    }
}

void convolve_multi_filter_neon(
    float * __restrict__ output,
    const float * __restrict__ filters,
    const float * __restrict__ input,
    unsigned int n_filters,
    unsigned int filter_len,
    unsigned int n_out)
{
#ifdef __APPLE__
    /*
     * Strategy selection based on problem size:
     * - For smaller problems, direct NEON avoids im2col overhead
     * - For larger problems, BLAS SGEMM uses AMX coprocessor
     *
     * The crossover point depends on n_filters * filter_len * n_out.
     * AMX excels at large matrix multiplications.
     */
    const size_t total_ops = (size_t)n_filters * filter_len * n_out;

    /*
     * Use direct NEON for smaller/medium problems to avoid im2col overhead.
     * Testing shows direct NEON is faster up to ~500M ops on Apple M-series.
     * Above that, AMX SGEMM's matrix coprocessor wins.
     */
    if (0) {  /* Direct NEON disabled for groups=1 - BLAS AMX is 10x faster */
        convolve_multi_filter_direct_neon(output, filters, input, n_filters, filter_len, n_out);
        return;
    }

    /*
     * Use im2col + cblas_sgemm for large problems.
     * This leverages Apple's AMX coprocessor for matrix multiplication.
     */
    const size_t col_size = (size_t)filter_len * n_out;
    float *col_matrix;
    /* Aligned stack buffer for small allocations - 64-byte aligned for cache line */
    float stack_buffer[8192] __attribute__((aligned(CACHE_LINE_SIZE)));
    int used_heap_malloc = 0;

    if (col_size <= 8192) {
        /* Small buffer: use aligned stack */
        col_matrix = stack_buffer;
    } else {
        /* Try thread-local pool first (already aligned) */
        col_matrix = get_im2col_buffer(col_size);
        if (!col_matrix) {
            /* Pool too small or failed, use aligned heap allocation */
            col_matrix = aligned_alloc_floats(col_size);
            used_heap_malloc = 1;
            if (!col_matrix) {
                /* Fallback to direct NEON if allocation fails */
                convolve_multi_filter_direct_neon(output, filters, input, n_filters, filter_len, n_out);
                return;
            }
        }
    }

    /*
     * Build im2col matrix using NEON for fast strided copy.
     * Layout: col_matrix[k * n_out + s] = input[s + k]
     */
    for (unsigned int k = 0; k < filter_len; k++) {
        const float *src = &input[k];
        float *dst = &col_matrix[k * n_out];

        unsigned int s = 0;
        for (; s + 16 <= n_out; s += 16) {
            vst1q_f32(&dst[s], vld1q_f32(&src[s]));
            vst1q_f32(&dst[s + 4], vld1q_f32(&src[s + 4]));
            vst1q_f32(&dst[s + 8], vld1q_f32(&src[s + 8]));
            vst1q_f32(&dst[s + 12], vld1q_f32(&src[s + 12]));
        }
        for (; s + 4 <= n_out; s += 4) {
            vst1q_f32(&dst[s], vld1q_f32(&src[s]));
        }
        for (; s < n_out; s++) {
            dst[s] = src[s];
        }
    }

    /*
     * SGEMM: output = filters × col_matrix
     */
    cblas_sgemm(
        CblasRowMajor,
        CblasNoTrans,
        CblasNoTrans,
        (int)n_filters,
        (int)n_out,
        (int)filter_len,
        1.0f,
        filters,
        (int)filter_len,
        col_matrix,
        (int)n_out,
        0.0f,
        output,
        (int)n_out
    );

    if (used_heap_malloc) {
        aligned_free_floats(col_matrix);
    }
#else
    /* NEON implementation for non-Apple ARM platforms (AWS Graviton, etc.) */

    /* Process each filter independently */
    for (unsigned int f = 0; f < n_filters; f++) {
        const float *filter = &filters[f * filter_len];
        float *out_row = &output[f * n_out];

        /* Process 16 output samples at a time */
        unsigned int s = 0;
        for (; s + 16 <= n_out; s += 16) {
            float32x4_t acc0 = vdupq_n_f32(0.0f);
            float32x4_t acc1 = vdupq_n_f32(0.0f);
            float32x4_t acc2 = vdupq_n_f32(0.0f);
            float32x4_t acc3 = vdupq_n_f32(0.0f);

            for (unsigned int k = 0; k < filter_len; k++) {
                float32x4_t kval = vdupq_n_f32(filter[k]);
                const float *in_ptr = &input[s + k];

                acc0 = vfmaq_f32(acc0, kval, vld1q_f32(in_ptr));
                acc1 = vfmaq_f32(acc1, kval, vld1q_f32(in_ptr + 4));
                acc2 = vfmaq_f32(acc2, kval, vld1q_f32(in_ptr + 8));
                acc3 = vfmaq_f32(acc3, kval, vld1q_f32(in_ptr + 12));
            }

            vst1q_f32(&out_row[s], acc0);
            vst1q_f32(&out_row[s + 4], acc1);
            vst1q_f32(&out_row[s + 8], acc2);
            vst1q_f32(&out_row[s + 12], acc3);
        }

        /* Process 4 output samples at a time */
        for (; s + 4 <= n_out; s += 4) {
            float32x4_t acc = vdupq_n_f32(0.0f);

            for (unsigned int k = 0; k < filter_len; k++) {
                float32x4_t kval = vdupq_n_f32(filter[k]);
                float32x4_t inp = vld1q_f32(&input[s + k]);
                acc = vfmaq_f32(acc, kval, inp);
            }

            vst1q_f32(&out_row[s], acc);
        }

        /* Handle remaining samples */
        for (; s < n_out; s++) {
            float sum = 0.0f;
            for (unsigned int k = 0; k < filter_len; k++) {
                sum += filter[k] * input[s + k];
            }
            out_row[s] = sum;
        }
    }
#endif
}

/**
 * Polyphase upsampling for transposed layout using NEON.
 * 128-bit (4 floats) with FMA.
 */
void upsample_polyphase_neon(
    float * __restrict__ output,
    const float * __restrict__ kernel_T,
    const float * __restrict__ input,
    unsigned int kernel_len,
    unsigned int factor,
    unsigned int n_valid)
{
    if (factor == 8) {
        /* factor=8: 8 phases = 2 x float32x4_t per position, 4 positions for ILP */
        unsigned int s = 0;
        for (; s + 4 <= n_valid; s += 4) {
            float32x4_t a0_lo = vdupq_n_f32(0), a0_hi = vdupq_n_f32(0);
            float32x4_t a1_lo = vdupq_n_f32(0), a1_hi = vdupq_n_f32(0);
            float32x4_t a2_lo = vdupq_n_f32(0), a2_hi = vdupq_n_f32(0);
            float32x4_t a3_lo = vdupq_n_f32(0), a3_hi = vdupq_n_f32(0);

            for (unsigned int k = 0; k < kernel_len; k++) {
                float32x4_t kern_lo = vld1q_f32(&kernel_T[k * 8]);
                float32x4_t kern_hi = vld1q_f32(&kernel_T[k * 8 + 4]);
                a0_lo = vfmaq_n_f32(a0_lo, kern_lo, input[s + 0 + k]);
                a0_hi = vfmaq_n_f32(a0_hi, kern_hi, input[s + 0 + k]);
                a1_lo = vfmaq_n_f32(a1_lo, kern_lo, input[s + 1 + k]);
                a1_hi = vfmaq_n_f32(a1_hi, kern_hi, input[s + 1 + k]);
                a2_lo = vfmaq_n_f32(a2_lo, kern_lo, input[s + 2 + k]);
                a2_hi = vfmaq_n_f32(a2_hi, kern_hi, input[s + 2 + k]);
                a3_lo = vfmaq_n_f32(a3_lo, kern_lo, input[s + 3 + k]);
                a3_hi = vfmaq_n_f32(a3_hi, kern_hi, input[s + 3 + k]);
            }

            vst1q_f32(&output[s * 8 +  0], a0_lo);
            vst1q_f32(&output[s * 8 +  4], a0_hi);
            vst1q_f32(&output[s * 8 +  8], a1_lo);
            vst1q_f32(&output[s * 8 + 12], a1_hi);
            vst1q_f32(&output[s * 8 + 16], a2_lo);
            vst1q_f32(&output[s * 8 + 20], a2_hi);
            vst1q_f32(&output[s * 8 + 24], a3_lo);
            vst1q_f32(&output[s * 8 + 28], a3_hi);
        }

        for (; s < n_valid; s++) {
            float32x4_t acc_lo = vdupq_n_f32(0), acc_hi = vdupq_n_f32(0);
            for (unsigned int k = 0; k < kernel_len; k++) {
                acc_lo = vfmaq_n_f32(acc_lo, vld1q_f32(&kernel_T[k * 8]), input[s + k]);
                acc_hi = vfmaq_n_f32(acc_hi, vld1q_f32(&kernel_T[k * 8 + 4]), input[s + k]);
            }
            vst1q_f32(&output[s * 8], acc_lo);
            vst1q_f32(&output[s * 8 + 4], acc_hi);
        }
    } else if (factor == 2) {
        /* factor=2: process 4 positions per phase, then interleave with vzip */
        unsigned int s = 0;
        for (; s + 4 <= n_valid; s += 4) {
            float32x4_t acc_p0 = vdupq_n_f32(0);
            float32x4_t acc_p1 = vdupq_n_f32(0);

            for (unsigned int k = 0; k < kernel_len; k++) {
                float32x4_t in_val = vld1q_f32(&input[s + k]);
                acc_p0 = vfmaq_n_f32(acc_p0, in_val, kernel_T[k * 2 + 0]);
                acc_p1 = vfmaq_n_f32(acc_p1, in_val, kernel_T[k * 2 + 1]);
            }

            /* Interleave: zip produces [a0,b0,a1,b1] and [a2,b2,a3,b3] */
            float32x4x2_t zipped = vzipq_f32(acc_p0, acc_p1);
            vst1q_f32(&output[s * 2 + 0], zipped.val[0]);
            vst1q_f32(&output[s * 2 + 4], zipped.val[1]);
        }

        for (; s < n_valid; s++) {
            float acc0 = 0.0f, acc1 = 0.0f;
            for (unsigned int k = 0; k < kernel_len; k++) {
                float in_val = input[s + k];
                acc0 += in_val * kernel_T[k * 2 + 0];
                acc1 += in_val * kernel_T[k * 2 + 1];
            }
            output[s * 2 + 0] = acc0;
            output[s * 2 + 1] = acc1;
        }
    } else if (factor == 4) {
        /* factor=4: 4 phases = 1 x float32x4_t per position */
        unsigned int s = 0;
        for (; s + 4 <= n_valid; s += 4) {
            float32x4_t acc0 = vdupq_n_f32(0);
            float32x4_t acc1 = vdupq_n_f32(0);
            float32x4_t acc2 = vdupq_n_f32(0);
            float32x4_t acc3 = vdupq_n_f32(0);

            for (unsigned int k = 0; k < kernel_len; k++) {
                float32x4_t kern = vld1q_f32(&kernel_T[k * 4]);
                acc0 = vfmaq_n_f32(acc0, kern, input[s + 0 + k]);
                acc1 = vfmaq_n_f32(acc1, kern, input[s + 1 + k]);
                acc2 = vfmaq_n_f32(acc2, kern, input[s + 2 + k]);
                acc3 = vfmaq_n_f32(acc3, kern, input[s + 3 + k]);
            }

            vst1q_f32(&output[s * 4 +  0], acc0);
            vst1q_f32(&output[s * 4 +  4], acc1);
            vst1q_f32(&output[s * 4 +  8], acc2);
            vst1q_f32(&output[s * 4 + 12], acc3);
        }

        for (; s < n_valid; s++) {
            float32x4_t acc = vdupq_n_f32(0);
            for (unsigned int k = 0; k < kernel_len; k++) {
                acc = vfmaq_n_f32(acc, vld1q_f32(&kernel_T[k * 4]), input[s + k]);
            }
            vst1q_f32(&output[s * 4], acc);
        }
    } else {
        /* General fallback */
        for (unsigned int s = 0; s < n_valid; s++) {
            float *out_ptr = output + s * factor;
            unsigned int p = 0;
            for (; p + 4 <= factor; p += 4)
                vst1q_f32(&out_ptr[p], vdupq_n_f32(0));
            for (; p < factor; p++)
                out_ptr[p] = 0.0f;
            for (unsigned int k = 0; k < kernel_len; k++) {
                float in_val = input[s + k];
                const float *kr = kernel_T + k * factor;
                p = 0;
                for (; p + 4 <= factor; p += 4) {
                    float32x4_t a = vld1q_f32(&out_ptr[p]);
                    a = vfmaq_n_f32(a, vld1q_f32(&kr[p]), in_val);
                    vst1q_f32(&out_ptr[p], a);
                }
                for (; p < factor; p++)
                    out_ptr[p] += in_val * kr[p];
            }
        }
    }
}

#endif /* ARM64 */
