"""
Configuration parser module for ESPal

This module provides functionality to:
- Parse build system configuration files (PlatformIO, Arduino, ESP-IDF)
- Extract relevant settings (baudrate, ports, environments)
- Provide a unified interface for different build systems
"""

import os
import re
import configparser
import json
import yaml
import glob


class BuildSystemConfig:
    """Base class for build system configuration parsers"""
    
    def __init__(self, project_dir=None):
        """
        Initialize the configuration parser

        Args:
            project_dir: Project directory path, defaults to current directory
        """
        self.project_dir = project_dir or os.getcwd()
        self.config_file = None
        self.environments = []
        self.default_environment = None
        self.baudrates = {}
        self.upload_ports = {}
        self.monitor_ports = {}
        self.vid_pids = {}  # Store VID:PID for each environment
    
    def find_config(self):
        """
        Find configuration file in project directory
        
        Returns:
            str: Path to config file if found, None otherwise
        """
        raise NotImplementedError("Subclasses must implement find_config()")
    
    def parse_config(self):
        """
        Parse the configuration file
        
        Returns:
            bool: True if successful, False otherwise
        """
        raise NotImplementedError("Subclasses must implement parse_config()")
    
    def get_environment_list(self):
        """
        Get list of available environments/targets/boards
        
        Returns:
            list: List of environment names
        """
        return self.environments
    
    def get_default_environment(self):
        """
        Get default environment/target/board
        
        Returns:
            str: Default environment name
        """
        if self.default_environment:
            return self.default_environment
        elif self.environments:
            return self.environments[0]
        else:
            return None
    
    def get_baudrate(self, environment=None):
        """
        Get baudrate for environment
        
        Args:
            environment: Environment name, uses default if None
            
        Returns:
            int: Baudrate value
        """
        env = environment or self.get_default_environment()
        return self.baudrates.get(env, 115200)  # Default to 115200 if not specified
    
    def get_upload_port(self, environment=None):
        """
        Get upload port for environment
        
        Args:
            environment: Environment name, uses default if None
            
        Returns:
            str: Upload port
        """
        env = environment or self.get_default_environment()
        return self.upload_ports.get(env)
    
    def get_monitor_port(self, environment=None):
        """
        Get monitor port for environment

        Args:
            environment: Environment name, uses default if None

        Returns:
            str: Monitor port
        """
        env = environment or self.get_default_environment()
        # Use upload port as fallback if monitor port not specified
        return self.monitor_ports.get(env, self.upload_ports.get(env))

    def get_vid_pid(self, environment=None):
        """
        Get USB VID:PID for environment

        Args:
            environment: Environment name, uses default if None

        Returns:
            tuple: (vid, pid) as strings, or (None, None) if not specified
        """
        env = environment or self.get_default_environment()
        vid_pid = self.vid_pids.get(env)
        if vid_pid:
            # vid_pid should be a tuple (vid, pid)
            return vid_pid
        return (None, None)


class PlatformIOConfig(BuildSystemConfig):
    """PlatformIO configuration parser"""
    
    def find_config(self):
        """Find platformio.ini file"""
        config_path = os.path.join(self.project_dir, "platformio.ini")
        if os.path.isfile(config_path):
            self.config_file = config_path
            return config_path
        return None
    
    def parse_config(self):
        """Parse platformio.ini"""
        if not self.config_file:
            if not self.find_config():
                return False
        
        config = configparser.ConfigParser()
        config.read(self.config_file)
        
        # Extract environments
        for section in config.sections():
            if section.startswith("env:"):
                env_name = section[4:]  # Remove "env:" prefix
                self.environments.append(env_name)

                # Extract baudrate
                if "monitor_speed" in config[section]:
                    self.baudrates[env_name] = int(config[section]["monitor_speed"])

                # Extract upload port
                if "upload_port" in config[section]:
                    self.upload_ports[env_name] = config[section]["upload_port"]

                # Extract monitor port
                if "monitor_port" in config[section]:
                    self.monitor_ports[env_name] = config[section]["monitor_port"]

                # Extract VID/PID
                vid = None
                pid = None

                # Try combined format first: upload_vidpid = 10c4:ea60
                if "upload_vidpid" in config[section]:
                    vidpid_str = config[section]["upload_vidpid"].strip()
                    if ":" in vidpid_str:
                        parts = vidpid_str.split(":")
                        if len(parts) == 2:
                            vid = parts[0].strip().lower()
                            pid = parts[1].strip().lower()

                # Try separate format: upload_vid and upload_pid
                if not vid and "upload_vid" in config[section]:
                    vid = config[section]["upload_vid"].strip().lower()
                if not pid and "upload_pid" in config[section]:
                    pid = config[section]["upload_pid"].strip().lower()

                # Store if we have both
                if vid and pid:
                    self.vid_pids[env_name] = (vid, pid)
        
        # Get default environment
        if "platformio" in config and "default_envs" in config["platformio"]:
            default_envs = config["platformio"]["default_envs"].split(",")
            if default_envs:
                self.default_environment = default_envs[0].strip()
        
        return len(self.environments) > 0


class ArduinoConfig(BuildSystemConfig):
    """Arduino CLI configuration parser"""
    
    def find_config(self):
        """Find Arduino project config (sketch.json or arduino-cli.yaml)"""
        # Look for sketch.json in the project directory
        sketch_json = os.path.join(self.project_dir, "sketch.json")
        if os.path.isfile(sketch_json):
            self.config_file = sketch_json
            return sketch_json
        
        # If not found, check for .arduino/arduino-cli.yaml
        home = os.path.expanduser("~")
        cli_yaml = os.path.join(home, ".arduino", "arduino-cli.yaml")
        if os.path.isfile(cli_yaml):
            self.config_file = cli_yaml
            return cli_yaml
        
        # Not found, look for sketch files
        sketch_files = glob.glob(os.path.join(self.project_dir, "*.ino"))
        if sketch_files:
            # Use the first .ino file as "config"
            self.config_file = sketch_files[0]
            return sketch_files[0]
        
        return None
    
    def parse_config(self):
        """Parse Arduino config"""
        if not self.config_file:
            if not self.find_config():
                return False
        
        # If it's a sketch.json file
        if self.config_file.endswith(".json"):
            try:
                with open(self.config_file, 'r') as f:
                    data = json.load(f)
                
                # Extract board info
                if "cpu" in data:
                    self.environments.append(data["cpu"].get("fqbn", "unknown"))
                    
                    # Extract port
                    if "port" in data["cpu"]:
                        port = data["cpu"]["port"].get("address")
                        if port:
                            self.upload_ports[self.environments[0]] = port
                            self.monitor_ports[self.environments[0]] = port
                    
                    # Extract baudrate - not typically in sketch.json
                    # Default is set in get_baudrate()
                
                return len(self.environments) > 0
            
            except (json.JSONDecodeError, IOError):
                return False
        
        # If it's an arduino-cli.yaml file
        elif self.config_file.endswith(".yaml"):
            try:
                with open(self.config_file, 'r') as f:
                    data = yaml.safe_load(f)
                
                # Extract board info from global config
                # This doesn't typically have project-specific settings
                # but might have default board/port
                if "board" in data:
                    self.environments.append(data["board"])
                
                if "port" in data:
                    port = data["port"]
                    if self.environments:
                        self.upload_ports[self.environments[0]] = port
                        self.monitor_ports[self.environments[0]] = port
                
                return len(self.environments) > 0
            
            except (yaml.YAMLError, IOError):
                return False
        
        # If it's an .ino file
        elif self.config_file.endswith(".ino"):
            # Try to extract board info from file content
            try:
                with open(self.config_file, 'r') as f:
                    content = f.read()
                
                # Look for board type in comments or defines
                board_match = re.search(r'(?:\/\/|\/\*|\*)\s*Board:\s*([^\n\r\*\/]+)', content)
                if board_match:
                    self.environments.append(board_match.group(1).strip())
                else:
                    # Just use filename as environment
                    board_name = os.path.basename(self.config_file)[:-4]  # Remove .ino
                    self.environments.append(board_name)
                
                # Look for port in comments
                port_match = re.search(r'(?:\/\/|\/\*|\*)\s*Port:\s*([^\n\r\*\/]+)', content)
                if port_match and self.environments:
                    port = port_match.group(1).strip()
                    self.upload_ports[self.environments[0]] = port
                    self.monitor_ports[self.environments[0]] = port
                elif self.environments:
                    # Set empty strings as defaults instead of None
                    self.upload_ports[self.environments[0]] = ""
                    self.monitor_ports[self.environments[0]] = ""
                
                # Look for baudrate in comments or Serial.begin()
                baudrate_match = re.search(r'(?:\/\/|\/\*|\*)\s*Baudrate:\s*(\d+)', content)
                if not baudrate_match:
                    # Try to find in Serial.begin()
                    baudrate_match = re.search(r'Serial\.begin\s*\(\s*(\d+)\s*\)', content)
                
                if baudrate_match and self.environments:
                    baudrate = int(baudrate_match.group(1).strip())
                    self.baudrates[self.environments[0]] = baudrate
                
                return len(self.environments) > 0
            
            except IOError:
                return False
        
        return False


class ESPIDFConfig(BuildSystemConfig):
    """ESP-IDF configuration parser"""
    
    def find_config(self):
        """Find ESP-IDF sdkconfig file or CMakeLists.txt"""
        # First look for sdkconfig
        sdkconfig = os.path.join(self.project_dir, "sdkconfig")
        if os.path.isfile(sdkconfig):
            self.config_file = sdkconfig
            return sdkconfig
        
        # Then look for CMakeLists.txt
        cmake_file = os.path.join(self.project_dir, "CMakeLists.txt")
        if os.path.isfile(cmake_file):
            self.config_file = cmake_file
            return cmake_file
        
        return None
    
    def parse_config(self):
        """Parse ESP-IDF configuration"""
        if not self.config_file:
            if not self.find_config():
                return False
        
        # sdkconfig file has key=value format
        if os.path.basename(self.config_file) == "sdkconfig":
            try:
                with open(self.config_file, 'r') as f:
                    content = f.read()
                
                # Find the project name
                project_match = re.search(r'CONFIG_APP_PROJECT_NAME="([^"]+)"', content)
                if project_match:
                    self.environments.append(project_match.group(1))
                else:
                    # Use directory name as project name
                    self.environments.append(os.path.basename(self.project_dir))
                
                # Extract baudrate
                baudrate_match = re.search(r'CONFIG_ESP_CONSOLE_UART_BAUDRATE=(\d+)', content)
                if baudrate_match and self.environments:
                    baudrate = int(baudrate_match.group(1))
                    self.baudrates[self.environments[0]] = baudrate
                
                # Port is not typically in sdkconfig, so we don't extract it
                
                return len(self.environments) > 0
            
            except IOError:
                return False
        
        # CMakeLists.txt parse (limited)
        elif os.path.basename(self.config_file) == "CMakeLists.txt":
            try:
                with open(self.config_file, 'r') as f:
                    content = f.read()
                
                # Find the project name
                project_match = re.search(r'project\s*\(\s*([^\s\)]+)', content)
                if project_match:
                    self.environments.append(project_match.group(1))
                else:
                    # Use directory name as project name
                    self.environments.append(os.path.basename(self.project_dir))
                
                # Look for port definition in CMakeLists.txt
                port_match = re.search(r'(?:SERIAL_PORT|PORT|ESP_PORT)\s*=?\s*[\'"]?([^\'"\s]+)[\'"]?', content)
                if port_match and self.environments:
                    port = port_match.group(1)
                    self.upload_ports[self.environments[0]] = port
                    self.monitor_ports[self.environments[0]] = port
                
                # Baudrate is not typically in CMakeLists.txt
                
                return len(self.environments) > 0
            
            except IOError:
                return False
        
        return False


def detect_build_system(project_dir=None):
    """
    Detect which build system is used in the project
    
    Args:
        project_dir: Project directory path, defaults to current directory
        
    Returns:
        tuple: (system_name, config_object) where system_name is one of
               "platformio", "arduino", "espidf", or None if not detected
    """
    project_dir = project_dir or os.getcwd()
    
    # Try PlatformIO first
    pio_config = PlatformIOConfig(project_dir)
    if pio_config.find_config() and pio_config.parse_config():
        return "platformio", pio_config
    
    # Try ESP-IDF
    idf_config = ESPIDFConfig(project_dir)
    if idf_config.find_config() and idf_config.parse_config():
        return "espidf", idf_config
    
    # Try Arduino last
    arduino_config = ArduinoConfig(project_dir)
    if arduino_config.find_config() and arduino_config.parse_config():
        return "arduino", arduino_config
    
    return None, None


# Simple test function for module
if __name__ == "__main__":
    import sys
    
    if len(sys.argv) > 1:
        project_dir = sys.argv[1]
    else:
        project_dir = os.getcwd()
    
    print(f"Detecting build system in {project_dir}")
    
    system_name, config = detect_build_system(project_dir)
    
    if not system_name:
        print("No build system configuration detected")
        sys.exit(1)
    
    print(f"Detected build system: {system_name}")
    print(f"Configuration file: {config.config_file}")
    print(f"Environments: {config.get_environment_list()}")
    print(f"Default environment: {config.get_default_environment()}")
    
    default_env = config.get_default_environment()
    if default_env:
        print(f"Baudrate: {config.get_baudrate(default_env)}")
        print(f"Upload port: {config.get_upload_port(default_env)}")
        print(f"Monitor port: {config.get_monitor_port(default_env)}")
