#
# SPDX-FileCopyrightText: Copyright (c) 1993-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import glob
import os
from collections import OrderedDict, defaultdict

from polygraphy import constants, mod, util
from polygraphy.logger import G_LOGGER
from polygraphy.tools.base import Tool

algorithm_selector = mod.lazy_import("polygraphy.backend.trt.algorithm_selector")


class DiffTactics(Tool):
    """
    Determine potentially bad TensorRT tactics given sets of good and bad Polygraphy tactic
    replay files, such as those saved by `--save-tactics`.
    """

    def __init__(self):
        super().__init__("diff-tactics")

    def add_parser_args(self, parser):
        parser.add_argument(
            "--dir",
            help="A directory containing good and bad Polygraphy tactic replay files, such as the ones saved by --save-tactics. "
            "By default, this tool will search for files in directories called 'good' and 'bad'",
            default="",
        )
        parser.add_argument(
            "--good",
            help="Either a directory containing good Polygraphy tactic replay files or a single good file. ",
            default=None,
        )
        parser.add_argument(
            "--bad",
            help="Either a directory containing bad Polygraphy tactic replay files or a single bad file. ",
            default=None,
        )

    def run_impl(self, args):
        if args.dir is None and (args.good is None or args.bad is None):
            G_LOGGER.critical(
                "Either `--dir`, or both `--good` and `--bad` must be specified."
            )

        def load_tactics(dirpath):
            """
            Load all tactic replays from the specified directory into a single dictionary.

            Args:
                dirpath (str): Directory containing zero or more tactic replay files.

            Returns:
                dict[str, Set[polygraphy.backend.trt.algorithm_selector.Algorithm]]:
                        Maps layer names to the set of algorithms present in the tactic replays.
            """

            def try_load_replay(path):
                try:
                    return algorithm_selector.TacticReplayData.load(path)
                except:
                    return None

            tactics = defaultdict(set)
            replay_paths = []
            search_paths = (
                glob.iglob(os.path.join(dirpath, "**"), recursive=True)
                if os.path.isdir(dirpath)
                else [dirpath]
            )
            for path in search_paths:
                replay = try_load_replay(path)
                if replay is None:
                    G_LOGGER.verbose(
                        f"{path} does not look like a tactic replay file, skipping."
                    )
                    continue

                replay_paths.append(path)
                for name, algo in replay.items():
                    tactics[name].add(algo)
            return tactics, replay_paths

        good_dir = util.default(args.good, os.path.join(args.dir, "good"))
        good_tactics, good_paths = load_tactics(good_dir)
        G_LOGGER.info(f"Loaded {len(good_paths)} good tactic replays.")
        G_LOGGER.verbose(f"Good tactic replays: {good_paths}")

        bad_dir = util.default(args.bad, os.path.join(args.dir, "bad"))
        bad_tactics, bad_paths = load_tactics(bad_dir)
        G_LOGGER.info(f"Loaded {len(bad_paths)} bad tactic replays.")
        G_LOGGER.verbose(f"Bad tactic replays: {bad_paths}")

        # Walk bad tactics and remove all the known good tactics.
        potential_bad_tactics = OrderedDict()
        for name, algo_set in bad_tactics.items():
            if name in good_tactics:
                algo_set -= good_tactics[name]

            if algo_set:
                potential_bad_tactics[name] = algo_set

        if potential_bad_tactics:
            G_LOGGER.info("Found potentially bad tactics:")
            for name, algo_set in potential_bad_tactics.items():
                algo_set_str = list(map(str, algo_set))
                G_LOGGER.info(
                    f"Layer: {name}\n{constants.TAB}Algorithms: {algo_set_str}"
                )
        else:
            G_LOGGER.info(
                "Could not determine potentially bad tactics. Try providing more tactic replay files if possible."
            )
