# Path handling in repositories: absolute, relative, and include.
#
# 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 Any, Iterable, Literal
from itertools import islice
from pathlib import PurePath, Path


def relative_path(root: PurePath, path: str) -> PurePath:
    """
    Parses a slash-denominated relative path `path` and
    returns its interpretation relative to `root`.
    """
    components = path.split('/')
    res = root / components[0]
    for c in islice(components, 1, None):
        res /= c
    return res.relative_to(root)


class RelativePath:
    """
    Encapsulate relative paths into their own type.
    """
    _root: PurePath
    _path: PurePath
    parent: "RelativePath"
    stem: str
    suffix: str

    def __init__(self, root: PurePath, path: str):
        assert isinstance(root, PurePath)
        assert isinstance(path, str)
        assert "\\" not in path
        self._path = relative_path(root, path)
        self._root = root


    @staticmethod
    def from_paths(root: PurePath, child: PurePath) -> "RelativePath":
        """
        Path of `child` relative to `root`
        """
        rp = RelativePath.__new__(RelativePath)
        rp._path = PurePath(child.relative_to(root))
        rp._root = root
        return rp


    def __getattr__(self, attribute: str) -> "RelativePath | str":
        if attribute == "parent":
            p = RelativePath.__new__(RelativePath)
            p._path = self._path.parent
            p._root = self._root
            return p
        elif attribute == "suffix":
            return self._path.suffix
        elif attribute == "stem":
            return self._path.stem

        raise AttributeError(f"Unknown attribute '{attribute}'")


    def is_dir(self) -> bool:
        return Path(self._path).is_dir()


    def exists(self) -> bool:
        return Path(self._path).exists()


    def is_file(self) -> bool:
        return Path(self._path).is_file()

    def __itruediv__(self, subpath: str | Iterable[str]) -> "RelativePath":
        path = self._path
        if isinstance(subpath, str):
            path /= subpath
        else:
            for c in subpath:
                path /= c
        self._path = path
        return self


    def __eq__(self, other: "Any | RelativePath | PurePath") -> bool:
        if isinstance(other, RelativePath):
            return self._path == other._path
        elif isinstance(other, PurePath):
            return self._path == other
        return False


    def __truediv__(self, subpath: str | Iterable[str]) -> "RelativePath":
        res = RelativePath.__new__(RelativePath)
        res._root = self._root
        if isinstance(subpath, str):
            assert "/" not in subpath and "\\" not in subpath
            res._path = self._path / subpath
        else:
            it = subpath.__iter__()
            # The first __next__() may fail if the iterable is
            # empty. Then, we simply return the same path.
            path = self._path
            try:
                path = self._path / it.__next__()
                while True:
                    path /= it.__next__()
            except StopIteration:
                pass
            res._path = path

        return res


    def relative_to(self, other: "RelativePath | PurePath") -> "RelativePath":
        res = RelativePath.__new__(RelativePath)
        if isinstance(other, RelativePath):
            root = other._root / other._path
            res._root = root
            path = self._root / self._path
            res._path = path.relative_to(root)
            return res
        elif isinstance(other, PurePath): #pyright: ignore[reportUnnecessaryIsInstance]
            res = RelativePath.__new__(RelativePath)
            res._root = other
            path = self._root / self._path
            res._path = path.relative_to(other)
            return res
        raise TypeError(
            "'other' has to be a RelativePath or PurePath instance."
        )

    def full(self) -> PurePath:
        return self._root / self._path


    def is_relative_to(self, other: "PurePath | RelativePath") -> bool:
        if isinstance(other, PurePath):
            other_path = other
        else:
            other_path = other.full()
        self_path = self.full()
        return self_path.is_relative_to(other_path)


    def as_posix(self, relative_to: "PurePath | RelativePath") -> str:
        path = self.relative_to(relative_to)
        return path._path.as_posix()


    def to_path(self) -> Path:
        return Path(self._root / self._path)



class IncludePath(RelativePath):
    """
    C++ include paths.
    """
    def __init__(self,
            header: RelativePath,
            include_root: RelativePath
        ):
        assert header.is_relative_to(include_root)
        # Defer the splitting into root and path to
        # RelativePath.relative_to, and copy the attributes
        # here:
        res = header.relative_to(include_root)
        self._root = res._root
        self._path = res._path


    def to_include(self) -> str:
        return str(self._path)




def include_statement(
        path: IncludePath,
        delimiter: Literal['<>'] | Literal['""']='<>') -> str:
    """
    Get the include
    """
    return f"#include {delimiter[0]}{path.to_include()}{delimiter[1]}"


def wrapper_header_path(
        header: RelativePath,
        include_root: RelativePath
    ) -> RelativePath:
    """
    Creates the wrapper header file name from the original header's
    stem, and returns the wrapper header's relative path to the
    include root for include purposes.
    """
    filename = header.stem + "_wrapped.hpp"
    return (header.parent / filename).relative_to(include_root)