#
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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.
#
from __future__ import annotations

import argparse
from typing import Any, Dict, Tuple

import torch

# Define valid optimization levels for TensorRT engine build
VALID_OPTIMIZATION_LEVELS = list(range(6))


def parse_key_value_pairs(string: str) -> Dict[str, str]:
    """Parse a string of comma-separated key-value pairs into a dictionary.

    Args:
        string (str): A string of comma-separated key-value pairs.

    Returns:
        Dict[str, str]: Parsed dictionary of key-value pairs.

    Example:
        >>> parse_key_value_pairs("key1:value1,key2:value2")
        {"key1": "value1", "key2": "value2"}
    """
    parsed = {}

    for key_value_pair in string.split(","):
        if not key_value_pair:
            continue

        key_value_pair = key_value_pair.split(":")
        if len(key_value_pair) != 2:
            raise argparse.ArgumentTypeError(f"Invalid key-value pair: {key_value_pair}. Must have length 2.")
        key, value = key_value_pair
        parsed[key] = value

    return parsed


def add_arguments(parser):
    # Stable Diffusion configuration
    parser.add_argument(
        "--version",
        type=str,
        default="1.4",
        choices=(
            "1.4",
            "dreamshaper-7",
            "xl-1.0",
            "xl-turbo",
            "svd-xt-1.1",
            "sd3",
            "3.5-medium",
            "3.5-large",
            "cascade",
            "flux.1-dev",
            "flux.1-schnell",
            "flux.1-dev-canny",
            "flux.1-dev-depth",
            "flux.1-kontext-dev",
            "cosmos-predict2-2b-text2image",
            "cosmos-predict2-14b-text2image",
            "cosmos-predict2-2b-video2world",
            "cosmos-predict2-14b-video2world",
        ),
        help="Version of Stable Diffusion",
    )
    parser.add_argument("prompt", nargs="*", help="Text prompt(s) to guide image generation")
    parser.add_argument(
        "--negative-prompt", nargs="*", default=[""], help="The negative prompt(s) to guide the image generation."
    )
    parser.add_argument("--batch-size", type=int, default=1, choices=[1, 2, 4], help="Batch size (repeat prompt)")
    parser.add_argument(
        "--batch-count", type=int, default=1, help="Number of images to generate in sequence, one at a time."
    )
    parser.add_argument("--height", type=int, default=512, help="Height of image to generate (must be multiple of 8)")
    parser.add_argument("--width", type=int, default=512, help="Height of image to generate (must be multiple of 8)")
    parser.add_argument("--denoising-steps", type=int, default=30, help="Number of denoising steps")
    parser.add_argument(
        "--scheduler",
        type=str,
        default=None,
        choices=("DDIM", "DDPM", "EulerA", "Euler", "LCM", "LMSD", "PNDM", "UniPC", "DDPMWuerstchen", "FlowMatchEuler"),
        help="Scheduler for diffusion process",
    )
    parser.add_argument(
        "--guidance-scale",
        type=float,
        default=7.5,
        help="Value of classifier-free guidance scale (must be greater than 1)",
    )
    parser.add_argument(
        "--lora-scale",
        type=float,
        default=1.0,
        help="Controls how much to influence the outputs with the LoRA parameters. (must between 0 and 1)",
    )
    parser.add_argument(
        "--lora-weight",
        type=float,
        nargs="+",
        default=None,
        help="The LoRA adapter(s) weights to use with the UNet. (must between 0 and 1)",
    )
    parser.add_argument(
        "--lora-path",
        type=str,
        nargs="+",
        default=None,
        help="Path to LoRA adaptor. Ex: 'latent-consistency/lcm-lora-sdv1-5'",
    )
    parser.add_argument("--bf16", action="store_true", help="Run pipeline in BFloat16 precision")

    # ONNX export
    parser.add_argument(
        "--onnx-opset",
        type=int,
        default=19,
        choices=range(7, 24),
        help="Select ONNX opset version to target for exported models",
    )
    parser.add_argument("--onnx-dir", default="onnx", help="Output directory for ONNX export")
    parser.add_argument(
        "--custom-onnx-paths",
        type=parse_key_value_pairs,
        help=(
            "[FLUX, Stable Diffusion 3.5-large, Cosmos only] Custom override paths to pre-exported ONNX model files. These ONNX models are directly used to "
            "build TRT engines without further optimization on the ONNX graphs. Paths should be a comma-separated list "
            "of <model_name>:<path> pairs. For example: "
            "--custom-onnx-paths=transformer:/path/to/transformer.onnx,vae:/path/to/vae.onnx. Call "
            "<PipelineClass>.get_model_names(...) for the list of supported model names."
        ),
    )
    parser.add_argument(
        "--onnx-export-only",
        action="store_true",
        help="If set, only performs the export of models to ONNX, skipping engine build and inference.",
    )
    parser.add_argument(
        "--download-onnx-models",
        action="store_true",
        help=("[FLUX and Stable Diffusion 3.5-large only] Download pre-exported ONNX models"),
    )

    # Framework model ckpt
    parser.add_argument("--framework-model-dir", default="pytorch_model", help="Directory for HF saved models")

    # TensorRT engine build
    parser.add_argument("--engine-dir", default="engine", help="Output directory for TensorRT engines")
    parser.add_argument(
        "--custom-engine-paths",
        type=parse_key_value_pairs,
        help=(
            "[FLUX only] Custom override paths to pre-built engine files. Paths should be a comma-separated list of "
            "<model_name>:<path> pairs. For example: "
            "--custom-onnx-paths=transformer:/path/to/transformer.plan,vae:/path/to/vae.plan. Call "
            "<PipelineClass>.get_model_names(...) for the list of supported model names."
        ),
    )

    parser.add_argument(
        "--optimization-level",
        type=int,
        default=None,
        help=f"Set the builder optimization level to build the engine with. A higher level allows TensorRT to spend more building time for more optimization options. Must be one of {VALID_OPTIMIZATION_LEVELS}.",
    )
    parser.add_argument(
        "--build-static-batch", action="store_true", help="Build TensorRT engines with fixed batch size."
    )
    parser.add_argument(
        "--build-dynamic-shape", action="store_true", help="Build TensorRT engines with dynamic image shapes."
    )
    parser.add_argument(
        "--build-enable-refit", action="store_true", help="Enable Refit option in TensorRT engines during build."
    )
    parser.add_argument(
        "--build-all-tactics", action="store_true", help="Build TensorRT engines using all tactic sources."
    )
    parser.add_argument(
        "--timing-cache", default=None, type=str, help="Path to the precached timing measurements to accelerate build."
    )
    parser.add_argument("--ws", action="store_true", help="Build TensorRT engines with weight streaming enabled.")

    # Quantization configuration.
    parser.add_argument("--int8", action="store_true", help="Apply int8 quantization.")
    parser.add_argument("--fp8", action="store_true", help="Apply fp8 quantization.")
    parser.add_argument("--fp4", action="store_true", help="Apply fp4 quantization.")
    parser.add_argument(
        "--quantization-level",
        type=float,
        default=0.0,
        choices=[0.0, 1.0, 2.0, 2.5, 3.0, 4.0],
        help="int8/fp8 quantization level, 1: CNN, 2: CNN + FFN, 2.5: CNN + FFN + QKV, 3: CNN + Almost all Linear (Including FFN, QKV, Proj and others), 4: CNN + Almost all Linear + fMHA, 0: Default to 2.5 for int8 and 4.0 for fp8.",
    )
    parser.add_argument(
        "--quantization-percentile",
        type=float,
        default=1.0,
        help="Control quantization scaling factors (amax) collecting range, where the minimum amax in range(n_steps * percentile) will be collected. Recommendation: 1.0.",
    )
    parser.add_argument(
        "--quantization-alpha",
        type=float,
        default=0.8,
        help="The alpha parameter for SmoothQuant quantization used for linear layers. Recommendation: 0.8 for SDXL.",
    )
    parser.add_argument(
        "--calibration-size",
        type=int,
        default=32,
        help="The number of steps to use for calibrating the model for quantization. Recommendation: 32, 64, 128 for SDXL",
    )

    # Inference
    parser.add_argument(
        "--num-warmup-runs", type=int, default=5, help="Number of warmup runs before benchmarking performance"
    )
    parser.add_argument("--use-cuda-graph", action="store_true", help="Enable cuda graph")
    parser.add_argument("--nvtx-profile", action="store_true", help="Enable NVTX markers for performance profiling")
    parser.add_argument(
        "--torch-inference",
        default="",
        help="Run inference with PyTorch (using specified compilation mode) instead of TensorRT.",
    )
    parser.add_argument(
        "--torch-fallback",
        default=None,
        type=str,
        help="[FLUX, SD3.5, and Wan] Comma separated list of models to be inferenced using PyTorch instead of TRT. For example --torch-fallback text_encoder,transformer,transformer_2. If --torch-inference set, this parameter will be ignored.",
    )
    parser.add_argument(
        "--low-vram",
        action="store_true",
        help="[FLUX, SD3.5, and Wan] Optimize for low VRAM usage, possibly at the expense of inference performance. Disabled by default.",
    )
    parser.add_argument("--seed", type=int, default=None, help="Seed for random generator to get consistent results")
    parser.add_argument("--output-dir", default="output", help="Output directory for logs and image artifacts")
    parser.add_argument("--hf-token", type=str, help="HuggingFace API access token for downloading model checkpoints")
    parser.add_argument("-v", "--verbose", action="store_true", help="Show verbose output")
    return parser


def process_pipeline_args(args: argparse.Namespace) -> Tuple[Dict[str, Any], Dict[str, Any], Tuple]:
    """Validate parsed arguments and process argument values.

    Some argument values are resolved or overwritten during processing.

    Args:
        args (argparse.Namespace): Parsed argument. This is modified in-place.

    Returns:
        Dict[str, Any]: Keyword arguments for initializing a pipeline. This is only used in legacy pipelines that do not
            have factory methods `FromArgs` that construct the pipeline directly from the parsed argument.
        Dict[str, Any]: Keyword arguments for calling the `.load_engine` method of the pipeline.
        Tuple: Arguments for calling the `.run` method of the pipeline.
    """

    # GPU device info
    device_info = torch.cuda.get_device_properties(0)
    sm_version = device_info.major * 10 + device_info.minor

    is_flux = args.version.startswith("flux")
    is_sd35 = args.version.startswith("3.5")
    is_wan = args.version.startswith("wan")
    is_cosmos = args.version.startswith("cosmos")

    if args.height % 8 != 0 or args.width % 8 != 0:
        raise ValueError(
            f"Image height and width have to be divisible by 8 but specified as: {args.image_height} and {args.width}."
        )

    # Handle batch size
    max_batch_size = 4
    if args.batch_size > max_batch_size:
        raise ValueError(f"Batch size {args.batch_size} is larger than allowed {max_batch_size}.")

    if args.use_cuda_graph and (not args.build_static_batch or args.build_dynamic_shape):
        raise ValueError(
            "Using CUDA graph requires static dimensions. Enable `--build-static-batch` and do not specify `--build-dynamic-shape`"
        )

    # TensorRT builder optimization level
    if args.optimization_level is None:
        # optimization level set to 3 for all Flux pipelines to reduce GPU memory usage
        if args.int8 or args.fp8 and not is_flux:
            args.optimization_level = 4
        else:
            args.optimization_level = 3

    if args.optimization_level not in VALID_OPTIMIZATION_LEVELS:
        raise ValueError(
            f"Optimization level {args.optimization_level} not valid.  Valid values are: {VALID_OPTIMIZATION_LEVELS}"
        )

    # Quantized pipeline
    # int8 support
    if args.int8 and not any(args.version.startswith(prefix) for prefix in ("xl", "1.4")):
        raise ValueError("int8 quantization is only supported for SDXL and SD1.4 pipelines.")

    # fp8 support validation
    if args.fp8:
        # Check version compatibility
        supported_versions = ("xl", "1.4", "3.5-large")
        if not (any(args.version.startswith(prefix) for prefix in supported_versions) or is_flux):
            raise ValueError(
                "fp8 quantization is only supported for SDXL, SD1.4, SD3.5-large and FLUX pipelines."
            )

        # Check controlnet compatibility
        if getattr(args, "controlnet_type", None) is not None:
            if args.version not in ("xl-1.0", "3.5-large"):
                raise ValueError("fp8 controlnet quantization is only supported for SDXL and SD3.5-large.")
            if args.version == "3.5-large" and args.controlnet_type == "blur":
                raise ValueError("Blur controlnet type is not supported for SD3.5.")
        # Check for conflicting quantization
        if args.int8:
            raise ValueError("Cannot apply both int8 and fp8 quantization, please choose only one.")

        # Check GPU compute capability
        if sm_version < 89:
            raise ValueError(
                f"Cannot apply FP8 quantization for GPU with compute capability {sm_version / 10.0}. A minimum compute capability of 8.9 is required."
            )

        # Check SD3.5-large specific requirement
        if args.version == "3.5-large" and not args.download_onnx_models:
            raise ValueError(
                "Native FP8 quantization is not supported for SD3.5-large. Please pass --download-onnx-models."
            )

    # TensorRT ModelOpt quantization level
    if args.quantization_level == 0.0:
        def override_quant_level(level: float, dtype_str: str):
            args.quantization_level = level
            print(f"[W] The default quantization level has been set to {level} for {dtype_str}.")

        if args.fp8:
            # L4 fp8 fMHA on Hopper not yet enabled.
            if sm_version == 90 and is_flux:
                override_quant_level(3.0, "FP8")
            else:
                override_quant_level(3.0 if args.version == "1.4" else 4.0, "FP8")

        elif args.int8:
            override_quant_level(3.0, "INT8")

    if args.version.startswith("flux") and args.quantization_level == 3.0 and args.download_onnx_models:
        raise ValueError(
            "Transformer ONNX model for Quantization level 3 is not available for download. Please export the quantized Transformer model natively with the removal of --download-onnx-models."
        )
    if args.fp4:
        # FP4 precision is only supported for the Flux pipeline
        assert is_flux, "FP4 precision is only supported for the Flux pipeline"

    # Handle LoRA
    # FLUX canny and depth official LoRAs are not supported because they modify the transformer architecture, conflicting with refit
    if args.lora_path and not any(args.version.startswith(prefix) for prefix in ("xl", "flux.1-dev", "flux.1-schnell")):
        raise ValueError("LoRA adapter support is only supported for SDXL, FLUX.1-dev and FLUX.1-schnell pipelines")

    if args.lora_weight:
        for weight in (weight for weight in args.lora_weight if not 0 <= weight <= 1):
            raise ValueError(f"LoRA adapter weights must be between 0 and 1, provided {weight}")

    if not 0 <= args.lora_scale <= 1:
        raise ValueError(f"LoRA scale value must be between 0 and 1, provided {args.lora_scale}")

    # Force lora merge when fp8 or int8 is used with LoRA
    if args.build_enable_refit and args.lora_path and (args.int8 or args.fp8):
        raise ValueError(
            "Engine refit should not be enabled for quantized models with LoRA. ModelOpt recommends fusing the LoRA to the model before quantization. \
            See https://github.com/NVIDIA/TensorRT-Model-Optimizer/tree/main/examples/diffusers/quantization#lora"
        )

    # Torch-fallback and Torch-inference
    if args.torch_fallback and not args.torch_inference:
        assert (
            is_flux or is_sd35 or is_wan or is_cosmos
        ), "PyTorch Fallback is only supported for Flux, Stable Diffusion 3.5, Wan and Cosmos pipelines."
        args.torch_fallback = args.torch_fallback.split(",")

    if args.torch_fallback and args.torch_inference:
        print(
            "[W] All models will run in PyTorch when --torch-inference is set. Parameter --torch-fallback will be ignored."
        )
        args.torch_fallback = None

    # low-vram
    if args.low_vram:
        assert (
            is_flux or is_sd35 or is_wan or is_cosmos
        ), "low-vram mode is only supported for Flux, Stable Diffusion 3.5, Wan and Cosmos pipelines."

    # Disable SDXL LCM pipeline
    if args.version == "xl-1.0" and args.scheduler == "LCM":
        raise ValueError("SDXL pipeline does not support the LCM scheduler currently. Please use a different scheduler.")

    # Pack arguments
    kwargs_init_pipeline = {
        "version": args.version,
        "max_batch_size": max_batch_size,
        "denoising_steps": args.denoising_steps,
        "scheduler": args.scheduler,
        "guidance_scale": args.guidance_scale,
        "output_dir": args.output_dir,
        "hf_token": args.hf_token,
        "verbose": args.verbose,
        "nvtx_profile": args.nvtx_profile,
        "use_cuda_graph": args.use_cuda_graph,
        "lora_scale": args.lora_scale,
        "lora_weight": args.lora_weight,
        "lora_path": args.lora_path,
        "framework_model_dir": args.framework_model_dir,
        "torch_inference": args.torch_inference,
    }

    kwargs_load_engine = {
        "onnx_opset": args.onnx_opset,
        "opt_batch_size": args.batch_size,
        "opt_image_height": args.height,
        "opt_image_width": args.width,
        "optimization_level": args.optimization_level,
        "static_batch": args.build_static_batch,
        "static_shape": not args.build_dynamic_shape,
        "enable_all_tactics": args.build_all_tactics,
        "enable_refit": args.build_enable_refit,
        "timing_cache": args.timing_cache,
        "int8": args.int8,
        "fp8": args.fp8,
        "fp4": args.fp4,
        "quantization_level": args.quantization_level,
        "quantization_percentile": args.quantization_percentile,
        "quantization_alpha": args.quantization_alpha,
        "calibration_size": args.calibration_size,
        "onnx_export_only": args.onnx_export_only,
        "download_onnx_models": args.download_onnx_models,
    }

    args_run_demo = (
        args.prompt,
        args.negative_prompt,
        args.height,
        args.width,
        args.batch_size,
        args.batch_count,
        args.num_warmup_runs,
        args.use_cuda_graph,
    )

    return kwargs_init_pipeline, kwargs_load_engine, args_run_demo
