import glob
import os
import re
import subprocess
import sys
import tempfile
import textwrap
import typing
import urllib.parse
import urllib.request
from pathlib import Path
from typing import Iterable, Iterator, Union

from .handler import _check_log_handler, logger
from .pandoc_download import DEFAULT_TARGET_FOLDER, download_pandoc

__author__ = "Juho Vepsäläinen; Maintained by Jessica Tegner"
__version__ = "1.17"
__all__ = [
    "convert_file",
    "convert_text",
    "get_pandoc_formats",
    "get_pandoc_version",
    "get_pandoc_path",
    "download_pandoc",
]

_MAX_TINYTEX_INSTALL_ATTEMPTS = 3


def convert_text(
    source: typing.Union[str, bytes],
    to: str,
    format: str,
    extra_args: Iterable = (),
    encoding: str = "utf-8",
    outputfile: Union[None, str, Path] = None,
    filters: Union[Iterable, None] = None,
    verify_format: bool = True,
    sandbox: bool = False,
    cworkdir: Union[str, None] = None,
) -> str:
    """Converts given `source` from `format` to `to`.

    :param source: Unicode string or bytes (see encoding)

    :param str to: format into which the input should be converted;
        can be one of `pypandoc.get_pandoc_formats()[1]`

    :param str format: the format of the inputs;
        can be one of `pypandoc.get_pandoc_formats()[1]`

    :param list extra_args: extra arguments (list of strings) to be passed to pandoc
        (Default value = ())

    :param str encoding: the encoding of the input bytes (Default value = 'utf-8')

    :param str outputfile: output will be written to outputfile
        or the converted content returned if None.
        The output filename can be specified as a string or pathlib.Path object.
        (Default value = None)

    :param list filters: pandoc filters e.g. filters=['pandoc-citeproc']

    :param bool verify_format: Verify from and to format before converting.
        Should only be set False when confident of the formats
        and performance is an issue. (Default value = True)

    :param bool sandbox: Run pandoc in pandocs own sandbox mode, limiting IO operations
        in readers and writers to reading the files specified on the command line.
        Anyone using pandoc on untrusted user input should use this option.
        Note: This only does something, on pandoc >= 2.15 (Default value = False)

    :param str cworkdir: set the current working directory (Default value = None)

    :returns: converted string or an empty string if an outputfile was given
    :rtype: str

    :raises RuntimeError:
        if any of the inputs are not valid of if pandoc fails with an error
    :raises OSError:
        if pandoc is not found; make sure it has been installed
        and is available at path.
    """

    # Binary container formats must not be decoded — they are ZIP archives
    # where a decode/encode round-trip silently corrupts data.
    _binary_input_formats = {"docx", "odt", "epub", "epub3", "pdf", "pptx"}
    base_format = _get_base_format(format) if format else ""
    if isinstance(source, bytes) and base_format not in _binary_input_formats:
        source = source.decode(encoding, errors="replace")

    return _convert_input(
        source,
        format,
        "string",
        to,
        extra_args=extra_args,
        outputfile=outputfile,
        filters=filters,
        verify_format=verify_format,
        sandbox=sandbox,
        cworkdir=cworkdir,
    )


def convert_file(
    source_file: Union[list, str, Path, Iterator],
    to: str,
    format: Union[str, None] = None,
    extra_args: Iterable = (),
    outputfile: Union[None, str, Path] = None,
    filters: Union[Iterable, None] = None,
    verify_format: bool = True,
    sandbox: bool = False,
    cworkdir: Union[str, None] = None,
    sort_files=True,
) -> str:
    """Converts given `source` from `format` to `to`.

    :param (str, list, pathlib.Path) source_file: If a string, should be either
        an absolute file path, relative file path, or a file pattern (like dir/*.md).
        If a list, should be a list of file paths, file patterns, or pathlib.Path
        objects. In addition, pathlib.Path objects as well as the generators produced by
        pathlib.Path.glob may be specified.

    :param str to: format into which the input should be converted; can be one of
            `pypandoc.get_pandoc_formats()[1]`

    :param str format: the format of the inputs;
        will be inferred from the source_file with a known filename extension;
        can be one of `pypandoc.get_pandoc_formats()[1]` (Default value = None)

    :param list extra_args: extra arguments (list of strings) to be passed to pandoc
            (Default value = ())

    :param str outputfile: output will be written to outputfile or the converted content
            returned if None. The output filename can be specified as a string
            or pathlib.Path object. (Default value = None)

    :param list filters: pandoc filters e.g. filters=['pandoc-citeproc']

    :param bool verify_format: Verify from and to format before converting.
        Should only be set False when confident of the formats
        and performance is an issue. (Default value = True)

    :param bool sandbox: Run pandoc in pandocs own sandbox mode, limiting IO operations
        in readers and writers to reading the files specified on the command line.
        Anyone using pandoc on untrusted user input should use this option.
        Note: This only does something, on pandoc >= 2.15 (Default value = False)

    :param str cworkdir: set the current working directory (Default value = None)

    :param bool sort_files: causes the files to be sorted before being passed to pandoc
        (Default value = True)

    :returns: converted string or an empty string if an outputfile was given
    :rtype: str

    :raises RuntimeError:
        if any of the inputs are not valid of if pandoc fails with an error
    :raises OSError:
        if pandoc is not found; make sure it has been installed
        and is available at path.
    """
    # check if we have a working directory
    # if we don't, we use the current working directory
    if cworkdir is None:
        cworkdir = os.getcwd()

    if _is_network_path(source_file):  # if the source_file is an url
        format = _identify_format_from_path(source_file, format)
        return _convert_input(
            source_file,
            format,
            "path",
            to,
            extra_args=extra_args,
            outputfile=outputfile,
            filters=filters,
            verify_format=verify_format,
            sandbox=sandbox,
            cworkdir=cworkdir,
        )

    # convert the source file to a path object internally
    if isinstance(source_file, str):
        source_file = Path(source_file)
    elif isinstance(source_file, list):
        source_file = [Path(x) for x in source_file]
    elif isinstance(source_file, Iterator):
        source_file = [Path(x) for x in source_file]

    # we are basically interested to figure out if it's an absolute path or not.
    # if it's not, we want to prefix the working directory.
    # if it's a list, we want to prefix the working directory to each item
    # if it's not an absolute path.
    # if it is, just use the absolute path.
    if isinstance(source_file, list):
        source_file = [x if x.is_absolute() else Path(cworkdir, x) for x in source_file]
    elif isinstance(source_file, Iterator):
        source_file = (x if x.is_absolute() else Path(cworkdir, x) for x in source_file)
    # check ifjust a single path was given
    elif isinstance(source_file, Path):
        source_file = (
            source_file if source_file.is_absolute() else Path(cworkdir, source_file)
        )

    discovered_source_files = []
    # if we have a list of files, we need to glob them
    # if we have a single file, we need to glob it
    # remember that we already converted the source_file to a path object
    # so for glob.glob use both the dir and file name
    if isinstance(source_file, list):
        for single_source in source_file:
            discovered_source_files.extend(glob.glob(str(single_source)))
        if discovered_source_files == []:
            discovered_source_files = source_file
    else:
        discovered_source_files.extend(glob.glob(str(source_file)))
        if discovered_source_files == []:
            discovered_source_files = [source_file]

    if not _identify_path(discovered_source_files):
        raise RuntimeError("source_file is not a valid path")
    format = _identify_format_from_path(discovered_source_files[0], format)
    if len(discovered_source_files) == 1:
        discovered_source_files = discovered_source_files[0]

    return _convert_input(
        discovered_source_files,
        format,
        "path",
        to,
        extra_args=extra_args,
        outputfile=outputfile,
        filters=filters,
        verify_format=verify_format,
        sandbox=sandbox,
        cworkdir=cworkdir,
        sort_files=sort_files,
    )


def _identify_path(source) -> bool:
    if isinstance(source, list):
        for single_source in source:
            if not _identify_path(single_source):
                return False
        return True
    is_path = False
    try:
        is_path = os.path.exists(source)
    except UnicodeEncodeError:
        is_path = os.path.exists(source.encode("utf-8"))
    except:  # noqa
        # still false
        pass

    if not is_path:
        try:
            is_path = len(glob.glob(source)) >= 1
        except UnicodeEncodeError:
            is_path = len(glob.glob(source.encode("utf-8"))) >= 1
        except:  # noqa
            # still false
            pass

    if not is_path:
        try:
            # check if it's an URL
            result = urllib.parse.urlparse(source)
            if result.scheme in ["http", "https"]:
                is_path = True
            elif result.scheme and result.netloc and result.path:
                # complete uri including one with a network path
                is_path = True
            elif result.scheme == "file" and result.path:
                is_path = os.path.exists(url2path(source))
        except AttributeError:
            pass

    return is_path


def _is_network_path(source):
    try:
        # check if it's an URL
        result = urllib.parse.urlparse(source)
        if result.scheme in ["http", "https"]:
            return True
        elif result.scheme and result.netloc and result.path:
            # complete uri including one with a network path
            return True
        elif result.scheme == "file" and result.path:
            return os.path.exists(url2path(source))
    except AttributeError:
        pass
    return False


def _identify_format_from_path(sourcefile: str, format: str) -> str:
    return format or os.path.splitext(sourcefile)[1].strip(".")


def normalize_format(fmt):
    formats = {
        "dbk": "docbook",
        "md": "markdown",
        "tex": "latex",
    }
    fmt = formats.get(fmt, fmt)
    # rst format can have extensions
    if fmt[:4] == "rest":
        fmt = "rst" + fmt[4:]
    return fmt


def _validate_formats(format, to, outputfile):

    format = normalize_format(format)
    to = normalize_format(to)

    if not format:
        raise RuntimeError("Missing format!")

    from_formats, to_formats = get_pandoc_formats()

    if _get_base_format(format) not in from_formats:
        raise RuntimeError(
            'Invalid input format! Got "{}" but expected one of these: {}'.format(
                _get_base_format(format), ", ".join(from_formats)
            )
        )

    base_to_format = _get_base_format(to)

    file_extension = os.path.splitext(to)[1]

    if (
        base_to_format not in to_formats
        and base_to_format != "pdf"  # pdf is handled later # noqa: E127
        and file_extension != ".lua"
    ):
        raise RuntimeError(
            "Invalid output format! Got {} but expected one of these: {}".format(
                base_to_format, ", ".join(to_formats)
            )
        )

    # list from https://github.com/jgm/pandoc/blob/master/pandoc.hs
    # `[...] where binaries = ["odt","docx","epub","epub3"] [...]`
    # pdf has the same restriction
    if base_to_format in ["odt", "docx", "epub", "epub3", "pdf"] and not outputfile:
        raise RuntimeError(
            "Output to %s only works by using a outputfile." % base_to_format
        )

    if base_to_format == "pdf":
        # pdf formats need a filename with an ending of .pdf
        if isinstance(outputfile, str):
            if outputfile[-4:] != ".pdf":
                raise RuntimeError(
                    'PDF output needs an outputfile with ".pdf" as a fileending.'
                )
        elif isinstance(outputfile, Path):
            if outputfile.suffix != ".pdf":
                raise RuntimeError(
                    'PDF output needs an outputfile with ".pdf" as a fileending.'
                )
        # it's also not allowed to contain extensions according to the docs
        if to != base_to_format:
            raise RuntimeError("PDF output can't contain any extensions: %s" % to)

    return format, to


def _try_setup_tinytex():
    """If pytinytex is installed, ensure TinyTeX bin is on PATH."""
    try:
        import pytinytex

        pytinytex.ensure_tinytex_installed()
    except (ImportError, RuntimeError):
        pass


def _is_tinytex_available():
    """Check if pytinytex is installed and TinyTeX is set up."""
    try:
        import pytinytex

        pytinytex.ensure_tinytex_installed()
        return True
    except (ImportError, RuntimeError):
        return False


def _try_auto_install_packages(stderr):
    """Parse stderr for missing LaTeX packages, install them via pytinytex."""
    try:
        from pytinytex import install, parse_log

        parsed = parse_log(stderr)
        installed = []
        for pkg in parsed.missing_packages:
            try:
                install(pkg)
                installed.append(pkg)
            except RuntimeError:
                pass
        return installed
    except ImportError:
        return []


def _convert_input(
    source: str,
    format,
    input_type,
    to,
    extra_args=(),
    outputfile=None,
    filters=None,
    verify_format=True,
    sandbox=False,
    cworkdir=None,
    sort_files=True,
):

    _check_log_handler()

    logger.debug("Ensuring pandoc path...")
    _ensure_pandoc_path()

    if verify_format:
        logger.debug("Verifying format...")
        format, to = _validate_formats(format, to, outputfile)
    else:
        format = normalize_format(format)
        to = normalize_format(to)

    logger.debug("Identifying input type...")
    string_input = input_type == "string"
    if not string_input:
        if isinstance(source, str):
            input_file = [source]
        else:
            input_file = source
    else:
        input_file = []

    if sort_files:
        input_file = sorted(input_file)

    args = [__pandoc_path, "--from=" + format]

    args.append("--to=" + to)

    args += input_file

    if outputfile:
        args.append("--output=" + str(outputfile))

    if sandbox:
        # sandbox was introduced in pandoc 2.15, so only add if we are using >= 2.15.
        if ensure_pandoc_minimal_version(2, 15):
            logger.debug("Adding sandbox argument...")
            args.append("--sandbox")
        else:
            logger.warning(
                "Sandbox argument was used, but pandoc version is too low. "
                "Ignoring argument."
            )

    args.extend(extra_args)

    # adds the proper filter syntax for each item in the filters list
    if filters is not None:
        if isinstance(filters, str):
            filters = filters.split()
        f = [
            "--lua-filter=" + x if x.endswith(".lua") else "--filter=" + x
            for x in filters
        ]
        args.extend(f)

    # If converting to PDF or LaTeX, try to set up TinyTeX on PATH
    # so pandoc finds LaTeX engines automatically.
    needs_latex = _get_base_format(to) in ("pdf", "latex")
    if needs_latex:
        _try_setup_tinytex()

    # To get access to pandoc-citeproc when we use a included copy of pandoc,
    # we need to add the pypandoc/files dir to the PATH
    new_env = os.environ.copy()
    files_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "files")
    new_env["PATH"] = new_env.get("PATH", "") + os.pathsep + files_path
    creation_flag = (
        0x08000000 if sys.platform == "win32" else 0
    )  # set creation flag to not open pandoc in new console on windows

    # When converting to PDF with pytinytex available, retry on missing
    # LaTeX packages (auto-install via tlmgr and re-run pandoc).
    if needs_latex and _is_tinytex_available():
        max_attempts = _MAX_TINYTEX_INSTALL_ATTEMPTS
    else:
        max_attempts = 1

    for attempt in range(max_attempts):
        old_wd = os.getcwd()
        if cworkdir and old_wd != cworkdir:
            os.chdir(cworkdir)

        logger.debug("Running pandoc...")
        p = subprocess.Popen(
            args,
            stdin=subprocess.PIPE if string_input else None,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            env=new_env,
            creationflags=creation_flag,
        )

        if cworkdir is not None:
            os.chdir(old_wd)

        # something else than 'None' indicates that the process already terminated
        if not (p.returncode is None):
            raise RuntimeError(
                'Pandoc died with exitcode "{}" before receiving input: {}'.format(
                    p.returncode,
                    p.stderr.read().decode("utf-8", errors="replace"),
                )
            )

        if string_input:
            if isinstance(source, str):
                source = source.encode("utf-8")
        stdout, stderr = p.communicate(source if string_input else None)

        if not (to in ["odt", "docx", "epub", "epub3", "pdf"] and outputfile == "-"):
            stdout = stdout.decode("utf-8", errors="replace")

        stderr = stderr.decode("utf-8", errors="replace")

        # If pandoc failed and we have retries left, try auto-installing
        # missing LaTeX packages.
        if p.returncode != 0 and attempt < max_attempts - 1:
            installed = _try_auto_install_packages(stderr)
            if installed:
                logger.info(
                    "Auto-installed LaTeX packages: %s, retrying pandoc...",
                    installed,
                )
                continue  # retry with newly installed packages
            # Nothing could be installed, no point retrying
            break

        break  # success or single-attempt mode

    # check that pandoc returned successfully
    if p.returncode != 0:
        hint = ""
        if needs_latex:
            try:
                import pytinytex  # noqa: F401

                hint = (
                    "\nHint: pytinytex is installed but could not resolve "
                    "the missing LaTeX packages. You may need to install "
                    "them manually with pytinytex.install('<package>')."
                )
            except ImportError:
                hint = (
                    "\nHint: Install pypandoc[tinytex] for automatic "
                    "LaTeX package management: pip install pypandoc[tinytex]"
                )
        raise RuntimeError(
            "Pandoc died with exitcode "
            f'"{p.returncode}" during conversion: '
            f"{stderr}{hint}"
        )

    # if there is output on stderr, process it and send to logger
    if stderr:
        for level, msg in _classify_pandoc_logging(stderr):
            logger.log(level, msg)

    # if there is an outputfile, then stdout is likely empty!
    return stdout


def _classify_pandoc_logging(raw, default_level="WARNING"):
    # Process raw and yield the contained logging levels and messages.
    # Assumes that the messages are formatted like "[LEVEL] message". If the
    # first message does not have a level or any other message has a level
    # that does not conform to the pandoc standard, use the default_level
    # value instead.

    # Available pandoc logging levels adapted from:
    # https://github.com/jgm/pandoc/blob/5e1249481b2e3fc27e845245a0c96c3687a23c3d/src/Text/Pandoc/Logging.hs#L44
    def get_python_level(pandoc_level):

        level_map = {"ERROR": 40, "WARNING": 30, "INFO": 20, "DEBUG": 10}

        if pandoc_level not in level_map:
            level = level_map[default_level]
        else:
            level = level_map[pandoc_level]

        return level

    msgs = raw.split("\n")
    first = msgs.pop(0)

    search = re.search(r"\[(.*?)\]", first)

    # Use the default if the first message doesn't have a level
    if search is None:
        pandoc_level = default_level
    else:
        pandoc_level = first[search.start(1) : search.end(1)]

    log_msgs = [first.replace(f"[{pandoc_level}] ", "")]

    for msg in msgs:

        search = re.search(r"\[(.*?)\]", msg)

        if search is not None:
            yield get_python_level(pandoc_level), "\n".join(log_msgs)
            pandoc_level = msg[search.start(1) : search.end(1)]
            log_msgs = [msg.replace(f"[{pandoc_level}] ", "")]
            continue

        log_msgs.append(msg)

    yield get_python_level(pandoc_level), "\n".join(log_msgs)


def _get_base_format(format):
    """
    According to http://johnmacfarlane.net/pandoc/README.html#general-options,
    syntax extensions for markdown can be individually enabled or disabled by
    appending +EXTENSION or -EXTENSION to the format name.
    Return the base format without any extensions.
    """
    return re.split(r"\+|-", format)[0]


def get_pandoc_formats() -> Iterable:
    """
    Dynamic preprocessor for Pandoc formats.
    Return 2 lists. "from_formats" and "to_formats".
    """
    _ensure_pandoc_path()
    creation_flag = (
        0x08000000 if sys.platform == "win32" else 0
    )  # set creation flag to not open pandoc in new console on windows
    p = subprocess.Popen(
        [__pandoc_path, "--list-output-formats"],
        stdin=subprocess.PIPE,
        stderr=subprocess.PIPE,
        stdout=subprocess.PIPE,
        creationflags=creation_flag,
    )

    comm = p.communicate()
    out = comm[0].decode().splitlines(False)
    if p.returncode != 0:
        # try the old version and see if that returns something
        return get_pandoc_formats_pre_1_18()

    p = subprocess.Popen(
        [__pandoc_path, "--list-input-formats"],
        stdin=subprocess.PIPE,
        stderr=subprocess.PIPE,
        stdout=subprocess.PIPE,
        creationflags=creation_flag,
    )

    comm = p.communicate()
    in_ = comm[0].decode().splitlines(False)

    return [f.strip() for f in in_], [f.strip() for f in out]


def get_pandoc_formats_pre_1_18() -> Iterable:
    """
    Dynamic preprocessor for Pandoc formats for version < 1.18.
    Return 2 lists. "from_formats" and "to_formats".
    """
    _ensure_pandoc_path()
    creation_flag = (
        0x08000000 if sys.platform == "win32" else 0
    )  # set creation flag to not open pandoc in new console on windows
    p = subprocess.Popen(
        [__pandoc_path, "-h"],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        creationflags=creation_flag,
    )

    comm = p.communicate()
    help_text = comm[0].decode().splitlines(False)
    if p.returncode != 0 or "Options:" not in help_text:
        raise RuntimeError(
            "Couldn't call pandoc to get output formats. Output from pandoc:\n%s"
            % str(comm)
        )
    txt = " ".join(help_text[1 : help_text.index("Options:")])

    aux = txt.split("Output formats: ")
    in_ = re.sub(r"Input\sformats:\s|\*|\[.*?\]", "", aux[0]).split(",")
    out = re.sub(r"\*|\[.*?\]", "", aux[1]).split(",")

    return [f.strip() for f in in_], [f.strip() for f in out]


# copied and adapted from jupyter_nbconvert/utils/pandoc.py, Modified BSD License


def _get_pandoc_version(pandoc_path: str) -> str:
    new_env = os.environ.copy()
    creation_flag = (
        0x08000000 if sys.platform == "win32" else 0
    )  # set creation flag to not open pandoc in new console on windows
    if "HOME" not in os.environ:
        new_env["HOME"] = tempfile.gettempdir()
    p = subprocess.Popen(
        [pandoc_path, "--version"],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        env=new_env,
        creationflags=creation_flag,
    )
    comm = p.communicate()
    out_lines = comm[0].decode().splitlines(False)
    if p.returncode != 0 or len(out_lines) == 0:
        raise RuntimeError(
            "Couldn't call pandoc to get version information. Output from "
            "pandoc:\n%s" % str(comm)
        )

    version_pattern = re.compile(r"^\d+(\.\d+){1,}$")
    for tok in out_lines[0].split():
        if version_pattern.match(tok):
            version = tok
            break
    return version


def get_pandoc_version() -> str:
    """Gets the Pandoc version if Pandoc is installed.

    It will probe Pandoc for its version, cache it and return that value.
    If a cached version is found, it will return the cached version
    and stop probing Pandoc (unless :func:`clean_version_cache()` is called).

    :raises OSError:
        if pandoc is not found; make sure it has been installed
        and is available at path.
    """
    global __version

    if __version is None:
        _ensure_pandoc_path()
        __version = _get_pandoc_version(__pandoc_path)
    return __version


def get_pandoc_path() -> str:
    """Gets the Pandoc path if Pandoc is installed.

    It will return a path to pandoc which is used by pypandoc.

    This might be a full path or, if pandoc is on PATH, simple `pandoc`. It's guaranteed
    to be callable (i.e. we could get version information from `pandoc --version`).
    If `PYPANDOC_PANDOC` is set and valid, it will return that value. If the environment
    variable is not set, either the full path to the included pandoc or the pandoc in
    `PATH` or a pandoc in some of the more usual (platform specific) install locations
    (whatever is the higher version) will be returned.

    If a cached path is found, it will return the cached path and stop probing Pandoc
    (unless :func:`clean_pandocpath_cache()` is called).

    :raises OSError: if pandoc is not found
    """
    _ensure_pandoc_path()
    return __pandoc_path


def ensure_pandoc_minimal_version(major: int, minor: int = 0) -> bool:
    """Check if the used pandoc fulfill a minimal version requirement.

    :param int major: pandoc major version, such as 1 or 2.

    :param int minor: pandoc minor version, such as 10 or 11.

    :returns: True only if the installed pandoc is above the minimal version.
    :rtype: bool
    """
    version = [int(x) for x in get_pandoc_version().split(".")]
    if version[0] > int(major):  # if we have pandoc2 but major is request to be 1
        return True
    return version[0] >= int(major) and version[1] >= int(minor)


def ensure_pandoc_maximal_version(major: int, minor: int = 9999) -> bool:
    """Check if the used pandoc fulfill a maximal version requirement.

    :param int major: pandoc major version, such as 1 or 2.

    :param int minor: pandoc minor version, such as 10 or 11.

    :returns: True if the installed pandoc is below the maximal version.
    :rtype: bool
    """
    version = [int(x) for x in get_pandoc_version().split(".")]
    if version[0] < int(major):  # if we have pandoc1 but major is request to be 2
        return True
    return version[0] <= int(major) and version[1] <= int(minor)


def _ensure_pandoc_path() -> None:
    global __pandoc_path

    _check_log_handler()

    if __pandoc_path is None:
        included_pandoc = os.path.join(
            os.path.dirname(os.path.realpath(__file__)), "files", "pandoc"
        )
        search_paths = ["pandoc", included_pandoc]
        pf = "linux" if sys.platform.startswith("linux") else sys.platform
        try:
            if pf == "win32":
                search_paths.append(
                    os.path.join(DEFAULT_TARGET_FOLDER[pf], "pandoc.exe")
                )
            else:
                search_paths.append(os.path.join(DEFAULT_TARGET_FOLDER[pf], "pandoc"))
        except:  # noqa
            # not one of the know platforms...
            pass
        if pf == "linux":
            # Currently we install into ~/bin, but this is equally likely...
            search_paths.append("~/.bin/pandoc")
        # Also add the interpreter script path, as that's where pandoc could be
        # installed if it's an environment and the environment wasn't activated
        if pf == "win32":
            search_paths.append(os.path.join(sys.exec_prefix, "Scripts", "pandoc.exe"))

            # Since this only runs on Windows, use Windows slashes
            if os.getenv("ProgramFiles", None):
                search_paths.append(
                    os.path.expandvars("${ProgramFiles}\\Pandoc\\pandoc.exe")
                )
                search_paths.append(
                    os.path.expandvars("${ProgramFiles}\\Pandoc\\Pandoc.exe")
                )
            if os.getenv("ProgramFiles(x86)", None):
                search_paths.append(
                    os.path.expandvars("${ProgramFiles(x86)}\\Pandoc\\pandoc.exe")
                )
                search_paths.append(
                    os.path.expandvars("${ProgramFiles(x86)}\\Pandoc\\Pandoc.exe")
                )

        # bin can also be used on windows (conda at least has it in path), so
        # include it unconditionally
        search_paths.append(os.path.join(sys.exec_prefix, "bin", "pandoc.exe"))
        search_paths.append(os.path.join(sys.exec_prefix, "bin", "pandoc"))
        # If a user added the complete path to pandoc to an env, use that as the
        # only way to get pandoc so that a user can overwrite even a higher
        # version in some other places.
        if os.getenv("PYPANDOC_PANDOC", None):
            search_paths = [os.getenv("PYPANDOC_PANDOC")]
        curr_version = [0, 0, 0]
        for path in search_paths:
            # Needed for windows and subprocess which can't expand it on it's
            # own...
            path = os.path.expanduser(path)
            version_string = "0.0.0"
            # print("Trying: %s" % path)
            try:
                version_string = _get_pandoc_version(path)
            except Exception:
                # we can't use that path...
                if os.path.exists(path):
                    # path exist but is not usable -> not executable?
                    log_msg = (
                        "Found {}, but not using it because of an "
                        "error:".format(path)
                    )
                    logger.exception(log_msg)
                continue
            version = [int(x) for x in version_string.split(".")]
            while len(version) < len(curr_version):
                version.append(0)
            # print("%s, %s" % (path, version))
            # Only use the new version if it is any bigger...
            if version > curr_version:
                # print("Found: %s" % path)
                __pandoc_path = path
                curr_version = version

        if __pandoc_path is None:
            # Only print hints if requested
            if os.path.exists("/usr/local/bin/brew"):
                logger.info(
                    textwrap.dedent(
                        """\
                    Maybe try:

                        brew install pandoc
                """
                    )
                )
            elif os.path.exists("/usr/bin/apt-get"):
                logger.info(
                    textwrap.dedent(
                        """\
                    Maybe try:

                        sudo apt-get install pandoc
                """
                    )
                )
            elif os.path.exists("/usr/bin/yum"):
                logger.info(
                    textwrap.dedent(
                        """\
                    Maybe try:

                    sudo yum install pandoc
                """
                    )
                )
            logger.info(
                textwrap.dedent(
                    """\
                See http://johnmacfarlane.net/pandoc/installing.html
                for installation options
            """
                )
            )
            logger.info(
                textwrap.dedent(
                    """\
                ---------------------------------------------------------------

            """
                )
            )
            raise OSError(
                "No pandoc was found: either install pandoc and add it\n"
                "to your PATH or or call pypandoc.download_pandoc(...) or\n"
                "install pypandoc wheels with included pandoc."
            )


def ensure_pandoc_installed(
    url: Union[str, None] = None,
    targetfolder: Union[str, None] = None,
    version: str = "latest",
    delete_installer: bool = False,
) -> None:
    """Try to install pandoc if it isn't installed.

    Parameters are passed to download_pandoc()

    :raises OSError: if pandoc cannot be installed
    """

    # Append targetfolder to the PATH so it is found by subprocesses
    if targetfolder is not None:
        os.environ["PATH"] = (
            os.environ.get("PATH", "")
            + os.pathsep
            + os.path.abspath(os.path.expanduser(targetfolder))
        )

    try:
        _ensure_pandoc_path()

    except OSError:
        download_pandoc(
            url=url,
            targetfolder=targetfolder,
            version=version,
            delete_installer=delete_installer,
        )

        # Show errors in case of secondary failure
        _ensure_pandoc_path()


def url2path(url):
    return urllib.request.url2pathname(urllib.parse.urlparse(url).path)


# -----------------------------------------------------------------------------
# Internal state management
# -----------------------------------------------------------------------------
def clean_version_cache():
    global __version
    __version = None


def clean_pandocpath_cache():
    global __pandoc_path
    __pandoc_path = None


__version = None
__pandoc_path = None
