#/*
# * Copyright (c) 2021 Xilinx Inc. All rights reserved.
# * Copyright (c) 2023, Advanced Micro Devices, Inc. All rights reserved.
# *
# * Author:
# *       Appana Durga Kedareswara rao <appana.durga.rao@xilinx.com>
# *       Naga Sureshkumar Relli <naga.sureshkumar.relli@xilinx.com>
# *
# * SPDX-License-Identifier: BSD-3-Clause
# */

import copy
import getopt
import glob
import os
import re
import struct
import subprocess
import sys
import types
from collections import OrderedDict
from pathlib import Path, PurePath
from re import *

import yaml

import lopper
from lopper import Lopper, LopperFmt, LopperSDT
from lopper.log import (__logger__, _debug, _error, _info, _init, _level,
                        _warning)
from lopper.tree import LopperNode, LopperTree

sys.path.append(os.path.dirname(__file__))
_init(__name__)
from baremetalconfig_xlnx import *


def is_compat( node, compat_string_to_test ):
    if re.search( "module,xlnx_overlay_pl_dt", compat_string_to_test):
        return xlnx_generate_overlay_dt
    return ""

def get_label(sdt, symbol_node, node):
    prop_dict = Lopper.node_properties_as_dict(sdt.FDT, symbol_node.abs_path, False)
    match = [label for label,node_abs in prop_dict.items() if re.match(node_abs[0], node.abs_path) and len(node_abs[0]) == len(node.abs_path)]
    if match:
        return match[0]
    else:
        return None

def remove_node_ref(sdt, tgt_node, ref_node):
    prop_dict = ref_node.__props__.copy()
    match_label_list = []
    for node in sdt.tree[tgt_node].subnodes():
        matched_label = get_label(sdt, ref_node, node)
        if matched_label:
            match_label_list.append(matched_label)
    for prop,node1 in prop_dict.items():
        if prop not in match_label_list:
            sdt.tree['/' + ref_node.name].delete(prop)

match_list = ["v_tc", "v_smpte_uhdsdi_tx", "v_smpte_uhdsdi_rx", "v_hdmi_tx1", "v_hdmi_rx1",
              "v_hdmi_rx", "v_hdmi_tx", "v_dp_tx", "v_dp_rx", "v_dp_rx1", "v_dp_tx1"]

def add_status_disabled_after_ipname(filepath, match_list):
    """
    This API adds `status = "disabled";` after each line matching `xlnx,ip-name = "{name}";`
    from the provided match_list. Preserves indentation.

    Args:
        filepath (str): Path to the input DTS/DTSI file.
        match_list (List[str]): List of IP names to match.
    """
    with open(filepath, "r") as f:
        lines = f.readlines()

    modified_lines = []
    for line in lines:
        modified_lines.append(line)
        stripped_line = line.strip()

        for name in match_list:
            if stripped_line == f'xlnx,ip-name = "{name}";':
                indent = line[:len(line) - len(line.lstrip())]
                modified_lines.append(f'{indent}status = "disabled";\n')
                break

    with open(filepath, "w") as f:
        f.writelines(modified_lines)

def infer_platform_from_sdt(sdt):
    """
    Infer the platform from the system device tree by parsing the family property.

    Args:
        sdt: System device tree object

    Returns:
        str: Inferred processor name or None if unable to determine
    """
    try:
        # Map family to processor names
        family_to_processor = {
            'Zynq': 'cortexa9',
            'ZynqMP': 'cortexa53',
            'Versal': 'cortexa72',
            'VersalNet': 'cortexa78',
            'Versal_2VE_2VM': 'cortexa78'
        }

        # Try to infer from the 'family' property at the root node
        family = sdt.tree['/'].propval('family')[0]
        if family in family_to_processor:
            processor = family_to_processor[family]
            print(f"Automatically inferred processor '{processor}' from family property '{family}'")
            return processor

        # Fallback: infer from CPU compatible strings
        for cpu_node in sdt.tree.nodes('/cpu.*@.*'):
            compatible = cpu_node.propval('compatible')[0]
            if 'cortex-a9' in compatible:
                print("Automatically inferred processor 'cortexa9' from CPU compatible string")
                return 'cortexa9'
            elif 'cortex-a53' in compatible:
                print("Automatically inferred processor 'cortexa53' from CPU compatible string")
                return 'cortexa53'
            elif 'cortex-a72' in compatible:
                print("Automatically inferred processor 'cortexa72' from CPU compatible string")
                return 'cortexa72'
            elif 'cortex-a78' in compatible:
                print("Automatically inferred processor 'cortexa78' from CPU compatible string")
                return 'cortexa78'

    except Exception as e:
        _error(f"Failed to infer platform from system device tree: {e}")

    return None

def usage():
    print('Usage: lopper -O <output_dir> -f --enhanced <system-top.dts> [<lopper-gen DT>] -- xlnx_overlay_pl_dt [<machine>] <configuration> [<pl.dtsi>] [options]')
    print('\nArguments:')
    print('  <output_dir>      - Directory where the lopper generated overlay `pl.dtsi` file will be stored')
    print('  <system-top.dts>  - Full system device tree source')
    print('  <lopper-gen DT>   - Lopper-generated system device tree file (excluding PL node)')
    print('  <machine>         - (Optional) Target architecture: cortexa9 | cortexa53 | cortexa72 | cortexa78')
    print('                      (or long forms: cortexa9-zynq | cortexa53-zynqmp | cortexa72-versal | cortexa78-versalnet)')
    print('                      If not provided, will be automatically inferred from system device tree')
    print('  <configuration>   - Configuration type: full | segmented | dfx | external-fpga-config')
    print('  <pl.dtsi>         - (Optional, for backward compatibility only) pl.dtsi file path - IGNORED')
    print('                      This argument is accepted but not used. The assist reads pl.dtsi from system device tree.')
    print('\nOptions:')
    print('  --firmware-name=<name> - Override the default firmware-name in the output')
    print('  --nodes=<node-spec>[;<node-spec>;...] - Specify nodes to include in overlay')
    print('                      Overrides default /amba_pl extraction when specified.')
    print('                      Semicolons separate node specs; commas separate properties.')
    print('                      Node spec format:')
    print('                        <node>           - Include entire node (by path or label)')
    print('                        <node>#<prop>,<prop>,... - Include only specific properties')
    print('                      Examples:')
    print('                        --nodes=/amba_pl                    - Extract entire /amba_pl node')
    print('                        --nodes=mmi_dc#clocks,clock-names   - Only clocks/clock-names from mmi_dc')
    print('                        --nodes=/amba_pl;mmi_dc#clocks      - Combine whole node and property specs')
    print('  --exclude-props=<prop>[,<prop>,...] - Comma-separated list of properties to exclude from overlay')
    print('                      These properties will be removed from the generated overlay output.')
    print('                      Example: --exclude-props=address-map,interrupt-map')
    print('  --exclude-nodes=<node>[,<node>,...] - Comma-separated list of node paths/labels to exclude')
    print('                      These nodes will be removed from the generated overlay output.')
    print('                      Example: --exclude-nodes=/amba_pl/some_node')
    print('\nExamples:')
    print(' With machine argument:')
    print(' lopper -O <output_dir>/ -f --enhanced <path_to_system_top>/system-top.dts <path_to_lopper_gen_dt>/lopper-gen.dts -- xlnx_overlay_pl_dt <machine> <config> <path_to_pl_dtsi>/pl.dtsi')
    print('')
    print(' With automatic platform inference:')
    print(' lopper -O <output_dir>/ -f --enhanced <path_to_system_top>/system-top.dts <path_to_lopper_gen_dt>/lopper-gen.dts -- xlnx_overlay_pl_dt <config> <path_to_pl_dtsi>/pl.dtsi')
    print('')
    print(' With explicit node selection:')
    print(' lopper -O <output_dir>/ -f --enhanced system-top.dts out.dts -- xlnx_overlay_pl_dt cortexa78_0 full --nodes="mmi_dc#clocks,clock-names;phy0"')

def parse_node_specs(specs_str):
    """
    Parse node specification string into structured format.

    Format: <node-spec>[;<node-spec>;...]
    Where node-spec is either:
      - <node>              : Include entire node (path like /amba_pl or label like mmi_dc)
      - <node>#<prop>[,<prop>,...] : Include only specific properties

    Semicolons separate node specs; commas separate properties within a node spec.

    Args:
        specs_str: The specification string (value after --nodes=)

    Returns:
        list of dict: [{'node': str, 'properties': list or None}, ...]
                      properties is None for whole-node extraction

    Examples:
        '/amba_pl'                    -> [{'node': '/amba_pl', 'properties': None}]
        'mmi_dc#clocks,clock-names'   -> [{'node': 'mmi_dc', 'properties': ['clocks', 'clock-names']}]
        'mmi_dc#clocks;phy0'          -> [{'node': 'mmi_dc', 'properties': ['clocks']},
                                          {'node': 'phy0', 'properties': None}]
    """
    result = []

    # Split on semicolons to get individual node specs
    node_specs = specs_str.split(';')

    for spec in node_specs:
        spec = spec.strip()
        if not spec:
            continue

        if '#' in spec:
            # Node with specific properties
            parts = spec.split('#', 1)
            node_name = parts[0].strip()
            props_part = parts[1].strip()
            props = [p.strip() for p in props_part.split(',') if p.strip()]
            result.append({'node': node_name, 'properties': props if props else None})
        else:
            # Whole node extraction
            result.append({'node': spec, 'properties': None})

    return result


def validate_and_parse_options(options, sdt):
    """
    Parse and validate platform and configuration options.
    Supports multiple invocation modes:
    - Mode 1 (1 arg):  <config> (machine auto-inferred)
    - Mode 2 (2 args): <machine> <config> OR <config> <pl.dtsi> (backward compatibility)
    - Mode 3 (3 args): <machine> <config> <pl.dtsi> (backward compatibility)
    - Optional: --firmware-name=<name> to override firmware name

    Note: pl.dtsi argument is accepted for backward compatibility but ignored with warning.

    Args:
        options: Dictionary containing 'args' list with platform and config parameters
        sdt: System device tree object for auto-inference

    Returns:
        tuple: (platform, config, zynq_platforms, versal_platforms, firmware_override)

    Raises:
        SystemExit: If validation fails for platform or config
    """
    # Define valid options
    valid_configs = ["full", "segmented", "dfx", "external-fpga-config"]
    valid_platforms = ["cortexa9-zynq", "cortexa9", "cortexa53-zynqmp", "cortexa53",
                       "cortexa72-versal", "cortexa72", "cortexa78-versalnet", "cortexa78",
                       "cortexa9_0", "psu_cortexa53_0", "cortexa53_0",
                       "psv_cortexa72_0", "cortexa72_0",
                       "psx_cortexa78_0", "cortexa78_0"]
    zynq_platforms = ["cortexa53-zynqmp", "cortexa53", "cortexa9-zynq", "cortexa9",
                      "cortexa9_0", "psu_cortexa53_0", "cortexa53_0"]
    versal_platforms = ["cortexa72-versal", "cortexa72", "cortexa78-versalnet", "cortexa78",
                        "psv_cortexa72_0", "cortexa72_0", "psx_cortexa78_0", "cortexa78_0"]

    # Separate positional arguments from optional arguments
    positional_args = []
    optional_args = []

    for arg in options['args']:
        if arg.startswith("--"):
            optional_args.append(arg)
        else:
            positional_args.append(arg)

    # Process optional arguments
    firmware_override = None
    node_specs = None
    exclude_props = None
    exclude_nodes = None
    for arg in optional_args:
        if arg.startswith("--firmware-name="):
            firmware_override = arg.split("=", 1)[1]
        elif arg.startswith("--nodes="):
            node_specs = parse_node_specs(arg.split("=", 1)[1])
        elif arg.startswith("--exclude-props="):
            exclude_props = [p.strip() for p in arg.split("=", 1)[1].split(",") if p.strip()]
        elif arg.startswith("--exclude-nodes="):
            exclude_nodes = [n.strip() for n in arg.split("=", 1)[1].split(",") if n.strip()]

    # Parse arguments based on count
    try:
        num_args = len(positional_args)

        if num_args == 3:
            # Mode 1 (backward compatibility): <machine> <config> <pl.dtsi>
            platform = positional_args[0]
            config = positional_args[1]
            _warning(f"pl.dtsi argument '{positional_args[2]}' is provided for backward compatibility but will be ignored.")
            _warning(f"The assist reads /amba_pl node from the system device tree instead.")

        elif num_args == 2:
            # Mode 2: Could be <machine> <config> OR <config> <pl.dtsi> (backward compatibility)
            # Detect by checking if second arg is a valid config or looks like a file path
            if positional_args[1] in valid_configs:
                # <machine> <config>
                platform = positional_args[0]
                config = positional_args[1]
            else:
                # <config> <pl.dtsi> (backward compatibility)
                config = positional_args[0]
                _warning(f"pl.dtsi argument '{positional_args[1]}' is provided for backward compatibility but will be ignored.")
                _warning(f"The assist reads /amba_pl node from the system device tree instead.")

                # Auto-infer platform
                platform = infer_platform_from_sdt(sdt)
                if platform is None:
                    _error(f"Unable to automatically infer platform from system device tree.")
                    _error(f"Please provide the machine argument explicitly.")
                    usage()
                    sys.exit(1)

        elif num_args == 1:
            # Mode 3: <config> only (auto-infer machine)
            config = positional_args[0]

            # Auto-infer platform
            platform = infer_platform_from_sdt(sdt)
            if platform is None:
                _error(f"Unable to automatically infer platform from system device tree.")
                _error(f"Please provide the machine argument explicitly.")
                usage()
                sys.exit(1)

        else:
            _error(f"Invalid number of arguments. Expected 1, 2, or 3 positional arguments, got {num_args}")
            usage()
            sys.exit(1)

    except (KeyError, IndexError) as e:
        _error(f"Failed to parse arguments: {e}")
        usage()
        sys.exit(1)

    # Validate config argument
    if config not in valid_configs:
        _error(f"Invalid configuration '{config}'.")
        _error(f"Supported configurations: {', '.join(valid_configs)}")
        usage()
        sys.exit(1)

    # Validate platform argument
    if platform not in valid_platforms:
        _error(f"Invalid platform '{platform}'.")
        _error(f"Supported platforms: {', '.join(valid_platforms)}")
        usage()
        sys.exit(1)

    return platform, config, zynq_platforms, versal_platforms, firmware_override, node_specs, exclude_props, exclude_nodes

def validate_amba_pl_node(amba_node):
    """
    Validate that amba_pl node has valid PL nodes with register properties.

    Args:
        amba_node: The /amba_pl node from the device tree

    Returns:
        bool: True if valid PL nodes exist, False otherwise
    """
    has_valid_pl = False
    for subnode in amba_node.subnodes():
        if subnode.propval('reg') != ['']:
            has_valid_pl = True
            break

    if not has_valid_pl:
        _error(f"No valid PL nodes found in amba_pl (no nodes with reg properties)")

    return has_valid_pl

def prepare_amba_node(amba_node, sdt, tgt_node):
    """
    Prepare amba node for overlay by cloning, removing from source tree, and cleaning up.

    Args:
        amba_node: The /amba_pl node from the device tree
        sdt: The system device tree object
        tgt_node: The target node for reference removal

    Returns:
        LopperNode: The prepared amba node ready for overlay
    """
    # Clone the amba_pl node
    new_amba_node = amba_node()

    # Remove amba_pl node from source tree
    sdt.tree = sdt.tree - amba_node

    # Remove "amba_pl" references from __symbols__ and aliases
    try:
        remove_node_ref(sdt, tgt_node, sdt.tree['/__symbols__'])
    except KeyError:
        _warning(f"No __symbols__ node found in device tree")

    try:
        remove_node_ref(sdt, tgt_node, sdt.tree['/aliases'])
    except KeyError:
        _warning(f"No aliases node found in device tree")

    # Rename the new node in the overlay
    new_amba_node.name = "&amba_pl"
    new_amba_node.label = ""

    # Delete structural properties that should only exist in the base device tree
    new_amba_node.delete("ranges")
    new_amba_node.delete("compatible")
    new_amba_node.delete("#address-cells")
    new_amba_node.delete("#size-cells")

    return new_amba_node

def create_fpga_node(platform, config, firmware_name, zynq_platforms, versal_platforms):
    """
    Create and configure FPGA node based on platform and configuration type.

    Args:
        platform: Target platform (e.g., cortexa53-zynqmp, cortexa72-versal)
        config: Configuration type (full, dfx, external-fpga-config)
        firmware_name: Property containing firmware name value
        zynq_platforms: List of Zynq/ZynqMP platform identifiers
        versal_platforms: List of Versal/VersalNet platform identifiers

    Returns:
        tuple: (fpga_node, fpga_node_name)

    Raises:
        SystemExit: If unsupported platform/config combination is detected
    """
    # Determine FPGA node name based on platform
    if platform in zynq_platforms:
        fpga_node_name = "&fpga_full"
    else:
        fpga_node_name = "&fpga"

    # Create FPGA node
    fpga_node = LopperNode(name=fpga_node_name)

    # Configure fpga node based on platform and configuration:
    # - "full" config: Uses firmware-name for all platforms
    # - "dfx" config: Uses firmware-name for ZynqMP, external-fpga-config for Versal
    # - "external-fpga-config": Always uses external-fpga-config property
    if config == "full":
        fpga_node["firmware-name"] = firmware_name.value
    elif config == "dfx":
        if platform in zynq_platforms:
            # ZynqMP/Zynq uses firmware-name for DFX
            fpga_node["firmware-name"] = firmware_name.value
        elif platform in versal_platforms:
            # Versal/VersalNet uses external-fpga-config for DFX
            fpga_node["external-fpga-config"] = None
        else:
            _error(f'Unsupported platform for DFX: {platform}')
            sys.exit(1)
    elif config == "external-fpga-config":
        fpga_node["external-fpga-config"] = None
    else:
        _error(f'Invalid configuration: {config}')
        sys.exit(1)

    return fpga_node, fpga_node_name

def collect_special_nodes(amba_node):
    """
    Collect special nodes from amba node that need to be moved to fpga node.

    Categorizes subnodes into:
    - misc_clk_*: Miscellaneous clock nodes
    - fpga_PR*/fpga-PR*: Partial reconfiguration regions
    - afi*: AFI configuration nodes
    - clocking*: Clock configuration nodes

    Args:
        amba_node: The amba node containing subnodes to categorize

    Returns:
        dict: Dictionary with keys 'misc_clk', 'fpga_pr', 'afi', 'clocking',
              each containing a list of matching nodes
    """
    misc_clk_nodes = []
    fpga_pr_nodes = []
    afi_nodes = []
    clocking_nodes = []

    for subnode in amba_node.subnodes():
        if subnode.name.startswith("misc_clk_"):
            misc_clk_nodes.append(subnode)
        elif subnode.name.startswith("fpga_PR") or subnode.name.startswith("fpga-PR"):
            fpga_pr_nodes.append(subnode)
        elif subnode.name.startswith("clocking"):
            clocking_nodes.append(subnode)
        elif subnode.name.startswith("afi"):
            afi_nodes.append(subnode)

    return {
        'misc_clk': misc_clk_nodes,
        'fpga_pr': fpga_pr_nodes,
        'afi': afi_nodes,
        'clocking': clocking_nodes
    }

def move_nodes_to_fpga(new_amba_node, fpga_node, node_collections, platform, config, zynq_platforms):
    """
    Move special nodes from amba node to fpga node based on platform and configuration.

    Always moves:
    - misc_clk nodes (all platforms)
    - fpga_pr nodes (all platforms)

    Conditionally moves (ZynqMP/Zynq platforms with "full" or "dfx" config only):
    - clocking nodes
    - afi nodes

    Args:
        new_amba_node: The amba node to remove nodes from
        fpga_node: The fpga node to add nodes to
        node_collections: Dictionary containing categorized node lists
        platform: Target platform identifier
        config: Configuration type (full, dfx, external-fpga-config)
        zynq_platforms: List of Zynq/ZynqMP platform identifiers

    Returns:
        tuple: (updated_amba_node, updated_fpga_node)
    """
    # Move misc_clk nodes (always)
    if node_collections['misc_clk']:
        for clk_node in node_collections['misc_clk']:
            new_amba_node = new_amba_node - clk_node
            fpga_node = fpga_node + clk_node

    # Move fpga_pr nodes (always)
    if node_collections['fpga_pr']:
        for pr_node in node_collections['fpga_pr']:
            new_amba_node = new_amba_node - pr_node
            fpga_node = fpga_node + pr_node

    # For ZynqMP/Zynq platforms with "full" or "dfx" config, move AFI and clocking to fpga node
    if (platform in zynq_platforms) and config != "external-fpga-config":
        if node_collections['clocking']:
            for clk_node in node_collections['clocking']:
                new_amba_node = new_amba_node - clk_node
                fpga_node = fpga_node + clk_node

        if node_collections['afi']:
            for afi_node in node_collections['afi']:
                new_amba_node = new_amba_node - afi_node
                fpga_node = fpga_node + afi_node

    return new_amba_node, fpga_node

def build_overlay_tree(new_amba_node, fpga_node, fpga_node_name, base_tree,
                       exclude_props=None, exclude_nodes=None):
    """
    Build overlay tree from amba and fpga nodes, establishing overlay relationship.

    Creates a new overlay tree, adds nodes, establishes overlay relationship with
    base tree, reorders nodes, and resolves all references. overlay_of() also
    automatically adds &label fragments for any user overlay files (-i) registered
    on base_tree via fragment_add_for_overlay_subtrees().

    Args:
        new_amba_node: The prepared amba node for overlay
        fpga_node: The configured fpga node
        fpga_node_name: The name of the fpga node (e.g., "&fpga" or "&fpga_full")
        base_tree: The base device tree to overlay against

    Returns:
        LopperTree: The constructed and resolved overlay tree

    Raises:
        SystemExit: If node reordering fails
    """
    # Create overlay tree
    overlay_tree = LopperTree()

    # Resolve the fpga_node before adding to overlay_tree
    fpga_node.resolve()

    # Add nodes to overlay_tree
    overlay_tree = overlay_tree + new_amba_node
    overlay_tree = overlay_tree + fpga_node

    # Establish overlay relationship to resolve phandle references against base tree.
    # overlay_of() calls fragment_add_for_refs(), which covers both phandle
    # cross-references and user overlay files (-i) registered on base_tree.
    overlay_tree.overlay_of(base_tree, exclude_props=exclude_props, exclude_nodes=exclude_nodes)

    # Reorder nodes: fpga node should come after amba node
    try:
        overlay_tree['/'].reorder_child("/&amba_pl", "/" + fpga_node_name, after=True, debug=True)
    except Exception as e:
        _error(f"Reordering nodes: {e}")
        sys.exit(1)

    # Resolve all phandle references and finalize tree structure
    overlay_tree.resolve()

    return overlay_tree

def clean_overlay_properties(overlay_tree):
    """
    Remove phandle and status="okay" properties from all nodes in overlay tree.

    Phandles should not be in overlays as they are resolved against the base tree.
    Status="okay" should not be in overlays as devices are enabled by default.

    Args:
        overlay_tree: The overlay tree to clean properties from

    Note:
        Modifies overlay_tree in place
    """
    for path, node in overlay_tree.__nodes__.items():
        # Remove phandle properties
        if node.phandle and node.phandle > 0:
            # Set phandle to 0 to prevent it from being printed
            node.phandle = 0
            # Also delete the property if it exists
            try:
                node.delete("phandle")
            except:
                pass

        # Remove status = "okay" property
        try:
            status_prop = node.propval('status')
            if status_prop and status_prop[0] == 'okay':
                node.delete('status')
        except:
            pass

def build_overlay_from_node_specs(sdt, node_specs, base_tree, exclude_props=None, exclude_nodes=None):
    """
    Build an overlay tree from explicit node specifications.

    This is an alternative to the default /amba_pl extraction, allowing users
    to specify exactly which nodes and/or properties should be in the overlay.

    Args:
        sdt: System device tree object
        node_specs: List of node specs from parse_node_specs()
                    [{'node': str, 'properties': list or None}, ...]
        base_tree: The base tree to establish overlay relationship with

    Returns:
        LopperTree: The constructed overlay tree
    """
    overlay_tree = LopperTree()

    for spec in node_specs:
        node_ref = spec['node']
        properties = spec['properties']

        if node_ref.startswith('/'):
            try:
                node = sdt.tree[node_ref]
            except KeyError:
                node = None
        else:
            matches = sdt.tree.lnodes(node_ref, exact=True)
            node = matches[0] if matches else None
        if node is None:
            _warning(f"Node '{node_ref}' not found in device tree, skipping")
            continue

        if properties is None:
            # Whole node extraction - clone the node for overlay
            if not node.label:
                _warning(f"Node '{node_ref}' has no label, cannot create overlay fragment")
                continue

            # Create overlay fragment reference
            fragment = LopperNode(name="&" + node.label)
            fragment.label = ""

            # Copy all properties (except structural ones)
            skip_props = {'phandle', 'linux,phandle', '#address-cells', '#size-cells',
                          'ranges', 'compatible'}
            for prop in node:
                if prop.name not in skip_props and not prop.name.startswith('lopper-'):
                    new_prop = copy.deepcopy(prop)
                    new_prop.node = fragment
                    fragment.__props__[prop.name] = new_prop

            overlay_tree.add(fragment)
            _info(f"Added overlay fragment for entire node '{node.label}'")

        else:
            # Property-specific extraction
            if not node.label:
                _warning(f"Node '{node_ref}' has no label, cannot create overlay fragment")
                continue

            # Use fragment_create from tree.py which handles companions
            fragment = base_tree.fragment_create(node, properties, include_companions=True)
            if fragment:
                overlay_tree.add(fragment)
                _info(f"Added overlay fragment for '{node.label}' with properties: {properties}")
            else:
                _warning(f"No matching properties found for '{node_ref}#{','.join(properties)}'")

    # Establish overlay relationship for phandle resolution
    overlay_tree.overlay_of(base_tree, exclude_props=exclude_props, exclude_nodes=exclude_nodes)
    overlay_tree.resolve()

    return overlay_tree


def write_output_files(overlay_tree, sdt, pl_file, dtso_file):
    """
    Write output files and perform post-processing.

    Writes overlay tree to pl.dtsi, performs post-processing
    (interrupt-parent replacement, status disabled for video IPs),
    and copies pl.dtsi to pl.dtso.

    Args:
        overlay_tree: The overlay tree to write
        sdt: The system device tree object
        pl_file: Path to output pl.dtsi file
        dtso_file: Path to output pl.dtso file
    """
    # Write overlay tree
    LopperSDT(None).write(overlay_tree, pl_file, True, True)

    # Post-process the generated pl.dtsi file to replace interrupt-parent references
    with open(pl_file, "r") as f:
        content = f.read()

    # Replace interrupt-parent references from imux to gic
    content = content.replace('interrupt-parent = <&imux>;', 'interrupt-parent = <&gic>;')

    # Remove empty overlay fragments left after property exclusion
    content = re.sub(r'\n&\w+ \{\n\};\n', '', content)

    with open(pl_file, "w") as f:
        f.write(content)

    # Add status = "disabled" for specific video IPs
    add_status_disabled_after_ipname(pl_file, match_list)

    # Copy contents from pl.dtsi to pl.dtso in the same directory
    with open(pl_file, "r") as src, open(dtso_file, "w") as dst:
        dst.writelines(src.readlines())

"""
This assist generates an overlay dts file (pl.dtsi) from the /amba_pl node
in the system device tree. Uses LopperTree operations to extract and transform
the PL (Programmable Logic) nodes into a proper device tree overlay format.

Args:
    tgt_node: is the baremetal config top level domain node number
    sdt:      is the system device-tree
    options:  There are two valid options
              Machine name: cortexa9-zynq | cortexa53-zynqmp | cortexa72-versal | cortexa78-versalnet
              (or short forms: cortexa9 | cortexa53 | cortexa72 | cortexa78)
              Configuration: full | segmented | dfx | external-fpga-config (default: full)

Output:
    - pl.dtsi: Device tree overlay file with &amba and &fpga nodes
    - pl.dtso: Copy of pl.dtsi with .dtso extension
    Note: Modified system device tree (with /amba_pl node removed) is written by lopper itself
"""
def xlnx_generate_overlay_dt(tgt_node, sdt, options):
    _level(utils.log_setup(options), __name__)
    # Parse and validate options
    platform, config, zynq_platforms, versal_platforms, firmware_override, node_specs, exclude_props, exclude_nodes = validate_and_parse_options(options, sdt)

    # Remove SDT-specific properties from overlay automatically
    if exclude_props is None:
        exclude_props = ['address-map']
    elif 'address-map' not in exclude_props:
        exclude_props.append('address-map')

    overlay_tree = LopperTree()

    print("Starting overlay generation...")

    # Check if explicit node selection was provided
    if node_specs:
        # Use explicit node specifications - completely overrides default behavior
        _info(f"Using explicit node selection: {node_specs}")
        overlay_tree = build_overlay_from_node_specs(sdt, node_specs, sdt.tree,
                                                     exclude_props=exclude_props, exclude_nodes=exclude_nodes)

        if len(overlay_tree.__nodes__) == 0:
            _error("No valid overlay nodes created from --nodes specification")
            return True

    else:
        # Default behavior: extract /amba_pl node
        # Extract the /amba_pl node from sdt.tree
        amba_node = sdt.tree["/amba_pl"]

        # Validate that amba_pl has valid PL nodes with register properties
        if not validate_amba_pl_node(amba_node):
            return True

        # Prepare amba node for overlay
        new_amba_node = prepare_amba_node(amba_node, sdt, tgt_node)

        # Extract firmware-name property from amba node and remove it from amba node
        firmware_name = new_amba_node["firmware-name"]
        new_amba_node = new_amba_node - firmware_name

        # Override firmware name if provided via command line
        if firmware_override:
            firmware_name.value = [firmware_override]

        # Create and configure FPGA node
        fpga_node, fpga_node_name = create_fpga_node(
            platform, config, firmware_name, zynq_platforms, versal_platforms
        )

        # Collect special nodes from amba node
        node_collections = collect_special_nodes(new_amba_node)

        # Move special nodes to fpga node based on platform and configuration
        new_amba_node, fpga_node = move_nodes_to_fpga(
            new_amba_node, fpga_node, node_collections,
            platform, config, zynq_platforms
        )

        overlay_tree = build_overlay_tree(new_amba_node, fpga_node, fpga_node_name, sdt.tree,
                                          exclude_props=exclude_props, exclude_nodes=exclude_nodes)


        amba_pl_ref = overlay_tree["/&amba_pl"]
        amba_pl_ref.name = "&amba"
        amba_pl_ref.label = ""


    # Clean overlay properties
    clean_overlay_properties(overlay_tree)

    pl_file = f"{sdt.outdir}/pl.dtsi"
    dtso_file = f"{sdt.outdir}/pl.dtso"

    print( f"pl: {pl_file} dtso: {dtso_file}" )

    # Write output files and perform post-processing
    write_output_files(overlay_tree, sdt, pl_file, dtso_file)

    # Write the main SDT output (base tree, no overlay values) and clear
    # sdt.output_file so the main lopper loop does not write a second copy.
    if getattr(sdt, '_overlay_targets', None) and sdt.output_file:
        sdt.write()
        sdt.output_file = ""

    print("Overlay generation completed successfully!")
    return True
