# _tar_executable.py
#
# Project: AutoArchive
# License: GNU GPLv3
#
# Copyright (C) 2003 - 2026 Róbert Čerňanský



import os
import queue
import select
import subprocess
import threading
from collections.abc import MutableMapping, Iterable
from enum import Enum
from functools import cached_property, partial
from time import sleep
from typing import Tuple, Self, IO

from AutoArchive._infrastructure.utils import Utils



class _TarOutputChannel(Enum):
    StdOut = 1
    StdErr = 2



class _TarExecutable:

    # directories where tar is being looked up if it was not found in PATH
    __ARCHIVER_LOCATIONS_FALLBACKS = ("/bin", "/usr/bin", "/usr/local/bin")



    def __init__(self):
        self.__tarProcess: subprocess.Popen[str] | None = None
        self.__outputQueue: queue.SimpleQueue[Tuple[str, _TarOutputChannel]] = queue.SimpleQueue()



    @cached_property
    def path(self) -> str:

        def getTarPath(directories):
            resultPath = None

            for directory in directories:
                testTarPath = os.path.join(directory, self.__archiverBinaryName)
                if os.path.exists(testTarPath) and Utils.effectiveAccess(testTarPath, os.R_OK | os.X_OK):
                    resultPath = testTarPath
                    break

            return resultPath



        # first try to find tar in PATH
        tarPath = getTarPath(os.get_exec_path())

        # then try fallback locations
        if tarPath is None:
            tarPath = getTarPath(self.__ARCHIVER_LOCATIONS_FALLBACKS)

        # TODO: Add logging capability to this class, then log the error and return None instead of exception
        if tarPath is None:
            raise FileNotFoundError(str.format("Unable to locate the archiver binary: {}.", self.__archiverBinaryName))

        return tarPath



    @property
    def returnCode(self) -> int:
        return self.__tarProcess.returncode



    def run(self, arguments: list[str], environment: MutableMapping[str, str]) -> Self:
        environment["LC_MESSAGES"] = "C"
        if "LC_ALL" in environment:
            del environment["LC_ALL"]
        try:
            self.__tarProcess = subprocess.Popen([self.path] + arguments, stdout = subprocess.PIPE,
                stderr = subprocess.PIPE, env = environment, universal_newlines = True)
        except FileNotFoundError:
            raise
        except OSError as ex:
            raise OSError(str.format("Error while executing external archiving program: {}.", self.path),
                          self.path, ex)

        if Utils.isWindows():
            threading.Thread(
                target = partial(self.__readProcessOutput, self.__tarProcess.stdout, _TarOutputChannel.StdOut)).start()
            threading.Thread(
                target = partial(self.__readProcessOutput, self.__tarProcess.stderr, _TarOutputChannel.StdErr)).start()

        return self



    def output(self) -> Iterable[Tuple[str, _TarOutputChannel]]:
        if Utils.isWindows():
            return self.__outputWin()
        else:
            return self.__outputLinux()



    def wait(self) -> int:
        return self.__tarProcess.wait()



    def __outputLinux(self) -> Iterable[Tuple[str, _TarOutputChannel]]:

        # capture program's standard output and standard error and use CmdlineUi-like interface to print captured
        # messages; note that the order of messages written to stdout vs. messages written to stderr might not be
        # preserved
        while True:
            readyStreams = select.select((self.__tarProcess.stdout, self.__tarProcess.stderr), (), ())[0]
            streamActive = False
            for readyStream in readyStreams:
                line = readyStream.readline()
                if line:
                    streamActive = True
                    yield line, _TarOutputChannel.StdOut if readyStream is self.__tarProcess.stdout \
                        else _TarOutputChannel.StdErr
            if self.__tarProcess.poll() is not None and not streamActive:
                break



    def __outputWin(self) -> Iterable[Tuple[str, _TarOutputChannel]]:
        while True:
            try:
                lineAndOutputChannel = self.__outputQueue.get(timeout = 0.1)
                yield lineAndOutputChannel
            except queue.Empty:
                if self.__tarProcess.poll() is not None:
                    while True:
                        try:
                            lineAndOutputChannel = self.__outputQueue.get_nowait()
                            yield lineAndOutputChannel
                        except queue.Empty:
                            break
                    break



    def __readProcessOutput(self, outputChannel: IO[str], outputChannelType: _TarOutputChannel) -> None:
        for line in outputChannel:
            self.__outputQueue.put((line, outputChannelType))
            sleep(0) # give another thread a chance (works only on Windows)



    @cached_property
    def __archiverBinaryName(self) -> str:
        archiverBinaryName = "tar"
        return archiverBinaryName + ".exe" if Utils.isWindows() else archiverBinaryName
