// 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.

use std::fs;
use std::path::Path;

const BLOB_MAGIC_NUMBER_BYTES: [u8; 4] = 1481511375_i32.to_le_bytes();
const BLOB_ENTRY_OVERHEAD: usize = 16;
const BLOB_FORMAT_VERSION: u8 = 1;

#[derive(Clone, Copy)]
pub(crate) enum BlobFixtureValue<'a> {
    Value(&'a [u8]),
    Null,
    Placeholder,
}

pub(crate) fn build_blob_file_bytes(rows: &[Option<&[u8]>]) -> Vec<u8> {
    let values = rows
        .iter()
        .map(|row| match row {
            Some(payload) => BlobFixtureValue::Value(payload),
            None => BlobFixtureValue::Null,
        })
        .collect::<Vec<_>>();
    build_blob_file_bytes_with_values(&values)
}

pub(crate) fn build_blob_file_bytes_with_values(rows: &[BlobFixtureValue<'_>]) -> Vec<u8> {
    let mut file_bytes = Vec::new();
    let mut lengths = Vec::with_capacity(rows.len());

    for row in rows {
        match row {
            BlobFixtureValue::Value(payload) => {
                let entry_length = payload
                    .len()
                    .checked_add(BLOB_ENTRY_OVERHEAD)
                    .and_then(|len| i64::try_from(len).ok())
                    .unwrap_or_else(|| {
                        panic!("Blob payload length {} exceeds test helper limits", payload.len())
                    });
                lengths.push(entry_length);

                file_bytes.extend_from_slice(&BLOB_MAGIC_NUMBER_BYTES);
                file_bytes.extend_from_slice(payload);
                let mut hasher = crc32fast::Hasher::new();
                hasher.update(&BLOB_MAGIC_NUMBER_BYTES);
                hasher.update(payload);

                let entry_length_bytes = entry_length.to_le_bytes();
                file_bytes.extend_from_slice(&entry_length_bytes);
                hasher.update(&entry_length_bytes);
                file_bytes.extend_from_slice(&hasher.finalize().to_le_bytes());
            }
            BlobFixtureValue::Null => lengths.push(-1),
            BlobFixtureValue::Placeholder => lengths.push(-2),
        }
    }

    let index_bytes = encode_delta_varints(&lengths);
    let index_length = i32::try_from(index_bytes.len()).unwrap_or_else(|_| {
        panic!(
            "Blob index length {} exceeds test helper limits",
            index_bytes.len()
        )
    });
    file_bytes.extend_from_slice(&index_bytes);
    file_bytes.extend_from_slice(&index_length.to_le_bytes());
    file_bytes.push(BLOB_FORMAT_VERSION);
    file_bytes
}

pub(crate) fn write_blob_file(path: &Path, rows: &[Option<&[u8]>]) {
    let file_bytes = build_blob_file_bytes(rows);
    fs::write(path, file_bytes)
        .unwrap_or_else(|e| panic!("Failed to write blob test file {path:?}: {e}"));
}

pub(crate) fn write_blob_file_with_values(path: &Path, rows: &[BlobFixtureValue<'_>]) {
    let file_bytes = build_blob_file_bytes_with_values(rows);
    fs::write(path, file_bytes)
        .unwrap_or_else(|e| panic!("Failed to write blob test file {path:?}: {e}"));
}

pub(crate) fn encode_delta_varints(values: &[i64]) -> Vec<u8> {
    if values.is_empty() {
        return Vec::new();
    }

    let mut encoded = Vec::new();
    let mut previous = 0_i64;
    for (idx, value) in values.iter().copied().enumerate() {
        let delta = if idx == 0 { value } else { value - previous };
        previous = value;
        encode_varint(delta, &mut encoded);
    }
    encoded
}

fn encode_varint(value: i64, out: &mut Vec<u8>) {
    let mut remaining = ((value << 1) ^ (value >> 63)) as u64;
    while (remaining & !0x7f) != 0 {
        out.push(((remaining & 0x7f) as u8) | 0x80);
        remaining >>= 7;
    }
    out.push(remaining as u8);
}
