import pandas as pd
import re
import json

# --- THE INTELLIGENCE DICTIONARY ---
# Hardcode the most common, major systems here. The parser will automatically 
# discover others, but this guarantees the big ones format beautifully.
MASTER_ROOTS = {
    "ACC": "Adaptive Cruise Control (ACC)",
    "TSR": "Traffic Sign Recognition (TSR)",
    "LKS": "Lane Keeping System (LKS)",
    "LKA": "Lane Keeping Assist (LKA)",
    "LCA": "Lane Centering Assist (LCA)",
    "BLIS": "Blind Spot Info System (BLIS)",
    "CTA": "Cross Traffic Alert (CTA)",
    "HUD": "Heads Up Display (HUD)",
    "BA": "Brake Assist (BA)",
    "DAS": "Driver Alert System (DAS)",
    "FCW": "Forward Collision Warning (FCW)",
    "DA": "Distance Alert (DA)",
    "DIST": "Distance Alert (DA)" # Catches "DistAlert"
}

def make_readable_name(text):
    # Insert space between lowercase and uppercase letters (e.g., SlaveExists -> Slave Exists)
    text = re.sub(r'([a-z])([A-Z])', r'\1 \2', text)
    # Insert space between letters and numbers (e.g., Camera2 -> Camera 2)
    text = re.sub(r'([a-zA-Z])(\d)', r'\1 \2', text)
    # Replace underscores with spaces
    text = text.replace('_', ' ')
    # Collapse any multiple spaces into a single space and trim edges
    return re.sub(r'\s+', ' ', text).strip()

def parse_forscan_excel(filepath, sheet_name=0):
    df = pd.read_excel(filepath, sheet_name=sheet_name, header=None)
    blocks = []
    
    # ==========================================
    # PASS 1: THE DATA EXTRACTION
    # ==========================================
    i = 0
    while i < len(df):
        row = df.iloc[i]
        addr = str(row[0]).strip()
        
        # Identify a valid address row (contains a hyphen and starts with a number)
        if pd.notnull(row[0]) and '-' in addr and addr[0].isdigit():
            
            mask_parts = []
            first_non_mask_val = ""
            
            for col_idx in range(1, len(row)):
                val = str(row[col_idx]).strip()
                if val == "nan" or val == "": 
                    continue
                if re.fullmatch(r'[\*x\-]+', val):
                    mask_parts.append(val)
                elif "Names & Values" not in val and "Address" not in val:
                    first_non_mask_val = val
                    break
            
            mask = " ".join(mask_parts)
            is_matrix = bool(re.match(r'^[0-9A-Fa-f]{1,2}\s*=', first_non_mask_val))
            
            block = {
                "address": addr,
                "mask": mask,
                "type": "discrete_matrix" if is_matrix else "continuous_input",
                "schema": [],
                "states": []
            }
            
            if is_matrix:
                title_text = ""
                if i > 0:
                    title_row = df.iloc[i-1]
                    for col_idx in range(1, len(title_row)):
                        val = str(title_row[col_idx]).strip()
                        if val != "nan" and val != "":
                            title_text = val
                            break
                
                if title_text:
                    schema_items = [item.strip() for item in title_text.split("•")]
                    for item in schema_items:
                        match = re.search(r'(.*?)\s*\((.*?)\)', item)
                        
                        if match:
                            raw_name = match.group(1).strip()
                            acronym = match.group(2).strip()
                        else:
                            raw_name = item
                            acronym = item
                            
                        is_config = False
                        config_type = None
                        base_name = raw_name
                        base_acronym_hint = acronym
                        
                        if "_" in raw_name and "Cfg" in raw_name.split("_")[0]:
                            is_config = True
                            parts = raw_name.split("_", 1) 
                            
                            config_type_raw = parts[0]
                            config_type = config_type_raw.replace("Cfg", "")
                            base_name = parts[1]
                            
                            cfg_acronym_prefix = "".join([c for c in config_type_raw if c.isupper()])
                            if acronym.startswith(cfg_acronym_prefix) and len(acronym) > len(cfg_acronym_prefix):
                                base_acronym_hint = acronym[len(cfg_acronym_prefix):]

                        readable_name = make_readable_name(base_name)

                        block["schema"].append({
                            "readable_name": readable_name,
                            "raw_name": raw_name,
                            "acronym": acronym,
                            "is_config": is_config,
                            "config_type": config_type,
                            "base_name": base_name,
                            "base_acronym_hint": base_acronym_hint
                        })
                        
                j = i
                while j < len(df):
                    check_row = df.iloc[j]
                    if j > i and pd.notnull(check_row[0]):
                        break 
                    
                    def extract_states(hex_col, desc_col):
                        if hex_col >= len(check_row) or desc_col >= len(check_row): return
                        
                        val_hex = str(check_row[hex_col]).strip()
                        val_desc = str(check_row[desc_col]).strip()
                        
                        if val_hex != "nan" and "=" in val_hex:
                            hex_clean = val_hex.split("=")[0].strip()
                            parsed_states = []
                            
                            if val_desc != "nan" and val_desc != "":
                                desc_chunks = [chunk.strip() for chunk in val_desc.split("•")]
                                for idx, chunk in enumerate(desc_chunks):
                                    parts = chunk.split(" ", 1)
                                    parsed_states.append({
                                        "index": idx,
                                        "acronym_used": parts[0],
                                        "state_value": parts[1] if len(parts) > 1 else ""
                                    })
                            
                            block["states"].append({
                                "hex": hex_clean,
                                "raw_string": val_desc if val_desc != "nan" else "",
                                "parsed": parsed_states
                            })
                    
                    extract_states(4, 5)
                    extract_states(7, 8)
                    
                    j += 1
                    
                try:
                    block["states"] = sorted(block["states"], key=lambda x: int(x["hex"], 16))
                except ValueError:
                    block["states"] = sorted(block["states"], key=lambda x: str(x["hex"]))
                
                blocks.append(block)
                i = j 
                continue
                
            else:
                block["schema"] = [{"instruction": first_non_mask_val}]
                blocks.append(block)
                i += 1
                continue
        else:
            i += 1
            
            
    # ==========================================
    # PASS 2: THE HARVESTER & TAGGER
    # ==========================================
    prefix_counts = {}
    
    # Step A: Harvest Dynamic Roots
    # We slice off the first logical chunk of the base_name (e.g., 'ACC' from 'ACCType')
    for block in blocks:
        if block["type"] == "discrete_matrix":
            for item in block["schema"]:
                base = item.get("base_name", "")
                # Regex pulls the leading CamelCase chunk
                match = re.match(r'^([A-Z]+[a-z]*|[A-Z]+)(?=[A-Z_]|$)', base)
                if match:
                    prefix = match.group(1).upper()
                    prefix_counts[prefix] = prefix_counts.get(prefix, 0) + 1

    # Merge dynamic roots with the MASTER_ROOTS dictionary
    dynamic_roots = dict(MASTER_ROOTS)
    for prefix, count in prefix_counts.items():
        if count > 1 and prefix not in dynamic_roots and len(prefix) > 1:
            # If a prefix appears multiple times (like "Slave"), it becomes a dynamic group
            dynamic_roots[prefix] = f"{prefix.capitalize()} Features"

    # Step B: Tag every feature with its calculated Major Group
    for block in blocks:
        if block["type"] == "discrete_matrix":
            for item in block["schema"]:
                base_name = item.get("base_name", "")
                # Default fallback: if no root matches, it is its own major group
                major_group = item.get("readable_name", "Unknown")
                
                # Sort roots by length descending to match the longest (most accurate) prefix first
                for root in sorted(dynamic_roots.keys(), key=len, reverse=True):
                    if base_name.upper().startswith(root):
                        major_group = dynamic_roots[root]
                        break
                        
                item["major_group"] = major_group
            
    return blocks

if __name__ == "__main__":
    input_file = r"C:\Users\kylem\Documents\Lightning Data\Forscan Sheets Tools\Livingitup\IPMA Livnitup's FORScan - 2021-26 F150.xlsx"
    output_file = "parsed_forscan.json"
    
    parsed_data = parse_forscan_excel(input_file, sheet_name=0) 
    
    with open(output_file, 'w', encoding='utf-8') as f:
        json.dump(parsed_data, f, indent=2, ensure_ascii=False)
        
    print(f"Success! Parsed {len(parsed_data)} memory blocks into {output_file}")