/* SPDX-FileCopyrightText: 2022-present Artur Drogunow <artur.drogunow@zf.com>
#
# SPDX-License-Identifier: MIT */

#define PY_SSIZE_T_CLEAN
#include <Python.h>

#include <stdbool.h>
#include <stdint.h>

#include "crclib.h"
#include "util.h"

#define P07LENGTH_POS   8u
#define P07LENGTH_LEN   4u
#define P07COUNTER_POS 12u
#define P07COUNTER_LEN  4u
#define P07DATAID_POS  16u
#define P07DATAID_LEN   4u
#define P07CRC_POS      0u
#define P07CRC_LEN      8u

#define P07HEADER_LEN (P07CRC_LEN+P07LENGTH_LEN+P07COUNTER_LEN+P07DATAID_LEN)

uint64_t compute_p07_crc(uint8_t *data_ptr, 
                         uint32_t length, 
                         uint32_t offset)
{
    uint64_t crc;

    // bytes before crc bytes
    uint32_t crc_offset = (uint32_t)(offset + P07CRC_POS);
    crc = Crc_CalculateCRC64(data_ptr, crc_offset, CRC64_INITIAL_VALUE, true);

    // bytes after crc bytes, if any
    if (offset + P07CRC_POS + P07CRC_LEN < length){
        uint32_t second_part_offset = offset + P07CRC_POS + P07CRC_LEN;
        uint8_t *second_part_ptr = data_ptr + second_part_offset;
        uint32_t second_part_len = length - (uint32_t)(offset + P07CRC_POS + P07CRC_LEN);
        crc = Crc_CalculateCRC64(second_part_ptr, second_part_len, crc, false);
    }
    return crc;
}

PyDoc_STRVAR(e2e_p07_protect_doc,
             "e2e_p07_protect(data: bytearray, length: int, data_id: int, *, offset: int = 0, increment_counter: bool = True) -> None \n"
             "Calculate CRC inplace according to AUTOSAR E2E Profile 7. \n"
             "\n"
             ":param bytearray data: \n"
             "    Mutable `bytes-like object <https://docs.python.org/3/glossary.html#term-bytes-like-object>`_.\n"
             ":param int length: \n"
             "    Number of data bytes which are considered for CRC calculation. `length` must fulfill \n"
             "    the following condition: ``20 <= length <= len(data)`` \n"
             ":param int data_id: \n"
             "    A unique identifier which is used to protect against masquerading. The `data_id` is a 32bit unsigned integer. \n"
             ":param int offset: \n"
             "    Byte offset of the E2E header. \n"
             ":param bool increment_counter: \n"
             "    If `True` the counter will be incremented before calculating the CRC. \n");

static PyObject *
py_e2e_p07_protect(PyObject *module,
                   PyObject *args,
                   PyObject *kwargs)
{
    Py_buffer data;
    unsigned long length;
    unsigned long data_id;
    unsigned long offset = 0;
    int increment = true;

    static char *kwlist[] = {
        "data",
        "length",
        "data_id",
        "offset",
        "increment_counter",
        NULL};

    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "y*kk|$kp:e2e_p07_protect",
                                     kwlist, &data, &length, &data_id, &offset, &increment))
    {
        return NULL;
    }
    if (data.readonly)
    {
        PyErr_SetString(PyExc_ValueError, "\"data\" must be mutable. Use a bytearray or any object that implements the buffer protocol.");
        goto error;
    }
    if (data.len < P07HEADER_LEN)
    {
        PyErr_SetString(PyExc_ValueError, "The length of bytearray \"data\" must be greater than or equal to 20.");
        goto error;
    }
    if (length < P07HEADER_LEN || length > data.len)
    {
        PyErr_SetString(PyExc_ValueError, "Parameter \"length\" must fulfill the following condition: 20 <= length <= len(data).");
        goto error;
    }
    if (offset > data.len - P07HEADER_LEN)
    {
        PyErr_SetString(PyExc_ValueError, "Argument \"offset\" invalid.");
        goto error;
    }

    uint8_t *data_ptr = (uint8_t *)data.buf;

    // write length
    uint32_to_bigendian(data_ptr + offset + P07LENGTH_POS, length);

    // increment counter
    if (increment) {
        uint32_t counter = bigendian_to_uint32(data_ptr + offset + P07COUNTER_POS);
        counter += 1;
        uint32_to_bigendian(data_ptr + offset + P07COUNTER_POS, counter);
    }

    // write data_id
    uint32_to_bigendian(data_ptr + offset + P07DATAID_POS, data_id);

    // calculate CRC
    uint64_t crc = compute_p07_crc(data_ptr, length, offset);
    uint64_to_bigendian(data_ptr + offset + P07CRC_POS, crc);

    PyBuffer_Release(&data);

    Py_RETURN_NONE;

error:
    PyBuffer_Release(&data);
    return NULL;
}

PyDoc_STRVAR(e2e_p07_check_doc,
             "e2e_p07_check(data: bytes, length: int, data_id: int, *, offset: int = 0) -> bool \n"
             "Return ``True`` if CRC is correct according to AUTOSAR E2E Profile 7. \n"
             "\n"
             ":param data: \n"
             "    `bytes-like object <https://docs.python.org/3/glossary.html#term-bytes-like-object>`_. \n"
             ":param length: \n"
             "    Data byte count over which the CRC must be calculated. `length` must fulfill \n"
             "    the following condition: ``20 <= length <= len(data)`` \n"
             ":param int data_id: \n"
             "    A unique identifier which is used to protect against masquerading. The `data_id` is a 32bit unsigned integer. \n"
             ":param int offset: \n"
             "    Byte offset of the E2E header. \n"
             ":return:\n"
             "    `True` if CRC is valid, otherwise return `False`");

static PyObject *
py_e2e_p07_check(PyObject *module,
                 PyObject *args,
                 PyObject *kwargs)
{
    Py_buffer data;
    unsigned long length;
    unsigned long data_id;
    unsigned long offset = 0;

    static char *kwlist[] = {
        "data",
        "length",
        "data_id",
        "offset",
        NULL};

    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "y*kk|$k:e2e_p07_check",
                                     kwlist, &data, &length, &data_id, &offset))
    {
        return NULL;
    }
    if (data.len < P07HEADER_LEN)
    {
        PyErr_SetString(PyExc_ValueError, "The length of bytearray \"data\" must be greater or equal to 20.");
        goto error;
    }
    if (length < P07HEADER_LEN || length > data.len)
    {
        PyErr_SetString(PyExc_ValueError, "Parameter \"length\" must fulfill the following condition: 20 <= length <= len(data).");
        goto error;
    }
    if (offset > data.len - P07HEADER_LEN)
    {
        PyErr_SetString(PyExc_ValueError, "Argument \"offset\" invalid.");
        goto error;
    }

    uint8_t *data_ptr = (uint8_t *)data.buf;

    // read length
    uint32_t length_actual = bigendian_to_uint32(data_ptr + offset + P07LENGTH_POS);

    // read data_id
    uint32_t data_id_actual = bigendian_to_uint32(data_ptr + offset + P07DATAID_POS);

    // read crc
    uint64_t crc_actual = bigendian_to_uint64(data_ptr + offset + P07CRC_POS);

    // calculate CRC
    uint64_t crc = compute_p07_crc(data_ptr, length, offset);

    PyBuffer_Release(&data);

    if ((length_actual == length) && (data_id_actual == data_id) && (crc_actual == crc))
    {
        Py_RETURN_TRUE;
    }
    else
    {
        Py_RETURN_FALSE;
    }

error:
    PyBuffer_Release(&data);
    return NULL;
}

static struct PyMethodDef methods[] = {
    {"e2e_p07_protect",
     (PyCFunction)py_e2e_p07_protect,
     METH_VARARGS | METH_KEYWORDS,
     e2e_p07_protect_doc},
    {"e2e_p07_check",
     (PyCFunction)py_e2e_p07_check,
     METH_VARARGS | METH_KEYWORDS,
     e2e_p07_check_doc},
    {NULL} // sentinel
};

static PyModuleDef module = {
    PyModuleDef_HEAD_INIT,
    .m_name = "e2e.p07",
    .m_doc = "",
    .m_size = -1,
    .m_methods = methods};

PyMODINIT_FUNC PyInit_p07(void)
{

    PyObject *module_p;
    module_p = PyModule_Create(&module);

    if (module_p == NULL)
        return (NULL);

    return (module_p);
}
