#Python script to update elements in the HydroPlusConfig.xml file to the latest revision
#Created winter 2023 by T. Endreny, SUNY ESF, te@esf.edu 

# Required Inputs:
#   root_path            - Folder to search for XML files recursively
#   target_filename      - Name of the config XML file (e.g., HydroPlusConfig.xml)
#   target_element       - XML element string to search for (e.g., "<Albedo_Tree_frac>")
#   operation            - "append", "replace", or "remove"
#   new_lines            - List of lines to insert if operation is append or replace
#   process_all_matches  - True = update all occurrences (BulkArea + GI), False = only update the first

import os

# --- Settings ---
root_path = r"C:\iTree\HydroPlus\TestingFilesAndScript\TestCases"
#root_path = r"C:\iTree\a_prep_TIGER_DEM_NLCD\a_scripts"
#root_path = r"C:\iTree\CoolAir_NationalRuns"
#root_path = r"C:\iTree\projects\a_TestCase\LCScale_12x12_scenario_5\input"

#flag_FindFiles_By_Prefix_and_Extension when 1 uses target_prefix and target_extension, when 0 uses target_filename
flag_FindFiles_By_Prefix_and_Extension = 1
target_filename = "HydroPlusConfig.xml"
target_prefix = "HydroPlusConfig"
target_extension = (".xml", ".tpl")

# --- Operation Setup ---
operation = "remove"  # Options: "append", "remove", "replace"
process_all_matches = True  # True = update all occurrences (BulkArea + GI), False = only update the first

#Note: For append, below this element the append will occur
#Note: For remove, this element will be removed
target_element = "<NODATA>"

#Note: If string is in double quotes and contains doubled quoted word, such as "Map", then use escape characters "<!--Options: \"Map\" ...-->"
new_lines = [
    '<Scenario_CoolAir_Irrigate_Day_or_Night>0</Scenario_CoolAir_Irrigate_Day_or_Night><!--Options: 0-2; 0=day and night; 1=daylight only; 2=nighttime only-->',
    '<Scenario_CoolAir_Irrigate_MaxPerDay>0</Scenario_CoolAir_Irrigate_MaxPerDay><!--Options: >=0; 0=unlimited; 1 or greater=maximum times per 24 hr day-->'
]

# --- Functions ---
#Note: send a single filename to this function
def find_config_file_by_name(root, name):
    for dirpath, _, filenames in os.walk(root):
        for file in filenames:
            if file == name:
                yield os.path.join(dirpath, file)

#Note: send a filename criteria of prefix and extensions to this function
def find_config_files_by_pre_ext(root, prefix, extensions):
    for dirpath, _, filenames in os.walk(root):
        for file in filenames:
            if file.startswith(prefix) and file.endswith(extensions):
                yield os.path.join(dirpath, file)
                
#Note: find element names in HydroPlusConfig.xml to ensure we don't write a duplicate
def get_element_names(lines):
    element_names = []
    for line in lines:
        start = line.find("<") + 1
        end = line.find(">", start)
        if start > 0 and end > start:
            name = line[start:end].split()[0].split(">")[0].strip("/")  # handles self-closing or commented tags
            element_names.append(name)
    return element_names
    
def already_updated(lines, target, new_first_line):
    for i, line in enumerate(lines):
        if target in line:
            if i + 1 < len(lines) and new_first_line in lines[i + 1]:
                return True
    return False

def update_file(filepath):
    #with open(filepath, 'r') as f:; opens f for readlines
    with open(filepath, 'r') as f:
        lines = f.readlines()
        
    #element_names = get_element_names(new_lines); used to find element names and prevent writing duplicate
    element_names = get_element_names(new_lines)
    #already_exists = any( ...; becomes true for any instance of element
    already_exists = any(f"<{name}>" in line or f"<{name} " in line  # catches tags with attributes too
        for name in element_names
        for line in lines
    )
    
    #if operation == "append" and already_exists:; then both are true, it will prohibit new append
    if operation == "append" and already_exists:
        print(f"Already updated: {filepath}")
        return

    updated_lines = []
    modified = False
    match_count = 0

    #for i, line in enumerate(lines):; loops through all lines in XML
    i = 0
    while i < len(lines):
        line = lines[i]
        stripped = line.strip()
        is_target = target_element in stripped

        if is_target:
            match_count += 1
            if not process_all_matches and match_count > 1:
                updated_lines.append(line)
                i += 1
                continue

            if operation == "append":
                indent = line[:len(line) - len(line.lstrip())]
                updated_lines.append(line)
                for new_line in new_lines:
                    updated_lines.append(indent + new_line + '\n')
                #i incremented to avoid infinite loop within while
                i += 1
            elif operation == "replace":
                indent = line[:len(line) - len(line.lstrip())]
                updated_lines.append(indent + new_lines[0] + '\n')
                for new_line in new_lines[1:]:
                    updated_lines.append(indent + new_line + '\n')

            elif operation == "remove":
                # Check if the element is a single-line element
                if f"</{target_element.strip('<>')}" in stripped:
                    # Skip just this line
                    i += 1
                    modified = True
                    continue
                else:
                    # Remove all lines until we reach the closing tag
                    end_tag = f"</{target_element.strip('<>')}>"
                    i += 1
                    while i < len(lines) and end_tag not in lines[i]:
                        i += 1
                    if i < len(lines):
                        i += 1  # skip the closing tag line
                    modified = True
                    continue
            modified = True
        else:
            updated_lines.append(line)
            i += 1

    #if modified:; checks for what was changed given is_target is true
    if modified:
        #with open(filepath, 'w') as f:; opened filepath for write as f
        with open(filepath, 'w') as f:
            f.writelines(updated_lines)
        print(f"{operation.capitalize()}: {filepath}")
    else:
        print(f"No changes made: {filepath}")

# --- Run ---
if __name__ == "__main__":
    #if flag_FindFiles_By_Prefix_and_Extension == 0:; will only find exact file names to modify
    if flag_FindFiles_By_Prefix_and_Extension == 0:
        print("🔍 Searching by full filename...")
        #config_files = list(find_config_file_by_name(root_path, target_filename)) is implementing list of files to modify 
        config_files = list(find_config_file_by_name(root_path, target_filename))
    #else flag_FindFiles_By_Prefix_and_Extension == 1:; will find all prefix and extension based file names to modify
    else:
        print("🔍 Searching by prefix and extension...")
        #config_files = list(find_config_files_by_pre_ext(root_path, target_prefix, target_extension)) is implementing list of files to modify 
        config_files = list(find_config_files_by_pre_ext(root_path, target_prefix, target_extension))
        
    #if not config_files:; reports no files found
    if not config_files:
        print("⚠️ No matching config files found. Check your search criteria or directory path.")
    #else, program does its work to modify files
    else:
        #for config_file in config_files:; loop through files to modify
        for config_file in config_files:
            #update_file(config_file); modify the files
            update_file(config_file)