# Parsing C++ function arguments (variables).
#
# Author: Malte J. Ziebarth (malte.ziebarth@tum.de)
#
# Copyright (C) 2025-2026 Technische Universität München
#
# Licensed under the EUPL, Version 1.2 or – as soon they will be approved by
# the European Commission - subsequent versions of the EUPL (the "Licence");
# You may not use this work except in compliance with the Licence.
# You may obtain a copy of the Licence at:
#
# https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the Licence is distributed on an "AS IS" basis,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the Licence for the specific language governing permissions and
# limitations under the Licence.
#
# SPDX-FileCopyrightText: 2025-2026 Technical University of Munich
# SPDX-License-Identifier: EUPL-1.2
# SPDX-FileContributor: Malte J. Ziebarth <malte.ziebarth@tum.de>

from typing import TypeVar, Mapping
from ..language.abstract import Const, Pointer, Input, Output
from ..language.fundamental import FundamentalBaseType
from ..language.cpp import L1Type, CppArgumentType, FundamentalType
from ..language.arrays import NDArray, QuantityArray
from ..language.conversion import fundamental_base_type



def _split_arg(arg: str) -> list[str]:
    """
    Splits an argument string into components and
    merges integer types in the component split.
    """
    # Separate the pointers:
    arg = " * ".join(arg.split('*'))

    # Split at white space:
    components = arg.split()

    i = 0
    while i < len(components)-1:
        # Merge 'unsigned':
        if components[i] == 'unsigned':
            if components[i+1] not in ('char','short','int','long'):
                raise RuntimeError(
                    "'unsigned' must be before an integer type (is "
                    f"{" ".join((components[i], components[i+1]))})"
                )
            components[i+1] = 'unsigned ' + components[i+1]
            del components[i]
            continue

        elif components[i] == 'signed':
            if components[i+1] not in ('char','short','int','long'):
                raise RuntimeError(
                    "'signed' must be before an integer type (is "
                    f"{" ".join((components[i], components[i+1]))})"
                )
            # Drop it!
            if components[i+1] == 'char':
                components[i+1] = 'signed char'
            del components[i]
            continue

        # Drop trailing int:
        if i > 0 and components[i] == 'int':
            cprev = components[i-1]
            if cprev.endswith('short') or cprev.endswith('long'):
                del components[i]
                continue

        # Merge 'long long':
        if components[i] in ('long', 'unsigned long') and i < len(components) - 1:
            if components[i+1] == 'long':
                components[i] += ' long'
                del components[i+1]
                continue
            elif components[i+1] == 'double':
                components[i] += ' double'
                del components[i+1]
                continue

        # Otherwise just continue:
        i += 1

    return components


T = TypeVar("T")

class _BasicSpecifier[T]:
    @staticmethod
    def find(
            components: list[str],
            type_map: Mapping[str, T],
            arg: str
        ) -> T:
        """
        Finds the basic type specifier.
        """
        the_type: T | None = None
        i = 0
        while i < len(components):
            c = components[i]
            check_type = type_map.get(c)
            if check_type is not None:
                if the_type is not None:
                    raise RuntimeError(
                        f"Found multiple type specifiers in argument {arg}"
                    )
                else:
                    the_type = check_type
                    # Remove it once we found it:
                    del components[i]

            else:
                i += 1

        if the_type is None:
            raise RuntimeError(
                f"Found no type specifier in argument '{arg}', which "
                f"was split into components {components}."
            )

        return the_type




def parse_cpp_arg(
        arg: str,
        type_map: Mapping[str, L1Type]
    ) -> tuple[L1Type, str | None]:
    """
    """
    # Merge the integer types:
    components = _split_arg(arg)
    if len(components) == 0:
        raise RuntimeError(
            "Empty component list trying to parse C++ argument "
            f"'{arg}'"
        )

    # Find the basic type specifier:
    the_type: L1Type = _BasicSpecifier[L1Type].find(
        components, type_map, arg
    )

    # Separate the potential name:
    name: None | str = None
    if len(components) > 0 and components[-1] not in ('const', '*'):
        name = components[-1]
        del components[-1]

    # Now handle all pointers:
    for c in components:
        if c == 'const':
            # Const not handled in ctypes:
            the_type = Const(the_type)
        elif c == '*':
            print("thetype:", the_type)
            the_type = Pointer(the_type)

        else:
            raise RuntimeError(
                f"Invalid or unknown keyword '{c}' in argument '{arg}'."
            )

    return the_type, name



def parse_fundamental_type(
        arg: str,
        type_map: dict[str, FundamentalBaseType]
    ) -> FundamentalType:
    """
    Parses a fundamental type.
    """
    # Merge the integer types:
    components = _split_arg(arg)

    # Find the basic type specifier:
    the_type: FundamentalType = \
        _BasicSpecifier[FundamentalType].find(components, type_map, arg)

    # Now handle all pointers:
    for c in components:
        if c == 'const':
            # Const not handled in ctypes:
            the_type = Const(the_type)
        elif c == '*':
            print("thetype:", the_type)
            the_type = Pointer(the_type)

        else:
            raise RuntimeError(
                f"Invalid or unknown keyword '{c}' in argument '{arg}'."
            )

    return the_type




def parse_variable(
        arg: str,
        type_map: Mapping[str, L1Type],
        fk_namespace: str
    ) -> tuple[CppArgumentType, str]:
    """
    Parses the type and name of a single parameter.
    """
    if '&' in arg:
        raise RuntimeError(
            "References not allowed in function argument types."
        )

    # Utilize the fact that the funktionsklempner syntax is
    # based on type and class templates:
    if '<' not in arg:
        raise NotImplementedError(
            "Detected a default input parameter (which are not yet implemented) "
            f"while parsing '{arg}'."
        )

    if not (arg.count('<') == arg.count('>') == 1):
        raise RuntimeError(
            "Number of opening '<' and closing '>' is not both equal to one "
            f"in arg '{arg}'"
        )
    assert arg.index('<') < arg.index('>')

    fk_type, type_name = arg.split("<")
    the_type_, name = (s.strip() for s in type_name.split(">"))

    # Remove leading and trailing white spaces:
    fk_type = fk_type.strip()
    the_type_ = the_type_.strip()
    name = name.strip()

    # Figure out what was in front of the template:
    if fk_type == f"{fk_namespace}::InputNDArray":
        # NDArray, input.
        the_type = Input(NDArray(fundamental_base_type(the_type_)))

    elif fk_type == f"{fk_namespace}::OutputNDArray":
        # NDArray, output.
        the_type = Output(NDArray(fundamental_base_type(the_type_)))

    elif fk_type == f"{fk_namespace}::InputQuantityArray":
        # QuantityArray.
        unit,dtype = (x.strip() for x in the_type_.split(','))
        the_type = Input(
            QuantityArray(
                Const(fundamental_base_type(dtype)), unit
            )
        )

    elif fk_type == f"{fk_namespace}::OutputQuantityArray":
        # QuantityArray, output.
        unit,dtype = (x.strip() for x in the_type_.split(','))
        the_type = Output(
            QuantityArray(
                fundamental_base_type(dtype), unit
            )
        )


    elif fk_type == f"{fk_namespace}::input":
        # Input.
        itype, tpl_param_name = parse_cpp_arg(the_type_, type_map)
        if tpl_param_name is not None:
            raise RuntimeError(
                f"Found name '{tpl_param_name}' in template argument '{arg}'."
            )
        the_type = Input[L1Type](itype)

    elif fk_type == f"{fk_namespace}::output":
        # Output.
        otype, tpl_param_name = parse_cpp_arg(the_type_, type_map)
        if tpl_param_name is not None:
            raise RuntimeError(
                f"Found name '{tpl_param_name}' in template argument '{arg}'."
            )
        the_type = Output[L1Type](otype)

    else:
        raise RuntimeError(
            f"Unknown templated type '{fk_type}' in argument "
            f"'{arg}'"
        )

    return the_type, name
