# Parsing C++ header code.
#
# 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 get_args

from ..language.abstract import Struct
from ..language.fundamental import FundamentalBaseType
from ..language.python import CTypeType
from ..language.cpp import L1Type
from ..language.conversion import equivalent_ctypes_type
from .function import Function
from .library import Library
from .macros import FK_EXPORT_FUNCTION, FK_EXPORT_STRUCT, \
    FK_SIZE_CONTROL
from.sizecontrol import SizeControl
from ..repo import Repository, RelativePath, wrapper_header_path

from .function import parse_function_signature
from .spdx import parse_spdx
from .struct import parse_struct
from .usertypes import UserTypeDict

#
# 0. Caching type names for parsing.
#
_TYPE_NAMES: set[FundamentalBaseType] = set(get_args(FundamentalBaseType))

#
# Map the strings of the fundamental base
# type to themselves...
# More of a typing trick (and to ensure that
# we raise an error when something expected
# to be a fundamental type is not actually
# in that set).
#
_TYPE_MAP: dict[str, FundamentalBaseType] = {
    t : t for t in _TYPE_NAMES
}


#
# Parse states:
#
class ParseState:
    """
    The parsing state.
    """
    functions: list[Function]
    structs: list[Struct[L1Type]]
    scopelevel: int
    user_types: UserTypeDict
    fk_namespace: str
    strict: bool
    header: RelativePath
    verbose: bool

    def __init__(self,
            fk_namespace: str,
            strict: bool,
            header: RelativePath,
            verbose: bool
        ):
        self.functions = []
        self.structs = []
        self.scopelevel = 0
        self.user_types = UserTypeDict()
        self.fk_namespace = fk_namespace
        self.strict = strict
        self.header = header
        self.verbose = verbose



class SearchState:
    """
    Searching for Funktionsklempner macros.
    """
    ps: ParseState

    def __init__(self, ps: ParseState):
        self.ps = ps

    def handle(self, line: str, i: int) -> "SearchState | FunctionState | StructState":
        # Check if we are starting:
        if line == FK_EXPORT_FUNCTION:
            return FunctionState(self.ps)

        elif line == FK_EXPORT_STRUCT:
            return StructState(self.ps)

        return self


class FunctionState:
    """
    Parsing a function signature.
    """
    ps: ParseState
    size_control: SizeControl
    current: list[str]

    def __init__(self, ps: ParseState):
        self.size_control = SizeControl()
        self.current = []
        self.ps = ps


    def handle(self, line: str, i: int) -> "SearchState | FunctionState":
        # Reading!
        if ';' in line:
            # Final part of the function declaration:
            self.current.append(line.split(';')[0])

            # Add the simplified string:
            simplified = " ".join(s for cl in self.current for s in cl.split())
            self.ps.functions.append(
                parse_function_signature(
                    simplified,
                    self.ps.user_types,
                    self.ps.fk_namespace,
                    self.size_control,
                    self.ps.strict
                )
            )

            # Set state to searching again:
            return SearchState(self.ps)

        elif line.startswith(FK_SIZE_CONTROL):
            # Size control.
            self.parse_size_control(line, i)
            return self
        elif line.startswith('#pragma funktionsklempner'):
            raise RuntimeError(
                f"The header {self.ps.header.full()} uses the pre-v1.0.0 "
                f"funktionsklempner syntax in line {i}. Please update the "
                 "header to the new syntax."
            )

        else:
            self.current.append(line)
            return self


    def parse_size_control(self, line: str, i: int):
        """
        Parses the `FUNKTIONSKLEMPNER_SIZE_CONTROL(controlled, controller)`
        preprocessor macro.
        """
        err_msg = (
            f"The {FK_SIZE_CONTROL} macro requires two parameters (in line {i}, "
            f"'{line}')."
        )
        if not line.startswith(FK_SIZE_CONTROL+"("):
            raise RuntimeError(f"{err_msg} An opening paranthesis is missing.")
        line = line[len(FK_SIZE_CONTROL)+1:]
        controlled_controller = line.split(")")[0]
        if (m := controlled_controller.count('"') != 4):
            raise RuntimeError(
                f"{err_msg} Need exactly four quotation marks (found {m})."
            )
        _, controlled, _, controller_str, _ = controlled_controller.split('"')
        #if (n := controlled_controller.count(',')) != 1:
        #    raise RuntimeError(f"{err_msg} Found {n+1} parameters.")

        # Might be multiple controller for multidimensional arrays:
        controller = controller_str.split(",")

        def parse_variable_name(s: str) -> str:
            s = s.strip()
            s_check = s.replace('_','')
            start_correct = s[0].isalpha() or s[0] == '_'
            if not s_check.isalnum() or not start_correct or not s.isascii():
                raise RuntimeError(
                    f"In line {i}: variable names in the {FK_SIZE_CONTROL} macro "
                    "have to be alpha-numeric ASCII (+ underscore) parameter names "
                    "starting with a letter or an underscore."
                )
            return s

        controlled = parse_variable_name(controlled)
        controller = map(parse_variable_name, controller)

        if self.ps.verbose:
            print(f"Setting size control for '{controlled}' to '{controller}'")
        self.size_control.set_control(controlled, controller)




class StructState:
    """
    Parsing a struct.
    """
    ps: ParseState
    current: list[str]

    def __init__(self, ps: ParseState):
        self.ps = ps
        self.current = []


    def handle(self, line: str, i: int) -> "SearchState | StructState":
        # Reading!
        if ';' in line:
            before = line.split(';')[0]
            if self.ps.scopelevel + before.count('{') - before.count('}') == 0:
                self.ps.scopelevel = 0
                # Final part of the function declaration:
                self.current.append(line.split(';')[0])

                # Add the simplified string:
                simplified = " ".join(s for cl in self.current for s in cl.split())
                self.ps.structs.append(
                    parse_struct(
                        simplified,
                        self.ps.user_types,
                        self.ps.fk_namespace
                    )
                )

                # Make the structure known:
                sname = self.ps.structs[-1].name
                if sname in self.ps.user_types:
                    raise RuntimeError(
                        "Line " + str(line) + ": struct '" + sname
                        + "' has already been declared! In line."
                    )
                self.ps.user_types[sname] = self.ps.structs[-1]

                # Set state to searching again:
                return SearchState(self.ps)

        self.ps.scopelevel += line.count('{')
        self.ps.scopelevel -= line.count('}')
        self.current.append(line)

        return self








#
# The actual parsing routine
# ==========================
#
def parse_header(
        header: RelativePath,
        module: tuple[str, ...],
        repository: Repository,
        license_boilerplate: str | None,
        accept_any_email: bool,
        extended_author_syntax: bool,
        fk_namespace: str,
        strict: bool,
        include_root: RelativePath,
        verbose: bool
    ) -> Library:
    """
    Parses a header.
    """
    # States:
    # 's' : seeking
    ps = ParseState(fk_namespace, strict, header, verbose)
    state: SearchState | FunctionState | StructState \
        = SearchState(ps)

    # We do a bit of comment parsing but this does not cover
    # all valid comment configurations.
    in_comment: bool = False

    assert isinstance(header, RelativePath)

    # Removing comments:
    def remove_comment(line: str, in_comment: bool) -> tuple[str, bool]:
        """
        This function removes comments from lines.
        """
        if in_comment:
            if '*/' in line:
                # Comment ends somewhere in this line.
                ls = line.split('*/')
                if len(ls) != 2:
                    raise RuntimeError(
                        "Found a comment configuration that is not supported ("
                        "in line " + str(i+1) + " of file '"
                        + str(header) + "')."
                    )

                # Return the part that is not commented, and update
                # the in_comment state:
                return remove_comment(ls[1], False)

            else:
                # Skip commented line:
                return "", True

        elif '/*' in line or '//' in line:
            # Comment may start here. Check whether it's in a string:
            in_literal = False
            escaped: bool = False
            line_stripped: list[str] = []
            j0 = 0
            in_line_comment = False
            for j,c in enumerate(line):
                if escaped:
                    escaped = False
                    continue
                elif c == '"':
                    in_literal = not in_literal
                elif c == '\\':
                    escaped = True
                elif not in_literal and j+1 < len(line):
                    # If we are not currently in a comment,
                    # we may start a comment here:
                    if c == '/' and not in_comment:
                        if line[j+1] == '*':
                            # A comment starts here.
                            in_comment = True

                            # Since we have already handled the next
                            # character (and to prevent something like /*/):
                            escaped = True

                            # Copy the previous string that was not commented:
                            if j - j0 > 0:
                                line_stripped.append(line[j0:j])

                        elif line[j+1] == '/':
                            # A // line comment starts here!
                            # we can skip the rest of the line.
                            in_line_comment = True

                            # Copy the previous string that was not commented:
                            if j - j0 > 0:
                                line_stripped.append(line[j0:j])

                            # End parsing this line:
                            break

                    elif c == '*' and line[j+1] == '/':
                        # Exiting comment again!
                        # Can be a pretty frequent occurrence when writing
                        # comments like /* a single line comment */
                        in_comment = False

                        # We have already handled the next character:
                        escaped = True

                        # After the '/' (at index j+1), the new out-of-comment
                        # part of the line starts:
                        j0 = j+2

            # If we end up out-of-comment, add any remaining part of the
            # line:
            if not in_comment and not in_line_comment:
                j = len(line)
                if j - j0 > 0:
                    line_stripped.append(line[j0:j])


            # Return any part of the line that is out-of-comment, and
            # return the comment-state at the end of the line:
            return "".join(line_stripped), in_comment

        return line, False


    with open(header.to_path(), 'r') as f:
        # Parse the initial header:
        spdx = parse_spdx(f, extended_author_syntax)

        # Then parse the remainder of the file:
        for i,line in enumerate(f):
            #
            # First step: handle comments.
            #
            line, in_comment = remove_comment(line, in_comment)

            # Check if there is anything to interpret:
            if len(line) == 0:
                continue

            line = line.strip()

            state = state.handle(line, i)



    print("functions:", ps.functions)
    print("structs:  ", ps.structs)

    py_structs: list[Struct[CTypeType]] = [
        equivalent_ctypes_type(s) for s in ps.structs
    ]

    return Library(
        ps.functions,
        py_structs,
        module,
        repository,
        spdx.authors,
        spdx.copyright,
        license_boilerplate,
        include_root,
        header,
        wrapper_header_path(header, include_root),
        accept_any_email
    )