from __future__ import annotations
import hashlib
import json
import os
import sublime
import tempfile

from .distro_utils import using_miktex
from .logging import logger
from .settings import get_setting
from .settings import global_settings
from .sublime_utils import get_project_data
from .sublime_utils import get_project_file_name
from .tex_directives import get_tex_root
from .tex_directives import parse_tex_directives


# raised whenever the root cannot be determined, which indicates an unsaved
# file
class UnsavedFileException(Exception):
    pass


def get_aux_directory(
    view: sublime.View | None,
    return_setting: bool=False
) -> str | None | tuple[str, str] | tuple[None, None]:
    """
    Find the aux-directory

    general algorithm:
    1. check for an explicit aux_directory directive
    2. check for a project setting
    3. check for a global setting
    4. assume aux_directory is the same as output_directory

    :param view:
        The view displaying a TeX document.

    :param return_setting:
        Indicates that the raw setting should be returned as well as the auxiliary directory

    :returns:
        Absolute path of specified auxiliary directory.
    """
    if view is None:
        window = sublime.active_window()
        if window:
            view = window.active_view()

    if view:
        # look for aux_directory directive in root document
        root = get_tex_root(view)
        aux_directory = get_directive(root, "aux_directory")
        if aux_directory:
            aux_dir = resolve_to_absolute_path(root, aux_directory, _get_root_directory(root))

            if return_setting:
                return (aux_dir, aux_directory)
            else:
                return aux_dir

        # look for aux_directory in project settings
        aux_directory = get_project_data(view).get("settings", {}).get("latextools.aux_directory")
        if aux_directory:
            aux_dir = resolve_to_absolute_path(
                root, aux_directory, _get_root_directory(get_project_file_name(view))
            )

            if return_setting:
                return (aux_dir, aux_directory)
            else:
                return aux_dir

    # look for aux_directory in global settings
    aux_directory = global_settings().get("aux_directory")
    if aux_directory:
        aux_dir = resolve_to_absolute_path(root, aux_directory, _get_root_directory(root))

        if return_setting:
            return (aux_dir, aux_directory)
        else:
            return aux_dir

    return get_output_directory(view, return_setting)


def get_output_directory(
    view: sublime.View | None,
    return_setting: bool=False
) -> str | None | tuple[str, str] | tuple[None, None]:
    """
    Find the output-directory

    general algorithm:
    1. check for an explicit aux_directory directive
    2. check for a project setting
    3. check for a global setting

    :param view:
        The view displaying a TeX document.

    :param return_setting:
        Indicates that the raw setting should be returned as well as the output directory

    :returns:
        Absolute path of specified output directory.
    """
    if view is None:
        window = sublime.active_window()
        if window:
            view = window.active_view()

    if view:
        # look for output_directory directive in root document
        root = get_tex_root(view)
        output_directory = get_directive(root, "output_directory")
        if output_directory:
            out_dir = resolve_to_absolute_path(root, output_directory, _get_root_directory(root))

            if return_setting:
                return (out_dir, output_directory)
            else:
                return out_dir

        output_directory = get_project_data(view).get("settings", {}).get("latextools.output_directory")
        if output_directory:
            out_dir = resolve_to_absolute_path(
                root, output_directory, _get_root_directory(get_project_file_name(view))
            )

            if return_setting:
                return (out_dir, output_directory)
            else:
                return out_dir

    output_directory = global_settings().get("output_directory")
    if output_directory:
        out_dir = resolve_to_absolute_path(root, output_directory, _get_root_directory(root))

        if return_setting:
            return (out_dir, output_directory)
        else:
            return out_dir

    if return_setting:
        return (None, None)
    else:
        return None


# finds the jobname
# general algorithm:
#   1. check for an explict jobname setting
#   2. check for a --jobname flag
#   3. check for a project setting
#   4. check for a global setting
#   5. assume jobname is basename of tex_root
# Note: returns None if root is unsaved
def get_jobname(view_or_root):
    root = get_root(view_or_root)
    if root is None:
        return None

    # exit condition: texify and simple do not support jobname
    # so always return the root path
    if using_texify_or_simple():
        return os.path.splitext(os.path.basename(root))[0]

    jobname = get_directive(view_or_root, "jobname")
    if (jobname is None or jobname == "") and view_or_root != root:
        jobname = get_directive(root, "jobname")

    if jobname is None or jobname == "":
        jobname = get_setting("jobname")

    if jobname is None or jobname == "":
        return os.path.splitext(os.path.basename(root))[0]

    return jobname


def using_texify_or_simple():
    if using_miktex():
        builder = get_setting("builder", "traditional")
        if builder in ["", "default", "traditional", "simple"]:
            return True
    return False


def get_root(view_or_root):
    if isinstance(view_or_root, sublime.View):
        # here we can still handle root being None if the output_directory
        # setting is an aboslute path
        return get_tex_root(view_or_root)
    else:
        return view_or_root


def get_directive(root, key):
    directives = parse_tex_directives(root, multi_values=["options"], only_for=["options", key])

    try:
        return directives[key]
    except KeyError:
        option = key.replace("_", "-")
        for opt in directives.get("options", []):
            if opt.lstrip("-").startswith(option):
                try:
                    return opt.split("=")[1].strip()
                except Exception:
                    # invalid option parameter
                    return None
        return None


def resolve_to_absolute_path(root, value, root_path):
    # special values
    if (
        len(value) > 4
        and value[0] == "<"
        and value[1] == "<"
        and value[-2] == ">"
        and value[-1] == ">"
    ):
        root_hash = _get_root_hash(root)
        if root_hash is None:
            raise UnsavedFileException()

        if value == "<<temp>>":
            result = os.path.join(_get_tmp_dir(), root_hash)
        elif value == "<<project>>":
            result = os.path.join(root_path, root_hash)
        elif value == "<<cache>>":
            result = os.path.join(get_cache_directory(), root_hash)
        else:
            logger.error(f"unrecognized special value: {value}")

            # NOTE this assumes that the value provided is a typo, etc.
            # and tries not to do anything harmful. This may not be the
            # best assumption
            return None

        return result

    result = os.path.expandvars(os.path.expanduser(value))

    if not os.path.isabs(result):
        result = os.path.join(root_path, result)
    result = os.path.normpath(result)
    if os.path.exists(result):
        result = os.path.realpath(result)

    return result


def get_cache_directory():
    return os.path.join(sublime.cache_path(), "LaTeXTools")


# uses a process-wide temp directory which should be cleaned-up on exit
def _get_tmp_dir():
    if hasattr(_get_tmp_dir, "directory"):
        return _get_tmp_dir.directory
    else:
        _get_tmp_dir.directory = tempfile.mkdtemp()

        # register directory to be deleted on next start-up
        # unfortunately, there is no reliable way to do clean-up on exit
        # see https://github.com/SublimeTextIssues/Core/issues/10
        cache_dir = get_cache_directory()
        os.makedirs(cache_dir, exist_ok=True)

        temporary_output_dirs = os.path.join(cache_dir, "temporary_output_dirs")

        if os.path.exists(temporary_output_dirs):
            with open(temporary_output_dirs, "r") as f:
                data = json.load(f)
        else:
            data = {"directories": []}

        data["directories"].append(_get_tmp_dir.directory)

        with open(temporary_output_dirs, "w") as f:
            json.dump(data, f)

        return _get_tmp_dir.directory


def _get_root_directory(root):
    if root is None:
        # best guess
        return os.getcwd()
    else:
        if not os.path.isabs(root):
            # again, best guess
            return os.path.join(os.getcwd(), os.path.dirname(root))
        return os.path.dirname(root)


def _get_root_hash(root):
    if root is None:
        return None

    if os.path.exists(root):
        root = os.path.realpath(root)

    return hashlib.md5(root.encode("utf-8")).hexdigest()
