from abc import ABC, abstractmethod
import os
from pathlib import Path
from types import TracebackType
from typing import Optional, Type, TypeVar, Union
from pydantic import BaseModel, PrivateAttr

AnyPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"]

OC = TypeVar("OC", bound="OpenClosable")


class OpenClosable(ABC, BaseModel):
    """
    An abstract base class for creating reentrant_ context managers.  A
    concrete subclass must define ``open()`` and ``close()`` methods;
    `OpenClosable` will then define ``__enter__`` and ``__exit__`` methods that
    keep track of the depth of nested ``with`` statements, calling ``open()``
    and ``close()`` only when entering & exiting the outermost ``with``.

    .. _reentrant: https://docs.python.org/3/library/contextlib.html
                   #reentrant-cms
    """

    _context_depth: int = PrivateAttr(0)

    @abstractmethod
    def open(self) -> None:
        ...

    @abstractmethod
    def close(self) -> None:
        ...

    def __enter__(self: OC) -> OC:
        if self._context_depth == 0:
            self.open()
        self._context_depth += 1
        return self

    def __exit__(
        self,
        _exc_type: Optional[Type[BaseException]],
        _exc_val: Optional[BaseException],
        _exc_tb: Optional[TracebackType],
    ) -> None:
        self._context_depth -= 1
        if self._context_depth == 0:
            self.close()


def resolve_path(path: AnyPath, basepath: Optional[AnyPath] = None) -> Path:
    """
    Convert a path to a `pathlib.Path` instance and resolve it using the same
    rules for as paths in ``outgoing`` configuration files: expand tildes by
    calling `Path.expanduser()`, prepend the parent directory of ``basepath``
    (usually the value of ``configpath``) to the path if given, and then
    resolve the resulting path to make it absolute.

    :param path path: the path to resolve
    :param path basepath: an optional path to resolve ``path`` relative to
    :rtype: pathlib.Path
    """
    p = Path(os.fsdecode(path)).expanduser()
    if basepath is not None:
        p = Path(os.fsdecode(basepath)).parent / p
    return p.resolve()
