import sys
import os
import glob
import os.path
import shutil
import subprocess
import xml.etree.ElementTree


def cmakeExt():
    if sys.platform == 'win32':
        return '.exe'
    return ''


def filterPreset(presetPath):
    # If this is a file path, extract the actual preset name from XML or filename
    if os.path.isfile(presetPath):
        try:
            presetXml = xml.etree.ElementTree.parse(presetPath).getroot()
            presetName = presetXml.get('name')
        except:
            # Fall back to just using the basename without extension if XML parsing fails
            basename = os.path.basename(presetPath)
            presetName = os.path.splitext(basename)[0]
    else:
        # If not a file path, assume it's already a preset name
        presetName = presetPath
    
    # Platform-specific filtering
    winPresetFilter = ['win','switch','crosscompile']
    if sys.platform == 'win32':
        # On Windows, include presets that contain win, switch, or crosscompile 
        # (but not windows-crosscompile)
        if any((presetName.find(elem) != -1 and 'windows-crosscompile' not in presetName) for elem in winPresetFilter):
            return True
    else:
        # On non-Windows, include Linux presets and windows-crosscompile
        # Check for Linux or other Unix/macOS presets (those not containing Windows-specific terms)
        # Special case: include windows-crosscompile, which is for cross-compiling Windows targets
        if 'linux' in presetName.lower() or 'mac' in presetName.lower() or 'windows-crosscompile' in presetName:
            return True
        if all(presetName.find(elem) == -1 for elem in ['win', 'switch']):
            return True
    return False

def noPresetProvided(physx_root_dir):
    global input
    print('Preset parameter required, available presets:')
    presets_dir = os.path.join(physx_root_dir, "buildtools", "presets")
    internal_presets = os.path.join(presets_dir, "*.xml")
    public_presets = os.path.join(presets_dir, "public", "*.xml")
    
    # Get all XML files in the presets directory
    internal_preset_files = glob.glob(internal_presets)
    
    # Check if we have any non-directory XML files directly in presets folder
    presetfiles = []
    for file in internal_preset_files:
        if not os.path.isdir(file):  # Make sure it's a file, not a directory
            basename = os.path.basename(file)
            dirname = os.path.dirname(file)
            if os.path.basename(dirname) != "public":  # Skip files in public subdirectory
                presetfiles.append(file)
    
    # If no XML files in main presets directory, we're in public distribution
    # So use the files from public directory
    if len(presetfiles) == 0:
        print("No presets in main folder, using public presets")
        presetfiles = glob.glob(public_presets)

    if len(presetfiles) == 0:
        print("Error: No preset files found. Make sure the directory structure is correct.")
        exit(1)

    counter = 0
    presetList = []
    for preset in presetfiles:
        if filterPreset(preset):
            try:
                presetXml = xml.etree.ElementTree.parse(preset).getroot()
                if preset.find('user') == -1:
                    print('(' + str(counter) + ') ' + presetXml.get('name') +
                        ' <--- ' + presetXml.get('comment'))
                    presetList.append(presetXml.get('name'))
                else:
                    print('(' + str(counter) + ') ' + presetXml.get('name') +
                        '.user <--- ' + presetXml.get('comment'))
                    presetList.append(presetXml.get('name') + '.user')
                counter = counter + 1
            except Exception as e:
                print(f"Warning: Could not parse preset file {preset}: {e}")
                continue
    
    if counter == 0:
        print("Error: No valid presets found for this platform.")
        exit(1)
        
    # Fix Python 2.x.
    try:
        input = raw_input
    except NameError:
        pass
    mode = int(eval(input('Enter preset number: ')))
    return presetList[mode]

class CMakePreset:
    presetName = ''
    targetPlatform = ''
    compiler = ''
    generator = ''
    cmakeSwitches = []
    cmakeParams = []

    def __init__(self, presetName, physx_root_dir):
        xmlPath = os.path.join(physx_root_dir, "buildtools", "presets", f"{presetName}.xml")
        if os.path.isfile(xmlPath):
            print('Using preset xml: '+xmlPath)
        else:
            xmlPath = os.path.join(physx_root_dir, "buildtools", "presets", "public", f"{presetName}.xml")
            if os.path.isfile(xmlPath):
                print('Using preset xml: '+xmlPath)
            else:
                print('Preset xml file: '+xmlPath+' not found')
                exit()

        # get the xml
        presetNode = xml.etree.ElementTree.parse(xmlPath).getroot()
        self.presetName = presetNode.attrib['name']
        for platform in presetNode.findall('platform'):
            self.targetPlatform = platform.attrib['targetPlatform']
            self.compiler = platform.attrib['compiler']
            self.generator = platform.get('generator')
            print('Target platform: ' + self.targetPlatform +
                  ' using compiler: ' + self.compiler)
            if self.generator is not None:
                print(' using generator: ' + self.generator)

        for cmakeSwitch in presetNode.find('CMakeSwitches'):
            cmSwitch = '-D' + \
                cmakeSwitch.attrib['name'] + '=' + \
                cmakeSwitch.attrib['value'].upper()
            self.cmakeSwitches.append(cmSwitch)

        for cmakeParam in presetNode.find('CMakeParams'):
            if cmakeParam.attrib['name'] == 'CMAKE_INSTALL_PREFIX' or cmakeParam.attrib['name'] == 'PX_OUTPUT_LIB_DIR' or cmakeParam.attrib['name'] == 'PX_OUTPUT_EXE_DIR' or cmakeParam.attrib['name'] == 'PX_OUTPUT_DLL_DIR':
                cmParam = '-D' + cmakeParam.attrib['name'] + '=\"' + \
                    os.environ['PHYSX_ROOT_DIR'] + '/' + \
                    cmakeParam.attrib['value'] + '\"'
            else:
                cmParam = '-D' + \
                    cmakeParam.attrib['name'] + '=' + \
                    cmakeParam.attrib['value']
            self.cmakeParams.append(cmParam)
    pass

    def isMultiConfigPlatform(self):
        if self.targetPlatform == 'linux':
            return False
        elif self.targetPlatform == 'linuxAarch64':
            return False
        elif self.compiler == 'x86_64-w64-mingw32-g++':
            return False
        return True


    def getCMakeSwitches(self):
        outString = ''
        # We need to check both GPU-related switches
        gpuProjectsEnabled = False
        gpuProjectsOnlyEnabled = False
        
        # Define the switch names for clarity and consistency
        GPU_PROJECTS_SWITCH = 'PX_GENERATE_GPU_PROJECTS'
        GPU_PROJECTS_ONLY_SWITCH = 'PX_GENERATE_GPU_PROJECTS_ONLY'
        
        # First pass: Check the state of GPU-related switches
        gpu_projects_found = False
        gpu_projects_only_found = False
        
        for cmakeSwitch in self.cmakeSwitches:
            # Format of cmakeSwitch is "-DSWITCH_NAME=VALUE"
            # Use a more flexible approach to match switches
            if f'-D{GPU_PROJECTS_SWITCH}=' in cmakeSwitch:
                gpu_projects_found = True
                gpuProjectsEnabled = cmakeSwitch.endswith('=TRUE')
            elif f'-D{GPU_PROJECTS_ONLY_SWITCH}=' in cmakeSwitch:
                gpu_projects_only_found = True
                gpuProjectsOnlyEnabled = cmakeSwitch.endswith('=TRUE')
        
        # Log the state of GPU switches for debugging
        if not gpu_projects_found:
            print(f"Warning: {GPU_PROJECTS_SWITCH} switch not found in preset. Defaulting to disabled.")
        if not gpu_projects_only_found:
            print(f"Warning: {GPU_PROJECTS_ONLY_SWITCH} switch not found in preset. Defaulting to disabled.")
            
        # Determine if we need to add CUDA paths
        gpuEnabled = gpuProjectsEnabled or gpuProjectsOnlyEnabled
        
        # Log GPU status
        print(f"GPU projects enabled: {gpuEnabled} ({GPU_PROJECTS_SWITCH}={gpuProjectsEnabled}, {GPU_PROJECTS_ONLY_SWITCH}={gpuProjectsOnlyEnabled})")
                
        # Second pass: Add all switches to output
        for cmakeSwitch in self.cmakeSwitches:
            outString = outString + ' ' + cmakeSwitch
            
        # Only add CUDA paths if GPU is enabled
        if gpuEnabled:
            if os.environ.get('PM_CUDA_PATH') is not None:
                if os.environ.get('PM_CUDA_PATH') is not None:
                    outString = outString + ' -DCUDAToolkit_ROOT_DIR=' + \
                            os.environ['PM_CUDA_PATH']
                    if self.compiler in ['vc15', 'vc16', 'vc17'] and self.generator != 'ninja':
                        outString = outString + ' -T cuda=' + os.environ['PM_CUDA_PATH']
                    # TODO: Need to do the same for gcc (aarch64) when we package it with Packman
                    elif self.compiler == 'clang':
                        if os.environ.get('PM_clang_PATH') is not None:
                            outString = outString + ' -DCMAKE_CUDA_HOST_COMPILER=' + \
                                os.environ['PM_clang_PATH'] + '/bin/clang++'
                        
        return outString

    def getCMakeParams(self):
        outString = ''
        for cmakeParam in self.cmakeParams:
            outString = outString + ' ' + cmakeParam # + ' --trace'
        return outString

    def getPlatformCMakeParams(self):
        cmake_modules_root = os.environ['PHYSX_ROOT_DIR'] + '/source/compiler/cmake/modules'
        outString = ' '

        vs_versions = {
            'vc15': '\"Visual Studio 15 2017\"',
            'vc16': '\"Visual Studio 16 2019\"',
            'vc17': '\"Visual Studio 17 2022\"'
        }

        # Visual studio
        if self.compiler in vs_versions:
            generator = '-G \"Ninja Multi-Config\"' if self.generator == 'ninja' else '-G ' + vs_versions[self.compiler]
            outString += generator
        # Windows crosscompile
        elif self.compiler == 'x86_64-w64-mingw32-g++':
            outString = outString + '-G \"Ninja\"'
        # mac
        elif self.compiler == 'xcode':
            outString = outString + '-G Xcode'
        # Linux
        elif self.targetPlatform in ['linux', 'linuxAarch64']:
            if self.generator is not None and self.generator == 'ninja':
                outString = outString + '-G \"Ninja\"'
                outString = outString + ' -DCMAKE_MAKE_PROGRAM=' + os.environ['PM_ninja_PATH'] + '/ninja'
            else:
                outString = outString + '-G \"Unix Makefiles\"'

        if self.targetPlatform == 'win64':
            if self.generator != 'ninja':
                outString = outString + ' -Ax64'
            outString = outString + ' -DTARGET_BUILD_PLATFORM=windows'
            outString = outString + ' -DPX_OUTPUT_ARCH=x86'
            if self.compiler == 'x86_64-w64-mingw32-g++':
                outString = outString + ' -DCMAKE_TOOLCHAIN_FILE=' + \
                    cmake_modules_root + '/linux/WindowsCrossToolchain.linux-unknown-x86_64.cmake'
            return outString
        elif self.targetPlatform == 'switch64':
            outString = outString + ' -DTARGET_BUILD_PLATFORM=switch'
            outString = outString + ' -DCMAKE_TOOLCHAIN_FILE=' + \
                cmake_modules_root + '/switch/NX64Toolchain.txt'
            outString = outString + ' -DCMAKE_GENERATOR_PLATFORM=NX64'
            return outString
        elif self.targetPlatform == 'linux':
            outString = outString + ' -DTARGET_BUILD_PLATFORM=linux'
            outString = outString + ' -DPX_OUTPUT_ARCH=x86'
            if self.compiler == 'clang-crosscompile':
                outString = outString + ' -DCMAKE_TOOLCHAIN_FILE=' + \
                    cmake_modules_root + '/linux/LinuxCrossToolchain.x86_64-unknown-linux-gnu.cmake'
                outString = outString + ' -DCMAKE_MAKE_PROGRAM=' + os.environ.get('PM_MinGW_PATH') + '/bin/mingw32-make.exe'
            elif self.compiler == 'clang':
                if os.environ.get('PM_clang_PATH') is not None:
                    outString = outString + ' -DCMAKE_C_COMPILER=' + \
                        os.environ['PM_clang_PATH'] + '/bin/clang'
                    outString = outString + ' -DCMAKE_CXX_COMPILER=' + \
                        os.environ['PM_clang_PATH'] + '/bin/clang++'
                else:
                    outString = outString + ' -DCMAKE_C_COMPILER=clang'
                    outString = outString + ' -DCMAKE_CXX_COMPILER=clang++'
            return outString
        elif self.targetPlatform == 'linuxAarch64':
            outString = outString + ' -DTARGET_BUILD_PLATFORM=linux'
            outString = outString + ' -DPX_OUTPUT_ARCH=arm'
            if self.compiler == 'clang-crosscompile':
                outString = outString + ' -DCMAKE_TOOLCHAIN_FILE=' + \
                    cmake_modules_root + '/linux/LinuxCrossToolchain.aarch64-unknown-linux-gnueabihf.cmake'
                outString = outString + ' -DCMAKE_MAKE_PROGRAM=' + os.environ.get('PM_MinGW_PATH') + '/bin/mingw32-make.exe'
            elif self.compiler == 'gcc':
                # TODO: To change so it uses Packman's compiler. Then add it as
                # host compiler for CUDA above.
                outString = outString + ' -DCMAKE_TOOLCHAIN_FILE=\"' + \
                    cmake_modules_root + '/linux/LinuxAarch64.cmake\"'
            elif self.compiler == 'clang':
                if os.environ.get('PM_clang_PATH') is not None:
                    outString = outString + ' -DCMAKE_C_COMPILER=' + \
                        os.environ['PM_clang_PATH'] + '/bin/clang'
                    outString = outString + ' -DCMAKE_CXX_COMPILER=' + \
                        os.environ['PM_clang_PATH'] + '/bin/clang++'
                else:
                    outString = outString + ' -DCMAKE_C_COMPILER=clang'
                    outString = outString + ' -DCMAKE_CXX_COMPILER=clang++'
            
            return outString
        elif self.targetPlatform == 'mac64':
            outString = outString + ' -DTARGET_BUILD_PLATFORM=mac'
            outString = outString + ' -DPX_OUTPUT_ARCH=x86'
            return outString
        return ''


def getCommonParams():
    outString = '--no-warn-unused-cli'
    outString = outString + ' -DCMAKE_PREFIX_PATH=\"' + os.environ['PM_PATHS'] + '\"'
    outString = outString + ' -DPHYSX_ROOT_DIR=\"' + \
        os.environ['PHYSX_ROOT_DIR'] + '\"'
    outString = outString + ' -DPX_OUTPUT_LIB_DIR=\"' + \
        os.environ['PHYSX_ROOT_DIR'] + '\"'
    outString = outString + ' -DPX_OUTPUT_BIN_DIR=\"' + \
        os.environ['PHYSX_ROOT_DIR'] + '\"'
    if os.environ.get('GENERATE_SOURCE_DISTRO') == '1':
        outString = outString + ' -DPX_GENERATE_SOURCE_DISTRO=1'
    return outString

def cleanupCompilerDir(compilerDirName):
    if os.path.exists(compilerDirName):
        if sys.platform == 'win32':
            os.system('rmdir /S /Q ' + compilerDirName)
        else:
            shutil.rmtree(compilerDirName, True)
    if os.path.exists(compilerDirName) == False:
        os.makedirs(compilerDirName)

def presetProvided(pName, physx_root_dir):
    parsedPreset = CMakePreset(pName, physx_root_dir)

    print('PM_PATHS: ' + os.environ['PM_PATHS'])

    if os.environ.get('PM_cmake_PATH') is not None:
        cmakeExec = os.environ['PM_cmake_PATH'] + '/bin/cmake' + cmakeExt()
    else:
        cmakeExec = 'cmake' + cmakeExt()
    print('Cmake: ' + cmakeExec)

    # gather cmake parameters
    cmakeParams = parsedPreset.getPlatformCMakeParams()
    cmakeParams = cmakeParams + ' ' + getCommonParams()
    cmakeParams = cmakeParams + ' ' + parsedPreset.getCMakeSwitches()
    cmakeParams = cmakeParams + ' ' + parsedPreset.getCMakeParams()
    # print(cmakeParams)

    if os.path.isfile(physx_root_dir + '/compiler/internal/CMakeLists.txt'):
        cmakeMasterDir = 'internal'
    else:
        cmakeMasterDir = 'public'
    if parsedPreset.isMultiConfigPlatform():
        # cleanup and create output directory
        outputDir = os.path.join(physx_root_dir, 'compiler', parsedPreset.presetName)
        cleanupCompilerDir(outputDir)

        # run the cmake script
        #print('Cmake params:' + cmakeParams)
        os.chdir(outputDir)
        os.system(cmakeExec + ' \"' +
                  physx_root_dir + '/compiler/' + cmakeMasterDir + '\"' + cmakeParams)
        os.chdir(physx_root_dir)
    else:
        configs = ['debug', 'checked', 'profile', 'release']
        for config in configs:
            # cleanup and create output directory
            outputDir = os.path.join(physx_root_dir, 'compiler', parsedPreset.presetName + '-' + config)
            cleanupCompilerDir(outputDir)

            # run the cmake script
            #print('Cmake params:' + cmakeParams)
            os.chdir(outputDir)
            # print(cmakeExec + ' \"' + physx_root_dir + '/compiler/' + cmakeMasterDir + '\"' + cmakeParams + ' -DCMAKE_BUILD_TYPE=' + config)
            os.system(cmakeExec + ' \"' + physx_root_dir + '/compiler/' +
                      cmakeMasterDir + '\"' + cmakeParams + ' -DCMAKE_BUILD_TYPE=' + config)
            os.chdir(physx_root_dir)
    pass


def main():
    if (sys.version_info[0] < 3) or (sys.version_info[0] == 3 and sys.version_info[1] < 5):
        print("You are using Python {}. You must use Python 3.5 and up. Please read README.md for requirements.").format(sys.version)
        exit()

    physx_root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
    os.environ['PHYSX_ROOT_DIR'] = physx_root_dir.replace("\\", "/")

    if len(sys.argv) != 2:
        presetName = noPresetProvided(physx_root_dir)  # Ensure this function returns the preset name
        if sys.platform == 'win32':
            print('Running generate_projects.bat ' + presetName)
            cmd_path = os.path.join(physx_root_dir, 'generate_projects.bat')
            cmd = f'"{cmd_path}" {presetName}'
            result = subprocess.run(cmd, cwd=physx_root_dir, check=True, shell=True, universal_newlines=True)
            # TODO: catch exception and add capture errors
        else:
            print('Running generate_projects.sh ' + presetName)
            cmd_path = os.path.join(physx_root_dir, 'generate_projects.sh')
            cmd = [cmd_path, presetName]
            result = subprocess.run(cmd, cwd=physx_root_dir, check=True, universal_newlines=True)
            # TODO: catch exception and add capture errors
    else:
        presetName = sys.argv[1]
        if filterPreset(presetName):
            presetProvided(presetName, physx_root_dir)
        else:
            print('Preset not supported on this build platform.')
main()
