# String I/O.
#
# Author: Malte J. Ziebarth (malte.ziebarth@tum.de)
#
# Copyright (C) 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: 2026 Technical University of Munich
# SPDX-License-Identifier: EUPL-1.2
# SPDX-FileContributor: Malte J. Ziebarth <malte.ziebarth@tum.de>

from typing import Literal
from ..language.abstract import Output, Input, Const
from .variablegraph import VariableGraphNode, VariableGraph
from .parameter import CTypeParameterByRef, PythonOutputParameter, \
    PythonInputParameter, CTypeParameterByValue, CppParameter


def add_string_input(
        name: str,
        vargraph: VariableGraph,
        root: VariableGraphNode | None,
        cpp_argument_id: int
    ) -> tuple[VariableGraphNode, VariableGraphNode]:
    """
    Adds a string input variable.
    """
    # Define the input parameter (nothing to be done there):
    py = PythonInputParameter(
        name,
        Input[Literal["str"]]("str")
    )
    if root is None:
        vgn_py = vargraph.insert(py)
    else:
        vgn_py = vargraph.insert(py, root)

    # Define and assign the `char*` variable:
    charp_name = vargraph.get_temporary_name(name, "char_p_")
    c_in = CTypeParameterByValue(
        charp_name,
        Const("c_char_p"),
        [
            f"    {charp_name} = c_char_p({name}.encode('utf-8'))"
        ]
    )
    vgn_char_p = vargraph.insert(c_in, vgn_py)

    # The C++ parameter:
    cpp_in = CppParameter.forward_input(c_in, cpp_argument_id)
    vargraph.insert(cpp_in, vgn_char_p)

    return vgn_py, vgn_char_p


class OutputStringBuilder:
    """
    Helper class for buildng an output string.
    """
    # The variable names:
    name: str
    id_name: str | None
    string_name: str | None
    errcode: str | None
    cpp_string_name: str | None
    cpp_argument_id: int

    vargraph: VariableGraph
    root: VariableGraphNode | None

    # The CTypes parameter:
    ctypes_param: tuple[CTypeParameterByRef, VariableGraphNode] | None
    cpp_param: tuple[CppParameter, VariableGraphNode] | None

    def __init__(self,
            name: str,
            vargraph: VariableGraph,
            root: VariableGraphNode | None,
            cpp_argument_id: int
        ):
        self.name = name
        self.root = root
        self.vargraph = vargraph
        self.cpp_argument_id = cpp_argument_id

        self.id_name = None
        self.string_name = None
        self.errcode = None
        self.cpp_string_name = None

        self.ctypes_param = None
        self.cpp_param = None



    def create_ctypes(self) ->  tuple[CTypeParameterByRef, VariableGraphNode]:
        """
        Creates the ctypes parameter.
        """
        if self.id_name is None:
            self.id_name = self.vargraph.get_temporary_name(self.name, prefix="id_")
        # The ID of the FKString.
        # This will be handed to the function call.
        # Later, we will retrieve the string.
        # We need to do this in two steps since we need to allocate
        # the char* buffer somewhere. If we don't allocate a buffer,
        # we cannot pass dynamic strings. If we do allocate, but we
        # don't follow up with a delete, we leak memory.
        c_id = CTypeParameterByRef(
            self.id_name,
            'c_size_t',
            [
                f"    {self.id_name} = c_size_t(0)"
            ]
        )
        if self.root is None:
            vgn_id = self.vargraph.insert(c_id)
        else:
            vgn_id = self.vargraph.insert(c_id, self.root)

        self.ctypes_param = (c_id, vgn_id)
        return self.ctypes_param


    def create_cpp(self) -> tuple[CppParameter, VariableGraphNode]:
        """
        Creates the C++ parameter.
        """
        if self.cpp_string_name is None:
            self.cpp_string_name = self.vargraph \
                .get_temporary_name(self.name, prefix='std_string_')
        # The C++ parameter:
        cpp_out = CppParameter(
            self.cpp_string_name,
            Output("std::string"),
            [
                f"            std::string {self.cpp_string_name};\n"
            ],
            [
                f"*{self.id_name} = "
                f"funktionsklempner::create_string({self.cpp_string_name})"
            ],
            self.cpp_string_name,
            self.cpp_argument_id
        )
        assert self.ctypes_param != None
        vgn = self.vargraph.insert(cpp_out, self.ctypes_param[1])

        self.cpp_param = (cpp_out, vgn)
        return self.cpp_param


    def ctypes_post_call_code(self) -> list[str]:
        """
        Returns the ctypes post call code.
        """
        if self.string_name is None:
            self.string_name = self.vargraph \
                .get_temporary_name(self.name, prefix="fks_")
        if self.errcode is None:
            self.errcode = self.vargraph.get_temporary_name(self.name, "err_")
        return [
            f"    # Get the string '{self.name}' from the allocated buffer.",
            f"    {self.string_name} = FKString()",
            f"    {self.errcode} = _get_string({self.id_name}, {self.string_name})",
            f"    if {self.errcode} != 0:",
            f"        _delete_string({self.errcode})",
            f"        raise RuntimeError(",
            f"            'An error occurred and the program failed to retrieve '",
            f"            'a string.'",
            f"        )",
            f"    {self.name} = str({self.string_name}.message.decode('utf-8'))",
            f"    # free the allocated buffer.",
            f"    {self.errcode} = _delete_string({self.id_name})",
            f"    if {self.errcode} != 0:",
            f"        raise RuntimeError('An error occurred while deleting a string.')"
        ]


def add_string_output(
        name: str,
        vargraph: VariableGraph,
        root: VariableGraphNode | None,
        cpp_argument_id: int
    ) -> tuple[VariableGraphNode,VariableGraphNode]:
    """
    Adds a string output variable.
    """
    osb = OutputStringBuilder(
        name, vargraph, root, cpp_argument_id
    )

    osb.create_ctypes()
    osb.create_cpp()


    py = PythonOutputParameter(
        name,
        Output[Literal["str"]]("str"),
        [],
        # All assignment happens post call:
        osb.ctypes_post_call_code()
    )
    assert osb.cpp_param is not None
    vgn = osb.cpp_param[1]
    vgn_str = vargraph.insert(py, vgn)

    return vgn, vgn_str