import re
import time
import warnings
from collections.abc import Iterator
from typing import (
    TYPE_CHECKING,
    TypeVar,
    cast,
)

from cantools import __version__
from cantools.database.can.message import SignalTreeMuxElemType, SignalTreeType

if TYPE_CHECKING:
    from cantools.database.can import Database, Message, Signal


_T1 = TypeVar("_T1")
_T2 = TypeVar("_T2")
THelperKind = tuple[str, int]


HEADER_FMT = '''\
/**
 * @file {file_name}
 *
 * @brief This header file was generated by cantools version {version} {date}.
 *
 * @copyright Copyright (c) 2018-2019 Erik Moqvist
 *
 * @par License
 * The MIT License (MIT)
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use, copy,
 * modify, merge, publish, distribute, sublicense, and/or sell copies
 * of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#ifndef {include_guard}
#define {include_guard}

#ifdef __cplusplus
extern "C" {{
#endif

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

#ifndef EINVAL
#    define EINVAL 22
#endif

/* Frame ids. */
{frame_id_defines}

/* Frame lengths in bytes. */
{frame_length_defines}

/* Extended or standard frame types. */
{is_extended_frame_defines}

/* Frame cycle times in milliseconds. */
{frame_cycle_time_defines}

/* Signal choices. */
{choices_defines}

/* Frame Names. */
{frame_name_macros}

/* Signal Names. */
{signal_name_macros}

{structs}
{declarations}

#ifdef __cplusplus
}}
#endif

#endif
'''

SOURCE_FMT = '''\
/**
 * @file {file_name}
 *
 * @brief This source file was generated by cantools version {version} {date}.
 *
 * @copyright Copyright (c) 2018-2019 Erik Moqvist
 *
 * @par License
 * The MIT License (MIT)
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use, copy,
 * modify, merge, publish, distribute, sublicense, and/or sell copies
 * of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

{includes}\
#include <string.h>

#include "{header}"

{helpers}\
{definitions}\
'''

FUZZER_SOURCE_FMT = '''\
/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2018-2019 Erik Moqvist
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use, copy,
 * modify, merge, publish, distribute, sublicense, and/or sell copies
 * of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

/**
 * This file was generated by cantools version {version} {date}.
 */

#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#include <stdio.h>

#include "{header}"

static void assert_first_pack(int res)
{{
    if (res < 0) {{
        printf("First pack failed with %ld.\\n", res);
        __builtin_trap();
    }}
}}

static void assert_second_unpack(int res)
{{
    if (res < 0) {{
        printf("Second unpack failed with %ld.\\n", res);
        __builtin_trap();
    }}
}}

static void assert_second_unpack_data(const void *unpacked_p,
                                      const void *unpacked2_p,
                                      size_t size)
{{
    if (memcmp(unpacked_p, unpacked2_p, size) != 0) {{
        printf("Second unpacked data does not match first unpacked data.\\n");
        __builtin_trap();
    }}
}}

static void assert_second_pack(int res, int res2)
{{
    if (res != res2) {{
        printf("Second pack result %ld does not match first pack "
               "result %ld.\\n",
               res,
               res2);
        __builtin_trap();
    }}
}}

static void assert_second_pack_data(const uint8_t *packed_p,
                                    const uint8_t *packed2_p,
                                    int size)
{{
    int i;

    if (memcmp(packed_p, packed2_p, size) != 0) {{
        for (i = 0; i < size; i++) {{
            printf("[%04ld]: 0x%02x 0x%02x\\n", i, packed_p[i], packed2_p[i]);
        }}

        __builtin_trap();
    }}
}}

{tests}

int LLVMFuzzerTestOneInput(const uint8_t *data_p, size_t size)
{{
{llvm_body}

    return (0);
}}
'''

FUZZER_MAKEFILE_FMT = '''\
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2019 Erik Moqvist
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use, copy,
# modify, merge, publish, distribute, sublicense, and/or sell copies
# of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#

#
# This file was generated by cantools version {version} {date}.
#

CC = clang
EXE = fuzzer
C_SOURCES = \\
\t{source} \\
\t{fuzzer_source}
CFLAGS = \\
\t-fprofile-instr-generate \\
\t-fcoverage-mapping \\
\t-I. \\
\t-g -fsanitize=address,fuzzer \\
\t-fsanitize=signed-integer-overflow \\
\t-fno-sanitize-recover=all
EXECUTION_TIME ?= 5

all:
\t$(CC) $(CFLAGS) $(C_SOURCES) -o $(EXE)
\trm -f $(EXE).profraw
\tLLVM_PROFILE_FILE="$(EXE).profraw" \\
\t    ./$(EXE) \\
\t    -max_total_time=$(EXECUTION_TIME)
\tllvm-profdata merge -sparse $(EXE).profraw -o $(EXE).profdata
\tllvm-cov show ./$(EXE) -instr-profile=$(EXE).profdata
\tllvm-cov report ./$(EXE) -instr-profile=$(EXE).profdata

'''

TEST_FMT = '''
static void test_{name}(
    const uint8_t *packed_p,
    size_t size)
{{
    int res;
    int res2;
    uint8_t packed[size];
    uint8_t packed2[size];
    struct {name}_t unpacked;
    struct {name}_t unpacked2;

    memset(&unpacked, 0, sizeof(unpacked));

    res = {name}_unpack(
        &unpacked,
        packed_p,
        size);

    if (res >= 0) {{
        res = {name}_pack(
            &packed[0],
            &unpacked,
            sizeof(packed));

        assert_first_pack(res);

        memset(&unpacked2, 0, sizeof(unpacked2));

        res2 = {name}_unpack(
            &unpacked2,
            &packed[0],
            res);

        assert_second_unpack(res2);
        assert_second_unpack_data(&unpacked,
                                  &unpacked2,
                                  sizeof(unpacked));

        res2 = {name}_pack(
            &packed2[0],
            &unpacked,
            sizeof(packed2));

        assert_second_pack(res, res2);
        assert_second_pack_data(&packed[0], &packed2[0], res);
    }}
}}\
'''

STRUCT_FMT = '''\
/**
 * Signals in message {database_message_name}.
 *
{comment}\
 * All signal values are as on the CAN bus.
 */
struct {database_name}_{message_name}_t {{
{members}
}};
'''

DECLARATION_PACK_FMT = '''\
/**
 * Pack message {database_message_name}.
 *
 * @param[out] dst_p Buffer to pack the message into.
 * @param[in] src_p Data to pack.
 * @param[in] size Size of dst_p.
 *
 * @return Size of packed data, or negative error code.
 */
int {database_name}_{message_name}_pack(
    uint8_t *dst_p,
    const struct {database_name}_{message_name}_t *src_p,
    size_t size);

'''

DECLARATION_UNPACK_FMT = '''\
/**
 * Unpack message {database_message_name}.
 *
 * @param[out] dst_p Object to unpack the message into.
 * @param[in] src_p Message to unpack.
 * @param[in] size Size of src_p.
 *
 * @return zero(0) or negative error code.
 */
int {database_name}_{message_name}_unpack(
    struct {database_name}_{message_name}_t *dst_p,
    const uint8_t *src_p,
    size_t size);

'''

SIGNAL_DECLARATION_ENCODE_FMT = '''\
/**
 * Encode given signal by applying scaling and offset.
 *
 * @param[in] value Signal to encode.
 *
 * @return Encoded signal.
 */
{type_name} {database_name}_{message_name}_{signal_name}_encode({floating_point_type} value);

'''

SIGNAL_DECLARATION_DECODE_FMT = '''\
/**
 * Decode given signal by applying scaling and offset.
 *
 * @param[in] value Signal to decode.
 *
 * @return Decoded signal.
 */
{floating_point_type} {database_name}_{message_name}_{signal_name}_decode({type_name} value);

'''

SIGNAL_DECLARATION_IS_IN_RANGE_FMT = '''\
/**
 * Check that given signal is in allowed range.
 *
 * @param[in] value Signal to check.
 *
 * @return true if in range, false otherwise.
 */
bool {database_name}_{message_name}_{signal_name}_is_in_range({type_name} value);
'''

SIGNAL_DECLARATION_IS_IN_PHYS_RANGE_FMT = '''\

/**
 * Check that given physical value is in allowed range before encoding.
 *
 * Use this BEFORE calling _encode() to avoid silent integer overflow:
 *   encode(-1.0) on a [0,100] signal wraps to 255 and passes is_in_range().
 *   is_in_phys_range(-1.0) correctly returns false.
 *
 * @param[in] value Physical signal value to check.
 *
 * @return true if in range, false otherwise.
 */
bool {database_name}_{message_name}_{signal_name}_is_in_phys_range({floating_point_type} value);
'''

MESSAGE_DECLARATION_INIT_FMT = '''\
/**
 * Init message fields to default values from {database_message_name}.
 *
 * @param[in] msg_p Message to init.
 *
 * @return zero(0) on success or (-1) in case of nullptr argument.
 */
int {database_name}_{message_name}_init(struct {database_name}_{message_name}_t *msg_p);
'''

MESSAGE_DEFINITION_INIT_FMT = '''\
int {database_name}_{message_name}_init(struct {database_name}_{message_name}_t *msg_p)
{{
    if (msg_p == NULL) return -1;

    memset(msg_p, 0, sizeof(struct {database_name}_{message_name}_t));
{init_body}
    return 0;
}}
'''

PACK_HELPER_LEFT_SHIFT_FMT = '''\
static inline uint8_t pack_left_shift_u{length}(
    {var_type} value,
    uint8_t shift,
    uint8_t mask)
{{
    return (uint8_t)((uint8_t)(value << shift) & mask);
}}
'''

PACK_HELPER_RIGHT_SHIFT_FMT = '''\
static inline uint8_t pack_right_shift_u{length}(
    {var_type} value,
    uint8_t shift,
    uint8_t mask)
{{
    return (uint8_t)((uint8_t)(value >> shift) & mask);
}}
'''

UNPACK_HELPER_LEFT_SHIFT_FMT = '''\
static inline {var_type} unpack_left_shift_u{length}(
    uint8_t value,
    uint8_t shift,
    uint8_t mask)
{{
    return ({var_type})(({var_type})(value & mask) << shift);
}}
'''

UNPACK_HELPER_RIGHT_SHIFT_FMT = '''\
static inline {var_type} unpack_right_shift_u{length}(
    uint8_t value,
    uint8_t shift,
    uint8_t mask)
{{
    return ({var_type})(({var_type})(value & mask) >> shift);
}}
'''

DEFINITION_PACK_FMT = '''\
int {database_name}_{message_name}_pack(
    uint8_t *dst_p,
    const struct {database_name}_{message_name}_t *src_p,
    size_t size)
{{
{pack_unused}\
{pack_variables}\
    if (size < {message_length}u) {{
        return (-EINVAL);
    }}

    memset(&dst_p[0], 0, {message_length});
{pack_body}
    return ({message_length});
}}

'''

DEFINITION_UNPACK_FMT = '''\
int {database_name}_{message_name}_unpack(
    struct {database_name}_{message_name}_t *dst_p,
    const uint8_t *src_p,
    size_t size)
{{
{unpack_unused}\
{unpack_variables}\
    if (size < {message_length}u) {{
        return (-EINVAL);
    }}
{unpack_body}
    return (0);
}}

'''

SIGNAL_DEFINITION_ENCODE_FMT = '''\
{type_name} {database_name}_{message_name}_{signal_name}_encode({floating_point_type} value)
{{
    return ({type_name})({encode});
}}

'''

SIGNAL_DEFINITION_DECODE_FMT = '''\
{floating_point_type} {database_name}_{message_name}_{signal_name}_decode({type_name} value)
{{
    return ({decode});
}}

'''

SIGNAL_DEFINITION_IS_IN_RANGE_FMT = '''\
bool {database_name}_{message_name}_{signal_name}_is_in_range({type_name} value)
{{
{unused}\
    return ({check});
}}
'''

SIGNAL_DEFINITION_IS_IN_PHYS_RANGE_FMT = '''\

bool {database_name}_{message_name}_{signal_name}_is_in_phys_range({floating_point_type} value)
{{
{phys_unused}\
    return ({phys_check});
}}
'''

EMPTY_DEFINITION_FMT = '''\
int {database_name}_{message_name}_pack(
    uint8_t *dst_p,
    const struct {database_name}_{message_name}_t *src_p,
    size_t size)
{{
    (void)dst_p;
    (void)src_p;
    (void)size;

    return (0);
}}

int {database_name}_{message_name}_unpack(
    struct {database_name}_{message_name}_t *dst_p,
    const uint8_t *src_p,
    size_t size)
{{
    (void)dst_p;
    (void)src_p;
    (void)size;

    return (0);
}}
'''

SIGN_EXTENSION_FMT = '''
    if (({name} & (1{suffix} << {shift})) != 0{suffix}) {{
        {name} |= 0x{mask:x}{suffix};
    }}

'''

SIGNAL_MEMBER_FMT = '''\
    /**
{comment}\
     * Range: {range}
     * Scale: {scale}
     * Offset: {offset}
     */
    {type_name} {name}{length};\
'''

INIT_SIGNAL_BODY_TEMPLATE_FMT = '''\
    msg_p->{signal_name} = {signal_initial};
'''


class CodeGenSignal:

    def __init__(self, signal: "Signal") -> None:
        self.signal: Signal = signal
        self.snake_name = camel_to_snake_case(signal.name)

    @property
    def unit(self) -> str:
        return _get(self.signal.unit, '-')

    @property
    def type_length(self) -> int:
        if self.signal.length <= 8:
            return 8
        elif self.signal.length <= 16:
            return 16
        elif self.signal.length <= 32:
            return 32
        else:
            return 64

    @property
    def type_name(self) -> str:
        if self.signal.conversion.is_float:
            if self.signal.length == 32:
                type_name = 'float'
            else:
                type_name = 'double'
        else:
            type_name = f'int{self.type_length}_t'

            if not self.signal.is_signed:
                type_name = 'u' + type_name

        return type_name

    @property
    def type_suffix(self) -> str:
        try:
            return {
                'uint8_t': 'u',
                'uint16_t': 'u',
                'uint32_t': 'u',
                'int64_t': 'll',
                'uint64_t': 'ull',
                'float': 'f'
            }[self.type_name]
        except KeyError:
            return ''

    @property
    def conversion_type_suffix(self) -> str:
        try:
            return {
                8: 'u',
                16: 'u',
                32: 'u',
                64: 'ull'
            }[self.type_length]
        except KeyError:
            return ''

    @property
    def unique_choices(self) -> dict[int, str]:
        """Make duplicated choice names unique by first appending its value
        and then underscores until unique.

        """
        if self.signal.choices is None:
            return {}

        items = {
            value: camel_to_snake_case(str(name)).upper()
            for value, name in self.signal.choices.items()
        }
        names = list(items.values())
        duplicated_names = [
            name
            for name in set(names)
            if names.count(name) > 1
        ]
        unique_choices = {
            value: name
            for value, name in items.items()
            if names.count(name) == 1
        }

        for value, name in items.items():
            if name in duplicated_names:
                name += _canonical(f'_{value}')

                while name in unique_choices.values():
                    name += '_'

                unique_choices[value] = name

        return unique_choices

    @property
    def minimum_ctype_value(self) -> int | None:
        if self.type_name == 'int8_t':
            return -2**7
        elif self.type_name == 'int16_t':
            return -2**15
        elif self.type_name == 'int32_t':
            return -2**31
        elif self.type_name == 'int64_t':
            return -2**63
        elif self.type_name.startswith('u'):
            return 0
        else:
            return None

    @property
    def maximum_ctype_value(self) -> int | None:
        if self.type_name == 'int8_t':
            return 2**7 - 1
        elif self.type_name == 'int16_t':
            return 2**15 - 1
        elif self.type_name == 'int32_t':
            return 2**31 - 1
        elif self.type_name == 'int64_t':
            return 2**63 - 1
        elif self.type_name == 'uint8_t':
            return 2**8 - 1
        elif self.type_name == 'uint16_t':
            return 2**16 - 1
        elif self.type_name == 'uint32_t':
            return 2**32 - 1
        elif self.type_name == 'uint64_t':
            return 2**64 - 1
        else:
            return None

    @property
    def minimum_can_raw_value(self) -> int | None:
        if self.signal.conversion.is_float:
            return None
        elif self.signal.is_signed:
            return cast("int", -(2 ** (self.signal.length - 1)))
        else:
            return 0

    @property
    def maximum_can_raw_value(self) -> int | None:
        if self.signal.conversion.is_float:
            return None
        elif self.signal.is_signed:
            return cast("int", (2 ** (self.signal.length - 1)) - 1)
        else:
            return cast("int", (2 ** self.signal.length) - 1)

    def segments(self, invert_shift: bool) -> Iterator[tuple[int, int, str, int]]:
        index, pos = divmod(self.signal.start, 8)
        left = self.signal.length

        while left > 0:
            if self.signal.byte_order == 'big_endian':
                if left >= (pos + 1):
                    length = (pos + 1)
                    pos = 7
                    shift = -(left - length)
                    mask = ((1 << length) - 1)
                else:
                    length = left
                    shift = (pos - length + 1)
                    mask = ((1 << length) - 1)
                    mask <<= (pos - length + 1)
            else:
                shift = (left - self.signal.length) + pos

                if left >= (8 - pos):
                    length = (8 - pos)
                    mask = ((1 << length) - 1)
                    mask <<= pos
                    pos = 0
                else:
                    length = left
                    mask = ((1 << length) - 1)
                    mask <<= pos

            if invert_shift:
                if shift < 0:
                    shift = -shift
                    shift_direction = 'left'
                else:
                    shift_direction = 'right'
            elif shift < 0:
                shift = -shift
                shift_direction = 'right'
            else:
                shift_direction = 'left'

            yield index, shift, shift_direction, mask

            left -= length
            index += 1


class CodeGenMessage:

    def __init__(self, message: "Message") -> None:
        self.message = message
        self.snake_name = camel_to_snake_case(message.name)
        self.cg_signals = [CodeGenSignal(signal) for signal in message.signals]

    def get_signal_by_name(self, name: str) -> "CodeGenSignal":
        for cg_signal in self.cg_signals:
            if cg_signal.signal.name == name:
                return cg_signal
        raise KeyError(f"Signal {name} not found.")


def _canonical(value: str) -> str:
    """Replace anything but 'a-z', 'A-Z' and '0-9' with '_'.

    """

    return re.sub(r'[^a-zA-Z0-9]', '_', value)


def camel_to_snake_case(value: str) -> str:
    value = re.sub(r'(.)([A-Z][a-z]+)', r'\1_\2', value)
    value = re.sub(r'(_+)', '_', value)
    value = re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', value).lower()
    value = _canonical(value)

    return value


def _strip_blank_lines(lines: list[str]) -> list[str]:
    try:
        while lines[0] == '':
            lines = lines[1:]

        while lines[-1] == '':
            lines = lines[:-1]
    except IndexError:
        pass

    return lines


def _get(value: _T1 | None, default: _T2) -> _T1 | _T2:
    if value is None:
        return default
    return value


def _format_comment(comment: str | None) -> str:
    if comment:
        return '\n'.join([
            '     * ' + line.rstrip()
            for line in comment.splitlines()
        ]) + '\n     *\n'
    else:
        return ''


def _format_range(cg_signal: "CodeGenSignal") -> str:
    minimum = cg_signal.signal.minimum
    maximum = cg_signal.signal.maximum

    def phys_to_raw(x: float) -> int | float:
        raw_val = cg_signal.signal.scaled_to_raw(x)
        if cg_signal.signal.is_float:
            return float(raw_val)
        return round(raw_val)

    if minimum is not None and maximum is not None:
        return \
            f'{phys_to_raw(minimum)}..' \
            f'{phys_to_raw(maximum)} ' \
            f'({round(minimum, 5)}..{round(maximum, 5)} {cg_signal.unit})'
    elif minimum is not None:
        return f'{phys_to_raw(minimum)}.. ({round(minimum, 5)}.. {cg_signal.unit})'
    elif maximum is not None:
        return f'..{phys_to_raw(maximum)} (..{round(maximum, 5)} {cg_signal.unit})'
    else:
        return '-'


def _generate_signal(cg_signal: "CodeGenSignal", bit_fields: bool) -> str:
    comment = _format_comment(cg_signal.signal.comment)
    range_ = _format_range(cg_signal)
    scale = _get(cg_signal.signal.conversion.scale, '-')
    offset = _get(cg_signal.signal.conversion.offset, '-')

    if cg_signal.signal.conversion.is_float or not bit_fields:
        length = ''
    else:
        length = f' : {cg_signal.signal.length}'

    member = SIGNAL_MEMBER_FMT.format(comment=comment,
                                      range=range_,
                                      scale=scale,
                                      offset=offset,
                                      type_name=cg_signal.type_name,
                                      name=cg_signal.snake_name,
                                      length=length)

    return member


def _format_pack_code_mux(cg_message: "CodeGenMessage",
                          mux: SignalTreeMuxElemType,
                          body_lines_per_index: list[str],
                          variable_lines: list[str],
                          helper_kinds: set[THelperKind]) -> list[str]:
    signal_name, multiplexed_signals = next(iter(mux.items()))
    _format_pack_code_signal(cg_message,
                             signal_name,
                             body_lines_per_index,
                             variable_lines,
                             helper_kinds)
    multiplexed_signals_per_id = sorted(multiplexed_signals.items())
    signal_name = camel_to_snake_case(signal_name)

    lines = [
        '',
        f'switch (src_p->{signal_name}) {{'
    ]

    for multiplexer_id, signals_of_multiplexer_id in multiplexed_signals_per_id:
        body_lines = _format_pack_code_level(cg_message,
                                             cast("SignalTreeType", signals_of_multiplexer_id),
                                             variable_lines,
                                             helper_kinds)
        lines.append('')
        lines.append(f'case {multiplexer_id}:')

        if body_lines:
            lines.extend(body_lines[1:-1])

        lines.append('    break;')

    lines.extend([
        '',
        'default:',
        '    break;',
        '}'])

    return [('    ' + line).rstrip() for line in lines]


def _format_pack_code_signal(cg_message: "CodeGenMessage",
                             signal_name: str,
                             body_lines: list[str],
                             variable_lines: list[str],
                             helper_kinds: set[THelperKind]) -> None:
    cg_signal = cg_message.get_signal_by_name(signal_name)

    if cg_signal.signal.conversion.is_float or cg_signal.signal.is_signed:
        variable = f'    uint{cg_signal.type_length}_t {cg_signal.snake_name};'

        if cg_signal.signal.conversion.is_float:
            conversion = f'    memcpy(&{cg_signal.snake_name}, &src_p->{cg_signal.snake_name}, sizeof({cg_signal.snake_name}));'
        else:
            conversion = f'    {cg_signal.snake_name} = (uint{cg_signal.type_length}_t)src_p->{cg_signal.snake_name};'

        variable_lines.append(variable)
        body_lines.append(conversion)

    for index, shift, shift_direction, mask in cg_signal.segments(invert_shift=False):
        if cg_signal.signal.conversion.is_float or cg_signal.signal.is_signed:
            fmt = '    dst_p[{}] |= pack_{}_shift_u{}({}, {}u, 0x{:02x}u);'
        else:
            fmt = '    dst_p[{}] |= pack_{}_shift_u{}(src_p->{}, {}u, 0x{:02x}u);'

        line = fmt.format(index,
                          shift_direction,
                          cg_signal.type_length,
                          cg_signal.snake_name,
                          shift,
                          mask)
        body_lines.append(line)
        helper_kinds.add((shift_direction, cg_signal.type_length))


def _format_pack_code_level(cg_message: "CodeGenMessage",
                            signal_names: SignalTreeType,
                            variable_lines: list[str],
                            helper_kinds: set[THelperKind]) -> list[str]:
    """Format one pack level in a signal tree.

    """

    body_lines: list[str] = []
    muxes_lines: list[str] = []

    for signal_name in signal_names:
        if isinstance(signal_name, dict):
            mux_lines = _format_pack_code_mux(cg_message,
                                              signal_name,
                                              body_lines,
                                              variable_lines,
                                              helper_kinds)
            muxes_lines += mux_lines
        else:
            _format_pack_code_signal(cg_message,
                                     signal_name,
                                     body_lines,
                                     variable_lines,
                                     helper_kinds)

    body_lines = body_lines + muxes_lines

    if body_lines:
        body_lines = ["", *body_lines, ""]

    return body_lines


def _format_pack_code(cg_message: "CodeGenMessage",
                      helper_kinds: set[THelperKind]
                      ) -> tuple[str, str]:
    variable_lines: list[str] = []
    body_lines = _format_pack_code_level(cg_message,
                                         cg_message.message.signal_tree,
                                         variable_lines,
                                         helper_kinds)

    if variable_lines:
        variable_lines = [*sorted(set(variable_lines)), "", ""]

    return '\n'.join(variable_lines), '\n'.join(body_lines)


def _format_unpack_code_mux(cg_message: "CodeGenMessage",
                            mux: SignalTreeMuxElemType,
                            body_lines_per_index: list[str],
                            variable_lines: list[str],
                            helper_kinds: set[THelperKind],
                            node_name: str | None) -> list[str]:
    signal_name, multiplexed_signals = next(iter(mux.items()))
    _format_unpack_code_signal(cg_message,
                               signal_name,
                               body_lines_per_index,
                               variable_lines,
                               helper_kinds)
    multiplexed_signals_per_id = sorted(multiplexed_signals.items())
    signal_name = camel_to_snake_case(signal_name)

    lines = [
        f'switch (dst_p->{signal_name}) {{'
    ]

    for multiplexer_id, signals_of_multiplexer_id in multiplexed_signals_per_id:
        body_lines = _format_unpack_code_level(cg_message,
                                               cast("SignalTreeType", signals_of_multiplexer_id),
                                               variable_lines,
                                               helper_kinds,
                                               node_name)
        lines.append('')
        lines.append(f'case {multiplexer_id}:')
        lines.extend(_strip_blank_lines(body_lines))
        lines.append('    break;')

    lines.extend([
        '',
        'default:',
        '    break;',
        '}'])

    return [('    ' + line).rstrip() for line in lines]


def _format_unpack_code_signal(cg_message: "CodeGenMessage",
                               signal_name: str,
                               body_lines: list[str],
                               variable_lines: list[str],
                               helper_kinds: set[THelperKind]) -> None:
    cg_signal = cg_message.get_signal_by_name(signal_name)
    conversion_type_name = f'uint{cg_signal.type_length}_t'

    if cg_signal.signal.conversion.is_float or cg_signal.signal.is_signed:
        variable = f'    {conversion_type_name} {cg_signal.snake_name};'
        variable_lines.append(variable)

    segments = cg_signal.segments(invert_shift=True)

    for i, (index, shift, shift_direction, mask) in enumerate(segments):
        if cg_signal.signal.conversion.is_float or cg_signal.signal.is_signed:
            fmt = '    {} {} unpack_{}_shift_u{}(src_p[{}], {}u, 0x{:02x}u);'
        else:
            fmt = '    dst_p->{} {} unpack_{}_shift_u{}(src_p[{}], {}u, 0x{:02x}u);'

        line = fmt.format(cg_signal.snake_name,
                          '=' if i == 0 else '|=',
                          shift_direction,
                          cg_signal.type_length,
                          index,
                          shift,
                          mask)
        body_lines.append(line)
        helper_kinds.add((shift_direction, cg_signal.type_length))

    if cg_signal.signal.conversion.is_float:
        conversion = f'    memcpy(&dst_p->{cg_signal.snake_name}, &{cg_signal.snake_name}, sizeof(dst_p->{cg_signal.snake_name}));'
        body_lines.append(conversion)
    elif cg_signal.signal.is_signed:
        mask = ((1 << (cg_signal.type_length - cg_signal.signal.length)) - 1)

        if mask != 0:
            mask <<= cg_signal.signal.length
            formatted = SIGN_EXTENSION_FMT.format(name=cg_signal.snake_name,
                                                  shift=cg_signal.signal.length - 1,
                                                  mask=mask,
                                                  suffix=cg_signal.conversion_type_suffix)
            body_lines.extend(formatted.splitlines())

        conversion = f'    dst_p->{cg_signal.snake_name} = (int{cg_signal.type_length}_t){cg_signal.snake_name};'
        body_lines.append(conversion)


def _format_unpack_code_level(cg_message: "CodeGenMessage",
                              signal_names: SignalTreeType,
                              variable_lines: list[str],
                              helper_kinds: set[THelperKind],
                              node_name: str | None) -> list[str]:
    """Format one unpack level in a signal tree.

    """

    body_lines: list[str] = []
    muxes_lines: list[str] = []

    for signal_name in signal_names:
        if isinstance(signal_name, dict):
            mux_lines = _format_unpack_code_mux(cg_message,
                                                signal_name,
                                                body_lines,
                                                variable_lines,
                                                helper_kinds,
                                                node_name)

            if muxes_lines:
                muxes_lines.append('')

            muxes_lines += mux_lines
        else:
            if not _is_receiver(cg_message.get_signal_by_name(signal_name), node_name):
                continue

            _format_unpack_code_signal(cg_message,
                                       signal_name,
                                       body_lines,
                                       variable_lines,
                                       helper_kinds)

    if body_lines and body_lines[-1] != '':
        body_lines.append('')

    if muxes_lines:
        muxes_lines.append('')

    body_lines = body_lines + muxes_lines

    if body_lines:
        body_lines = ["", *body_lines]

    return body_lines


def _format_unpack_code(cg_message: "CodeGenMessage",
                        helper_kinds: set[THelperKind],
                        node_name: str | None) -> tuple[str, str]:
    variable_lines: list[str] = []
    body_lines = _format_unpack_code_level(cg_message,
                                           cg_message.message.signal_tree,
                                           variable_lines,
                                           helper_kinds,
                                           node_name)

    if variable_lines:
        variable_lines = [*sorted(set(variable_lines)), "", ""]

    return '\n'.join(variable_lines), '\n'.join(body_lines)


def _generate_struct(cg_message: "CodeGenMessage", bit_fields: bool) -> tuple[str, list[str]]:
    members = []

    for cg_signal in cg_message.cg_signals:
        members.append(_generate_signal(cg_signal, bit_fields))

    if not members:
        members = [
            ('    /**\n'
            '     * Dummy signal in empty message.\n'
            '     */\n'
            '    uint8_t dummy;')
        ]

    if cg_message.message.comment is None:
        comment = ''
    else:
        comment = f' * {cg_message.message.comment}\n *\n'

    return comment, members


def _format_choices(cg_signal: "CodeGenSignal", signal_name: str) -> list[str]:
    choices = []

    for value, name in sorted(cg_signal.unique_choices.items()):
        if cg_signal.signal.is_signed:
            fmt = '{signal_name}_{name}_CHOICE ({value})'
        else:
            fmt = '{signal_name}_{name}_CHOICE ({value}u)'

        choices.append(fmt.format(signal_name=signal_name.upper(),
                                  name=str(name),
                                  value=value))

    return choices


def _generate_encode_decode(cg_signal: "CodeGenSignal", use_float: bool, use_round: bool) -> tuple[str, str]:
    floating_point_type = _get_floating_point_type(use_float)

    scale = cg_signal.signal.scale
    offset = cg_signal.signal.offset

    scale_literal = f"{scale}{'.0' if isinstance(scale, int) else ''}{'f' if use_float else ''}"
    offset_literal = f"{offset}{'.0' if isinstance(offset, int) else ''}{'f' if use_float else ''}"

    if offset == 0 and scale == 1:
        encoding = 'value'
        decoding = f'({floating_point_type})value'
    elif offset != 0 and scale != 1:
        encoding = f'(value - {offset_literal}) / {scale_literal}'
        decoding = f'(({floating_point_type})value * {scale_literal}) + {offset_literal}'
    elif offset != 0:
        encoding = f'value - {offset_literal}'
        decoding = f'({floating_point_type})value + {offset_literal}'
    else:
        encoding = f'value / {scale_literal}'
        decoding = f'({floating_point_type})value * {scale_literal}'

    if not cg_signal.signal.is_float and use_round:
        encoding = f'round{"f" if use_float else ""}({encoding})'

    return encoding, decoding


def _generate_is_in_range(cg_signal: "CodeGenSignal") -> str:
    """Generate range checks for all signals in given message.

    """
    minimum = cg_signal.signal.minimum
    maximum = cg_signal.signal.maximum

    if minimum is not None:
        minimum = cg_signal.signal.scaled_to_raw(minimum)

    if maximum is not None:
        maximum = cg_signal.signal.scaled_to_raw(maximum)

    if (minimum is None and cg_signal.minimum_can_raw_value is not None) and \
       (cg_signal.minimum_ctype_value is None or cg_signal.minimum_can_raw_value > cg_signal.minimum_ctype_value):
        minimum = cg_signal.minimum_can_raw_value

    if (maximum is None and cg_signal.maximum_can_raw_value is not None) and \
       (cg_signal.maximum_ctype_value is None or cg_signal.maximum_can_raw_value < cg_signal.maximum_ctype_value):
        maximum = cg_signal.maximum_can_raw_value

    suffix = cg_signal.type_suffix
    check = []

    if minimum is not None:
        if not cg_signal.signal.conversion.is_float:
            minimum = round(minimum)
        else:
            minimum = float(minimum)

        minimum_ctype_value = cg_signal.minimum_ctype_value

        if (minimum_ctype_value is None) or (minimum > minimum_ctype_value):
            check.append(f'(value >= {minimum}{suffix})')

    if maximum is not None:
        if not cg_signal.signal.conversion.is_float:
            maximum = round(maximum)
        else:
            maximum = float(maximum)

        maximum_ctype_value = cg_signal.maximum_ctype_value

        if (maximum_ctype_value is None) or (maximum < maximum_ctype_value):
            check.append(f'(value <= {maximum}{suffix})')

    if not check:
        check = ['true']
    elif len(check) == 1:
        check = [check[0][1:-1]]

    return ' && '.join(check)

def _generate_is_in_phys_range(cg_signal: "CodeGenSignal", use_float: bool) -> tuple[str, str]:
    """Generate is_in_range check in the physical (floating-point) domain.

    The standard is_in_range() checks the raw integer value AFTER encode().
    If the physical value overflows the raw type, it can produce a raw value
    that wrongly passes is_in_range() — silent data corruption.

    Example — uint8 signal, scale=1, offset=0, range [0, 100]:
        encode(260.0) -> (uint8_t)(260) = 4   -> is_in_range(4)   -> TRUE  <- BUG
        encode(-1.0)  -> (uint8_t)(-1)  = 255 -> is_in_range(255) -> FALSE <- ok by chance
        is_in_phys_range(260.0) -> FALSE  <- correct
        is_in_phys_range(-1.0)  -> FALSE  <- correct

    Returns:
        phys_check  -- C expression for the return statement
        phys_unused -- (void)value cast if no bounds defined, else ""
    """
    minimum = cg_signal.signal.minimum
    maximum = cg_signal.signal.maximum
    f_suffix = "f" if use_float else ""
    check = []

    if minimum is not None:
        check.append(f"(value >= {float(minimum)}{f_suffix})")

    if maximum is not None:
        check.append(f"(value <= {float(maximum)}{f_suffix})")

    if not check:
        return "true", "    (void)value;\n\n"
    elif len(check) == 1:
        return check[0][1:-1], ""
    else:
        return " && ".join(check), ""

def _generate_frame_id_defines(database_name: str,
                               cg_messages: list["CodeGenMessage"],
                               node_name: str | None) -> str:
    return '\n'.join([
        f'#define {database_name.upper()}_{cg_message.snake_name.upper()}_FRAME_ID (0x{cg_message.message.frame_id:02x}u)'
        for cg_message in cg_messages if _is_sender_or_receiver(cg_message, node_name)
    ])


def _generate_frame_length_defines(database_name: str,
                                   cg_messages: list["CodeGenMessage"],
                                   node_name: str | None) -> str:
    result = '\n'.join([
        f'#define {database_name.upper()}_{cg_message.snake_name.upper()}_LENGTH ({cg_message.message.length}u)'
        for cg_message in cg_messages if _is_sender_or_receiver(cg_message, node_name)
    ])

    return result


def _generate_frame_cycle_time_defines(database_name: str,
                                       cg_messages: list["CodeGenMessage"],
                                       node_name: str | None) -> str:
    result = '\n'.join([
        f'#define {database_name.upper()}_{cg_message.snake_name.upper()}_CYCLE_TIME_MS ({cg_message.message.cycle_time}u)'
        for cg_message in cg_messages if cg_message.message.cycle_time is not None and
                                      _is_sender_or_receiver(cg_message, node_name)
    ])

    return result


def _generate_is_extended_frame_defines(database_name: str,
                                        cg_messages: list["CodeGenMessage"],
                                        node_name: str | None) -> str:
    result = '\n'.join([
        f'#define {database_name.upper()}_{cg_message.snake_name.upper()}_IS_EXTENDED ({int(cg_message.message.is_extended_frame)})'
        for cg_message in cg_messages if _is_sender_or_receiver(cg_message, node_name)
    ])

    return result


def _generate_choices_defines(database_name: str,
                              cg_messages: list["CodeGenMessage"],
                              node_name: str | None) -> str:
    choices_defines = []

    for cg_message in cg_messages:
        is_sender = _is_sender(cg_message, node_name)
        for cg_signal in cg_message.cg_signals:
            if cg_signal.signal.conversion.choices is None:
                continue
            if not is_sender and not _is_receiver(cg_signal, node_name):
                continue

            choices = _format_choices(cg_signal, cg_signal.snake_name)
            signal_choices_defines = '\n'.join([
                f'#define {database_name.upper()}_{cg_message.snake_name.upper()}_{choice}'
                for choice in choices
            ])
            choices_defines.append(signal_choices_defines)

    return '\n\n'.join(choices_defines)


def _generate_frame_name_macros(database_name: str,
                                cg_messages: list["CodeGenMessage"],
                                node_name: str | None) -> str:
    result = '\n'.join([
        f'#define {database_name.upper()}_{cg_message.snake_name.upper()}_NAME "{cg_message.message.name}"'
        for cg_message in cg_messages if _is_sender_or_receiver(cg_message, node_name)
    ])

    return result


def _generate_signal_name_macros(database_name: str,
                                 cg_messages: list["CodeGenMessage"],
                                 node_name: str | None) -> str:
    result = '\n'.join([
        f'#define {database_name.upper()}_{cg_message.snake_name.upper()}_{cg_signal.snake_name.upper()}_NAME "{cg_signal.signal.name}"'
        for cg_message in cg_messages if _is_sender_or_receiver(cg_message, node_name) for cg_signal in cg_message.cg_signals
    ])

    return result


def _generate_structs(database_name: str,
                      cg_messages: list["CodeGenMessage"],
                      bit_fields: bool,
                      node_name: str | None) -> str:
    structs = []

    for cg_message in cg_messages:
        if _is_sender_or_receiver(cg_message, node_name):
            comment, members = _generate_struct(cg_message, bit_fields)
            structs.append(
                STRUCT_FMT.format(comment=comment,
                                  database_message_name=cg_message.message.name,
                                  message_name=cg_message.snake_name,
                                  database_name=database_name,
                                  members='\n\n'.join(members)))

    return '\n'.join(structs)


def _is_sender(cg_message: "CodeGenMessage", node_name: str | None) -> bool:
    return node_name is None or node_name in cg_message.message.senders


def _is_receiver(cg_signal: "CodeGenSignal", node_name: str | None) -> bool:
    return node_name is None or node_name in cg_signal.signal.receivers


def _is_sender_or_receiver(cg_message: "CodeGenMessage", node_name: str | None) -> bool:
    if _is_sender(cg_message, node_name):
        return True
    return any(_is_receiver(cg_signal, node_name) for cg_signal in cg_message.cg_signals)


def _get_floating_point_type(use_float: bool) -> str:
    return 'float' if use_float else 'double'


def _generate_declarations(database_name: str,
                           cg_messages: list["CodeGenMessage"],
                           floating_point_numbers: bool,
                           use_float: bool,
                           node_name: str | None) -> str:
    declarations = []

    for cg_message in cg_messages:
        signal_declarations = []
        is_sender = _is_sender(cg_message, node_name)
        is_receiver = node_name is None

        for cg_signal in cg_message.cg_signals:
            if _is_receiver(cg_signal, node_name):
                is_receiver = True

            signal_declaration = ''

            if floating_point_numbers:
                if is_sender:
                    signal_declaration += SIGNAL_DECLARATION_ENCODE_FMT.format(
                        database_name=database_name,
                        message_name=cg_message.snake_name,
                        signal_name=cg_signal.snake_name,
                        type_name=cg_signal.type_name,
                        floating_point_type=_get_floating_point_type(use_float))
                if node_name is None or _is_receiver(cg_signal, node_name):
                    signal_declaration += SIGNAL_DECLARATION_DECODE_FMT.format(
                        database_name=database_name,
                        message_name=cg_message.snake_name,
                        signal_name=cg_signal.snake_name,
                        type_name=cg_signal.type_name,
                        floating_point_type=_get_floating_point_type(use_float))

            if is_sender or _is_receiver(cg_signal, node_name):
                signal_declaration += SIGNAL_DECLARATION_IS_IN_RANGE_FMT.format(
                    database_name=database_name,
                    message_name=cg_message.snake_name,
                    signal_name=cg_signal.snake_name,
                    type_name=cg_signal.type_name)

                signal_declaration += SIGNAL_DECLARATION_IS_IN_PHYS_RANGE_FMT.format(
                    database_name=database_name,
                    message_name=cg_message.snake_name,
                    signal_name=cg_signal.snake_name,
                    floating_point_type=_get_floating_point_type(use_float))

                signal_declarations.append(signal_declaration)
        declaration = ""
        if is_sender:
            declaration += DECLARATION_PACK_FMT.format(database_name=database_name,
                                                       database_message_name=cg_message.message.name,
                                                       message_name=cg_message.snake_name)
        if is_receiver:
            declaration += DECLARATION_UNPACK_FMT.format(database_name=database_name,
                                                         database_message_name=cg_message.message.name,
                                                         message_name=cg_message.snake_name)

        if is_sender or is_receiver:
            declaration += MESSAGE_DECLARATION_INIT_FMT.format(database_name=database_name,
                                                            database_message_name=cg_message.message.name,
                                                            message_name=cg_message.snake_name)

        if signal_declarations:
            declaration += '\n' + '\n'.join(signal_declarations)

        if declaration:
            declarations.append(declaration)

    return '\n'.join(declarations)


def _generate_definitions(database_name: str,
                          cg_messages: list["CodeGenMessage"],
                          floating_point_numbers: bool,
                          use_float: bool,
                          node_name: str | None,
                          use_round: bool,
                          ) -> tuple[str, tuple[set[THelperKind], set[THelperKind]]]:
    definitions = []
    pack_helper_kinds: set[THelperKind] = set()
    unpack_helper_kinds: set[THelperKind] = set()

    for cg_message in cg_messages:
        signal_definitions = []
        is_sender = _is_sender(cg_message, node_name)
        is_receiver = node_name is None
        signals_init_body = ''

        for cg_signal in cg_message.cg_signals:
            if use_float and cg_signal.type_name == "double":
                warnings.warn(f"User selected `--use-float`, but database contains "
                              f"signal with data type `double`: "
                              f"\"{cg_message.message.name}::{cg_signal.signal.name}\"",
                              stacklevel=2)
                _use_float = False
            else:
                _use_float = use_float

            encode, decode = _generate_encode_decode(cg_signal, _use_float, use_round)
            check = _generate_is_in_range(cg_signal)
            phys_check, phys_unused = _generate_is_in_phys_range(cg_signal, _use_float)

            if _is_receiver(cg_signal, node_name):
                is_receiver = True

            if check == 'true':
                unused = '    (void)value;\n\n'
            else:
                unused = ''

            signal_definition = ''

            if floating_point_numbers:
                if is_sender:
                    signal_definition += SIGNAL_DEFINITION_ENCODE_FMT.format(
                        database_name=database_name,
                        message_name=cg_message.snake_name,
                        signal_name=cg_signal.snake_name,
                        type_name=cg_signal.type_name,
                        encode=encode,
                        floating_point_type=_get_floating_point_type(_use_float))
                if node_name is None or _is_receiver(cg_signal, node_name):
                    signal_definition += SIGNAL_DEFINITION_DECODE_FMT.format(
                        database_name=database_name,
                        message_name=cg_message.snake_name,
                        signal_name=cg_signal.snake_name,
                        type_name=cg_signal.type_name,
                        decode=decode,
                        floating_point_type=_get_floating_point_type(_use_float))

            if is_sender or _is_receiver(cg_signal, node_name):
                signal_definition += SIGNAL_DEFINITION_IS_IN_RANGE_FMT.format(
                    database_name=database_name,
                    message_name=cg_message.snake_name,
                    signal_name=cg_signal.snake_name,
                    type_name=cg_signal.type_name,
                    unused=unused,
                    check=check)

                signal_definition += SIGNAL_DEFINITION_IS_IN_PHYS_RANGE_FMT.format(
                    database_name=database_name,
                    message_name=cg_message.snake_name,
                    signal_name=cg_signal.snake_name,
                    floating_point_type=_get_floating_point_type(_use_float),
                    phys_check=phys_check,
                    phys_unused=phys_unused)

                signal_definitions.append(signal_definition)

            if cg_signal.signal.initial:
                signals_init_body += INIT_SIGNAL_BODY_TEMPLATE_FMT.format(signal_initial=cg_signal.signal.raw_initial,
                                                                          signal_name=cg_signal.snake_name)

        if cg_message.message.length > 0:
            pack_variables, pack_body = _format_pack_code(cg_message,
                                                          pack_helper_kinds)
            unpack_variables, unpack_body = _format_unpack_code(cg_message,
                                                                unpack_helper_kinds,
                                                                node_name)
            pack_unused = ''
            unpack_unused = ''

            if not pack_body:
                pack_unused += '    (void)src_p;\n\n'

            if not unpack_body:
                unpack_unused += '    (void)dst_p;\n'
                unpack_unused += '    (void)src_p;\n\n'

            definition = ""
            if is_sender:
                definition += DEFINITION_PACK_FMT.format(database_name=database_name,
                                                         database_message_name=cg_message.message.name,
                                                         message_name=cg_message.snake_name,
                                                         message_length=cg_message.message.length,
                                                         pack_unused=pack_unused,
                                                         pack_variables=pack_variables,
                                                         pack_body=pack_body)
            if is_receiver:
                definition += DEFINITION_UNPACK_FMT.format(database_name=database_name,
                                                           database_message_name=cg_message.message.name,
                                                           message_name=cg_message.snake_name,
                                                           message_length=cg_message.message.length,
                                                           unpack_unused=unpack_unused,
                                                           unpack_variables=unpack_variables,
                                                           unpack_body=unpack_body)

            if is_sender or is_receiver:
                definition += MESSAGE_DEFINITION_INIT_FMT.format(database_name=database_name,
                                                                database_message_name=cg_message.message.name,
                                                                message_name=cg_message.snake_name,
                                                                init_body=signals_init_body)

        else:
            definition = EMPTY_DEFINITION_FMT.format(database_name=database_name,
                                                     message_name=cg_message.snake_name)

        if signal_definitions:
            definition += '\n' + '\n'.join(signal_definitions)

        if definition:
            definitions.append(definition)

    return '\n'.join(definitions), (pack_helper_kinds, unpack_helper_kinds)


def _generate_helpers_kind(kinds: set[THelperKind],
                           left_format: str,
                           right_format: str) -> list[str]:
    formats = {
        'left': left_format,
        'right': right_format
    }
    helpers = []

    for shift_direction, length in sorted(kinds):
        var_type = f'uint{length}_t'
        helper = formats[shift_direction].format(length=length,
                                                 var_type=var_type)
        helpers.append(helper)

    return helpers


def _generate_helpers(kinds: tuple[set[THelperKind], set[THelperKind]]) -> str:
    pack_helpers = _generate_helpers_kind(kinds[0],
                                          PACK_HELPER_LEFT_SHIFT_FMT,
                                          PACK_HELPER_RIGHT_SHIFT_FMT)
    unpack_helpers = _generate_helpers_kind(kinds[1],
                                            UNPACK_HELPER_LEFT_SHIFT_FMT,
                                            UNPACK_HELPER_RIGHT_SHIFT_FMT)
    helpers = pack_helpers + unpack_helpers

    if helpers:
        helpers.append('')

    return '\n'.join(helpers)


def _generate_fuzzer_source(database_name: str,
                            cg_messages: list["CodeGenMessage"],
                            date: str,
                            header_name: str,
                            source_name: str,
                            fuzzer_source_name: str) -> tuple[str, str]:
    tests = []
    calls = []

    for cg_message in cg_messages:
        name = f'{database_name}_{camel_to_snake_case(cg_message.message.name)}'

        test = TEST_FMT.format(name=name)
        tests.append(test)

        call = f'    test_{name}(data_p, size);'
        calls.append(call)

    source = FUZZER_SOURCE_FMT.format(version=__version__,
                                      date=date,
                                      header=header_name,
                                      tests='\n'.join(tests),
                                      llvm_body='\n'.join(calls))

    makefile = FUZZER_MAKEFILE_FMT.format(version=__version__,
                                          date=date,
                                          source=source_name,
                                          fuzzer_source=fuzzer_source_name)

    return source, makefile


def generate(database: "Database",
             database_name: str,
             header_name: str,
             source_name: str,
             fuzzer_source_name: str,
             floating_point_numbers: bool = True,
             bit_fields: bool = False,
             use_float: bool = False,
             node_name: str | None = None,
             use_round: bool = False,
             ) -> tuple[str, str, str, str]:
    """Generate C source code from given CAN database `database`.

    `database_name` is used as a prefix for all defines, data
    structures and functions.

    `header_name` is the file name of the C header file, which is
    included by the C source file.

    `source_name` is the file name of the C source file, which is
    needed by the fuzzer makefile.

    `fuzzer_source_name` is the file name of the C source file, which
    is needed by the fuzzer makefile.

    Set `floating_point_numbers` to ``True`` to allow floating point
    numbers in the generated code.

    Set `bit_fields` to ``True`` to generate bit fields in structs.

    Set `use_float` to ``True`` to prefer the `float` type instead
    of the `double` type for floating point numbers.

    `node_name` specifies the node for which message packers will be generated.
    For all other messages, unpackers will be generated. If `node_name` is not
    provided, both packers and unpackers will be generated.

    Set `use_round` to ``True`` to round encoded values to the nearest
    integer (instead of truncating).

    This function returns a tuple of the C header and source files as
    strings.

    """

    date = time.ctime()
    cg_messages = [CodeGenMessage(message) for message in database.messages]
    include_guard = f'{database_name.upper()}_H'
    frame_id_defines = _generate_frame_id_defines(database_name, cg_messages, node_name)
    frame_length_defines = _generate_frame_length_defines(database_name,
                                                          cg_messages,
                                                          node_name)
    is_extended_frame_defines = _generate_is_extended_frame_defines(
        database_name,
        cg_messages,
        node_name)
    frame_cycle_time_defines = _generate_frame_cycle_time_defines(
        database_name,
        cg_messages,
        node_name)
    choices_defines = _generate_choices_defines(database_name, cg_messages, node_name)

    frame_name_macros = _generate_frame_name_macros(database_name, cg_messages, node_name)
    signal_name_macros = _generate_signal_name_macros(database_name, cg_messages, node_name)

    structs = _generate_structs(database_name, cg_messages, bit_fields, node_name)
    declarations = _generate_declarations(database_name,
                                          cg_messages,
                                          floating_point_numbers,
                                          use_float,
                                          node_name)
    definitions, helper_kinds = _generate_definitions(database_name,
                                                      cg_messages,
                                                      floating_point_numbers,
                                                      use_float,
                                                      node_name,
                                                      use_round)
    helpers = _generate_helpers(helper_kinds)

    header = HEADER_FMT.format(file_name=header_name,
                               version=__version__,
                               date=date,
                               include_guard=include_guard,
                               frame_id_defines=frame_id_defines,
                               frame_length_defines=frame_length_defines,
                               is_extended_frame_defines=is_extended_frame_defines,
                               frame_cycle_time_defines=frame_cycle_time_defines,
                               choices_defines=choices_defines,
                               frame_name_macros=frame_name_macros,
                               signal_name_macros=signal_name_macros,
                               structs=structs,
                               declarations=declarations)

    includes = []
    if use_round:
        includes.append('#include <math.h>\n')

    source = SOURCE_FMT.format(file_name=source_name,
                               version=__version__,
                               date=date,
                               header=header_name,
                               helpers=helpers,
                               definitions=definitions,
                               includes=''.join(includes))

    fuzzer_source, fuzzer_makefile = _generate_fuzzer_source(
        database_name,
        cg_messages,
        date,
        header_name,
        source_name,
        fuzzer_source_name)

    return header, source, fuzzer_source, fuzzer_makefile
