"""Orchestrate calls to planarity's testAllGraphs() functionality

Functions:
    call_planarity_testAllGraphs(
        planarity_path: Path,
        canonical_files: bool,
        makeg_g6: bool,
        command: str,
        order: int,
        num_edges: int,
        input_dir: Path,
        output_dir: Path,
    ) -> None

    _validate_and_normalize_planarity_testAllGraphs_workload_args(
        planarity_path: Path, order: int, input_dir: Path, output_dir: Path
    ) -> tuple[Path, int, Path, Path]

    distribute_planarity_testAllGraphs_workload(
        planarity_path: Path,
        canonical_files: bool,
        makeg_g6: bool,
        order: int,
        input_dir: Path,
        output_dir: Path,
    ) -> None
"""

#!/usr/bin/env python

__all__ = ["distribute_planarity_testAllGraphs_workload"]

import sys
import multiprocessing
import subprocess
import argparse
from pathlib import Path
from typing import Optional


from planaritytesting_utils import (
    PLANARITY_ALGORITHM_SPECIFIERS,
    max_num_edges_for_order,
    is_path_to_executable,
)


def call_planarity_testAllGraphs(
    planarity_path: Path,
    canonical_files: bool,
    makeg_g6: bool,
    command: str,
    order: int,
    num_edges: int,
    input_dir: Path,
    output_dir: Path,
    log_root_path: Optional[Path] = None,
) -> None:
    """Call planarity as blocking process on multiprocessing thread

    Uses subprocess.run() to start a blocking process on the multiprocessing
    pool thread to call the planarity executable so that it applies the
    algorithm specified by the command to all graphs in the input file and
    outputs results to the output file, i.e. calls
       planarity -t {command} {infile_path} {outfile_path}

    Args:
        planarity_path: Path to the planarity executable
        canonical_files: Bool to indicate whether or not the .g6 input files
            are in canonical form
        makeg_g6: Bool to indicate whether or not the .g6 input files were
            generated by planarity-backup's embedded makeg
        command: Algorithm specifier character
        order: Desired number of vertices
        num_edges: Desired number of edges
        input_dir: Directory containing the .g6 file with all graphs of the
            given order and num_edges
        output_dir: Directory to which you wish to write the output of applying
            the algorithm corresponding to the command to all graphs in the
            input .g6 file
        log_root_path: Optional path to directory under which logs containing
            output stream results shall be written
    """
    canonical_ext = ".canonical" if canonical_files else ""
    makeg_ext = ".makeg" if makeg_g6 else ""
    stem = f"n{order}.m{num_edges}{makeg_ext}{canonical_ext}"
    infile_path = Path.joinpath(
        input_dir, f"{stem}.g6"
    )
    outfile_path = Path.joinpath(
        output_dir,
        f"{command}",
        f"{stem}.{command}.out.txt",
    )
    if log_root_path is not None:
        logfile_path = Path.joinpath(log_root_path, f"{command}", f"{stem}.{command}.log")
        with open(logfile_path, "w") as logfile:
            subprocess.run(
                [
                    f"{planarity_path}",
                    "-t",
                    f"-{command}",
                    f"{infile_path}",
                    f"{outfile_path}",
                ],
                stdout=logfile,
                stderr=subprocess.STDOUT,
                check=False,
            )
    else:
        subprocess.run(
            [
                f"{planarity_path}",
                "-t",
                f"-{command}",
                f"{infile_path}",
                f"{outfile_path}",
            ],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            check=False,
        )


def _validate_and_normalize_planarity_testAllGraphs_workload_args(  # pylint: disable=too-many-branches
    planarity_path: Path, order: int, input_dir: Path, output_dir: Path
) -> tuple[Path, int, Path, Path]:
    """Validates and normalizes args provided to distribute_planarity_workload

    Ensures planarity_path corresponds to an executable, that order is an
    integer in closed interval [2, 12], that input_dir is a valid Path that
    corresponds to a directory containing .g6 files, and that output_dir is a
    valid Path specifying where results from executing planarity should be
    output.

    Args:
        planarity_path: Path to the planarity executable
        order: Desired number of vertices
        input_dir: Directory containing all graphs of the desired order,
            with each file containing all graphs of a specific edge-count.
            If none provided, defaults to:
                TestSupport/results/graph_generation_orchestrator/{order}
        output_dir: Directory to which the results of executing planarity Test
            All Graphs for the respective command on each .g6 file will be
            written. If none provided, defaults to:
                TestSupport/results/planarity_testAllGraphs_orchestrator/{order}

    Raises:
        argparse.ArgumentTypeError: If any of the args passed from the command
            line are determined invalid under more specific scrutiny

    Returns:
        A tuple comprised of the planarity_path, order, input_dir, and
        output_dir
    """
    if not is_path_to_executable(planarity_path):
        raise argparse.ArgumentTypeError(
            f"Path for planarity executable '{planarity_path}' does not "
            "correspond to an executable."
        )

    if not order or not isinstance(order, int) or order < 2 or order > 12:
        raise argparse.ArgumentTypeError(
            "Graph order must be an integer between 2 and 12."
        )

    if not input_dir:
        test_support_dir = Path(sys.argv[0]).resolve().parent.parent
        input_dir = Path.joinpath(
            test_support_dir,
            "results",
            "graph_generation_orchestrator",
            f"{order}",
        )

    if not isinstance(input_dir, Path) or not input_dir.is_dir():
        raise argparse.ArgumentTypeError("Input directory path is invalid.")

    if input_dir.is_dir() and not any(Path(input_dir).iterdir()):
        raise argparse.ArgumentTypeError("Input dir exists, but is empty.")

    input_dir = input_dir.resolve()
    try:
        candidate_order_from_path = (int)(input_dir.parts[-1])
    except ValueError:
        pass
    except IndexError as e:
        raise argparse.ArgumentTypeError(
            f"Unable to extract parts from input dir path '{input_dir}'."
        ) from e
    else:
        if candidate_order_from_path != order:
            raise argparse.ArgumentTypeError(
                f"Input directory '{input_dir}' seems to indicate "
                f"graph order should be '{candidate_order_from_path}'"
                f", which does not mach order from command line args "
                f"'{order}'. Please verify your command line args and retry."
            )

    if not output_dir:
        test_support_dir = Path(sys.argv[0]).resolve().parent.parent
        output_parent_dir = Path.joinpath(
            test_support_dir, "results", "planarity_testAllGraphs_orchestrator"
        )
        candidate_output_dir = Path.joinpath(output_parent_dir, f"{order}")
        output_dir = candidate_output_dir
    elif not isinstance(output_dir, Path) or output_dir.is_file():
        raise argparse.ArgumentTypeError("Output directory path is invalid.")

    output_dir = output_dir.resolve()
    try:
        candidate_order_from_path = (int)(output_dir.parts[-1])
    except ValueError:
        output_dir = Path.joinpath(output_dir, str(order))
    except IndexError as e:
        raise argparse.ArgumentTypeError(
            f"Unable to extract parts from output dir path '{output_dir}'."
        ) from e
    else:
        if candidate_order_from_path != order:
            raise argparse.ArgumentTypeError(
                f"Output directory '{output_dir}' seems to indicate "
                f"graph order should be '{candidate_order_from_path}'"
                f", which does not mach order from command line args "
                f"'{order}'. Please verify your command line args and retry."
            )

    Path.mkdir(output_dir, parents=True, exist_ok=True)

    return planarity_path, order, input_dir, output_dir


def distribute_planarity_testAllGraphs_workload(  # pylint: disable=too-many-arguments
    planarity_path: Path,
    canonical_files: bool,
    makeg_g6: bool,
    order: int,
    input_dir: Path,
    output_dir: Path,
    log_output_streams: bool = False,
) -> None:
    """Use starmap_async on multiprocessing pool to _call_planarity

    Args:
        planarity_path: Path to the planarity executable
        canonical_files: Bool to indicate whether or not the .g6 input files
            are in canonical form
        makeg_g6: Bool to indicate whether or not the .g6 input files were
            generated by planarity-backup's embedded makeg
        order: Desired number of vertices
        input_dir: Directory containing all graphs of the desired order,
            with each file containing all graphs of a specific edge-count
        output_dir: Directory in which we will create one subdirectory per
            graph algorithm command, where the results of executing planarity
            Test All Graphs for the respective command on each .g6 file will be
            written
        log_output_streams: determines whether we throw away output streams or
            write to file
    """
    planarity_path, order, input_dir, output_dir = (
        _validate_and_normalize_planarity_testAllGraphs_workload_args(
            planarity_path, order, input_dir, output_dir
        )
    )

    log_root_path = Path.joinpath(output_dir, "logs") if log_output_streams else None
    
    for command in PLANARITY_ALGORITHM_SPECIFIERS():
        path_to_make = Path.joinpath(output_dir, f"{command}")
        Path.mkdir(path_to_make, parents=True, exist_ok=True)
        if log_root_path is not None:
            path_to_make = Path.joinpath(log_root_path, f"{command}")
            Path.mkdir(path_to_make, parents=True, exist_ok=True)

    call_planarity_args = [
        (
            planarity_path,
            canonical_files,
            makeg_g6,
            command,
            order,
            num_edges,
            input_dir,
            output_dir,
            log_root_path,
        )
        for num_edges in range(max_num_edges_for_order(order) + 1)
        for command in PLANARITY_ALGORITHM_SPECIFIERS()
    ]

    with multiprocessing.Pool(processes=multiprocessing.cpu_count()) as pool:
        _ = pool.starmap_async(
            call_planarity_testAllGraphs, call_planarity_args
        )
        pool.close()
        pool.join()


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        formatter_class=argparse.RawTextHelpFormatter,
        usage="python %(prog)s [options]",
        description="Planarity testAllGraphs execution orchestrator\n\n"
        "Orchestrates calls to planarity's Test All Graphs "
        "functionality.\n"
        "Expects input directory to contain a subdirectory whose name is "
        "the order containing .g6 files to be tested. Each .g6 file "
        "contains all graphs of the given order with a specific number of "
        "edges:\n"
        "\t{input_dir}/{order}/n{order}.m{num_edges}(.makeg)?"
        "(.canonical)?.g6\n\n"
        "Output files will have paths:\n"
        "\t{output_dir}/{order}/{command}/n{order}.m{num_edges}(.makeg)?"
        "(.canonical)?.{command}.out.txt",
    )
    parser.add_argument(
        "-p",
        "--planaritypath",
        type=Path,
        metavar="PATH_TO_PLANARITY_EXECUTABLE",
    )
    parser.add_argument(
        "-l",
        "--canonicalfiles",
        action="store_true",
        help="Indicates .g6 input files are in canonical form",
    )
    parser.add_argument(
        "-m",
        "--makegfiles",
        action="store_true",
        help="Indicates .g6 input files were generated by makeg",
    )
    parser.add_argument("-n", "--order", type=int, metavar="N", default=11)
    parser.add_argument(
        "-i",
        "--inputdir",
        type=Path,
        default=None,
        metavar="DIR_CONTAINING_G6_FILES",
        help="If no input directory provided, defaults to\n"
        "\tTestSupport/results/graph_generation_orchestrator/{order}",
    )
    parser.add_argument(
        "-o",
        "--outputdir",
        type=Path,
        default=None,
        metavar="DIR_FOR_RESULTS",
        help="If no output directory provided, defaults to\n"
        "\tTestSupport/results/planarity_testAllGraphs_orchestrator/"
        "{order}",
    )
    parser.add_argument(
        "-w",
        "--writestreams",
        action="store_true",
        help="Write stdout/stderr to logfile. Defaults to False, but if true,"
        "writes to:"
        "\tTestSupport/results/planarity_testAllGraphs_orchestrator/{order}/logs",
    )

    args = parser.parse_args()

    distribute_planarity_testAllGraphs_workload(
        planarity_path=args.planaritypath,
        canonical_files=args.canonicalfiles,
        makeg_g6=args.makegfiles,
        order=args.order,
        input_dir=args.inputdir,
        output_dir=args.outputdir,
        log_output_streams=args.writestreams,
    )
