"""Get a LabVIEW Window netlist from the exported Vivado project."""

# Copyright (c) 2025 National Instruments Corporation
#
# SPDX-License-Identifier: MIT
#


import os
import shutil

from . import common


def _get_window_netlist(config, test=False):
    """Gets the Window netlist from the Vivado Project as well as other HDL Files."""
    get_netlist_tcl_path = os.path.join(
        os.getcwd(), config.vivado_tcl_scripts_folder, "GetWindowNetlist.tcl"
    )
    print(f"Using TCL script at: {get_netlist_tcl_path}")
    current_dir = os.getcwd()

    # Extract project directory and name from the XPR path
    vivado_project_path = os.path.dirname(config.vivado_project_export_xpr)
    project_name = os.path.splitext(os.path.basename(config.vivado_project_export_xpr))[0]

    print(f"Vivado project path: {vivado_project_path}")
    print(f"Vivado project name: {project_name}")

    os.chdir(vivado_project_path)

    # Use the vivado_tools_path from the config instead of the XILINX environment variable
    vivado_path = config.vivado_tools_path

    # Determine the Vivado executable based on the operating system
    if os.name == "nt":  # Windows
        vivado_executable = os.path.join(vivado_path, "bin", "vivado.bat")
    else:  # Linux or other OS
        vivado_executable = os.path.join(vivado_path, "bin", "vivado")

    source_file = os.path.join(vivado_project_path, "TheLvWindowFlatWrapper.v")
    destination_folder = config.the_window_folder_output

    # Create destination directory if it doesn't exist
    os.makedirs(destination_folder, exist_ok=True)

    # In test mode, skip running Vivado
    if test:
        print("TEST MODE: Skipping Vivado execution")

        # In test mode, create a mock Verilog netlist file if it doesn't exist
        if not os.path.exists(source_file):
            with open(source_file, "w") as f:
                f.write("// Mock Verilog netlist file created for testing\n")
            print(f"Created mock Verilog netlist file for testing: {source_file}")
    else:
        print(f"Current working directory: {os.getcwd()}")
        common.run_command(
            f'"{vivado_executable}" {project_name}.xpr -mode batch -source {get_netlist_tcl_path}',
            cwd=os.getcwd(),
            capture_output=False,
        )

        # Check for success marker in vivado.log file
        vivado_log_path = os.path.join(os.getcwd(), "vivado.log")
        if not os.path.exists(vivado_log_path):
            os.chdir(current_dir)
            raise RuntimeError("Vivado log file not found. TCL script execution may have failed.")

        # Read the log file and check for success marker
        try:
            with open(vivado_log_path, "r", encoding="utf-8", errors="replace") as log_file:
                log_content = log_file.read()
                # This GET_WINDOW=FAILED constant is set in the GetWindowNetlist.tcl file
                # We have not found a better way to surface an error from Vivado executing a
                # TCL script up to Python
                if "GET_WINDOW=FAILED" in log_content:
                    os.chdir(current_dir)
                    raise RuntimeError("Window netlist extraction failed.")
        except Exception as e:
            os.chdir(current_dir)
            raise RuntimeError(f"Errors found in Vivado log file: {str(e)}")

        # Check if the expected output file was generated
        if not os.path.exists(source_file):
            os.chdir(current_dir)
            raise RuntimeError(
                f"Vivado TCL script execution failed: Expected output file {source_file} was not generated."
            )

    destination_file = os.path.join(destination_folder, "TheLvWindowFlatWrapper.v")

    try:
        if os.path.exists(source_file):
            shutil.copy(source_file, destination_file)
            print(f"Copied {source_file} to {destination_file}")
        else:
            os.chdir(current_dir)
            raise FileNotFoundError(f"Source file {source_file} not found")
    except Exception as e:
        os.chdir(current_dir)
        raise RuntimeError(f"Error copying file: {str(e)}")

    os.chdir(current_dir)


def _copy_lv_generated_files(config):
    """Copy files that are generated by LV into TheWindow folder for use in the HDL vivado flow."""
    # Extract the parent directory path from the XPR path
    # The NIProtectedFiles folder is typically at the same level as VivadoProject
    project_export_base = os.path.dirname(os.path.dirname(config.vivado_project_export_xpr))
    protected_files_folder = os.path.join(project_export_base, "NIProtectedFiles")

    # This list is hardcoded for now
    #
    # As we expand to future target support, we may need to make the list of LV FPGA generated
    # files to copy more dynamic based on the target
    files_to_copy = [
        "CodeGenerationResults.lvtxt",
        "PkgLvFpgaConst.vhd",
        "PkgCommIntConfiguration.vhd",
        "PkgDmaPortCommIfcRegs.vhd",
        "PkgDmaPortDmaFifos.vhd",
    ]

    for file_name in files_to_copy:
        source_file = os.path.join(protected_files_folder, file_name)
        destination_file = os.path.join(config.the_window_folder_output, file_name)

        try:
            if os.path.exists(source_file):
                shutil.copy(source_file, destination_file)
                print(f"Copied {source_file} to {destination_file}")
            else:
                print(f"Error: Source file {source_file} not found")
        except Exception as e:
            print(f"Error copying file: {str(e)}")


def _extract_lv_window_constraints(config):
    """Extract LabVIEW FPGA constraints from the main constraints.xdc file.

    This function finds the constraints section marked with BEGIN_LV_FPGA_CONSTRAINTS and
    END_LV_FPGA_CONSTRAINTS in the constraints.xdc file, and creates a separate file
    containing just those constraints in the Window folder, excluding the marker lines.

    Args:
        config (FileConfiguration): Configuration settings object
    """
    # Extract the parent directory path from the XPR path
    # The NIProtectedFiles folder is typically at the same level as VivadoProject
    project_export_base = os.path.dirname(os.path.dirname(config.vivado_project_export_xpr))
    protected_files_folder = os.path.join(project_export_base, "NIProtectedFiles")

    source_file = os.path.join(protected_files_folder, "constraints.xdc")
    destination_folder = config.the_window_folder_output
    destination_file = os.path.join(destination_folder, "TheWindowConstraints.xdc")

    # Make sure destination folder exists
    os.makedirs(destination_folder, exist_ok=True)

    # Extract constraints between markers
    try:
        if os.path.exists(source_file):
            with open(source_file, "r", encoding="utf-8") as f_in:
                lines = f_in.readlines()

            # Find the marker lines
            start_idx = None
            end_idx = None

            for i, line in enumerate(lines):
                if "# BEGIN_LV_FPGA_CONSTRAINTS" in line:
                    start_idx = i
                if "# END_LV_FPGA_CONSTRAINTS" in line:
                    end_idx = i
                    break

            # Check if markers were found
            if start_idx is None or end_idx is None:
                print("Warning: Could not find constraint markers in constraints.xdc")
                return

            # Extract the constraints EXCLUDING the marker lines
            lv_constraints = lines[start_idx + 1 : end_idx]

            # Write the constraints to the destination file
            with open(destination_file, "w", encoding="utf-8") as f_out:
                f_out.writelines(lv_constraints)

            print(f"Successfully extracted LV FPGA constraints to {destination_file}")
        else:
            print(f"Error: Source file {source_file} not found")
    except Exception as e:
        print(f"Error extracting constraints: {str(e)}")


def _validate_ini(config, test):
    """Validate that all required configuration settings are present.

    This function checks that all settings required for getting window netlist
    are present in the configuration object and validates that paths exist.

    Args:
        config: Configuration object containing settings from INI file

    Raises:
        ValueError: If any required settings are missing or paths are invalid
    """
    missing_settings = []
    invalid_paths = []

    # Check required paths for window netlist generation
    if not config.vivado_project_export_xpr:
        missing_settings.append("LVWindowNetlistSettings.VivadoProjectExportXPR")
    else:
        # Validate that the XPR file exists
        invalid_path = common.validate_path(
            config.vivado_project_export_xpr,
            "LVWindowNetlistSettings.VivadoProjectExportXPR",
            "file",
        )
        if invalid_path:
            invalid_paths.append(invalid_path)
        if not test:
            # Check that the project folder path length does not exceed 80 characters
            # OK to skip this check in test mode because we are not running Vivado
            vivado_project_path = os.path.dirname(config.vivado_project_export_xpr)
            if len(vivado_project_path) > 80:
                raise ValueError(
                    f"Error: Vivado project path exceeds 80 characters ({len(vivado_project_path)} chars).\n"
                    f"Path: {vivado_project_path}\n"
                    f"Vivado has issues with long paths. Please use a shorter path for your project."
                )

    if not config.the_window_folder_output:
        missing_settings.append("LVWindowNetlistSettings.TheWindowFolder")

    # Check for Vivado tools path
    if not config.vivado_tools_path:
        missing_settings.append("VivadoProjectSettings.VivadoToolsPath")
    else:
        # Validate that the Vivado tools path exists
        invalid_path = common.validate_path(
            config.vivado_tools_path, "VivadoProjectSettings.VivadoToolsPath", "directory"
        )
        if invalid_path:
            invalid_paths.append(invalid_path)

    # Construct error message using common utility functions
    error_msg = common.get_missing_settings_error(missing_settings)
    error_msg += common.get_invalid_paths_error(invalid_paths)

    # If any issues found, raise an error with the helpful message
    if missing_settings or invalid_paths:
        error_msg += "\nPlease update your configuration file and try again."
        raise ValueError(error_msg)


def get_window(test=False, config_path=None):
    """Main entry point for the script.

    Args:
        test (bool): If True, validate settings but don't run Vivado
        config_path (str | None): Optional path to INI settings file
    """
    # Load configuration
    config = common.load_config(config_path)

    # Validate that all required settings are present
    try:
        _validate_ini(config, test)
    except Exception as e:
        print(f"Error: {e}")
        return 1

    _get_window_netlist(config, test=test)
    _copy_lv_generated_files(config)
    _extract_lv_window_constraints(config)

    print("Window netlist extraction completed successfully.")

    print("\n" + "=" * 80)
    print("NOTICE: If you have already created a Vivado project, you must run")
    print('         "nihdl create-project --update" to pull in the latest Window netlist files.')
    print("=" * 80)

    return 0
