XTAL = 16.66666  # MHz

def get_top_cpu_configs(target_cpu_mhz, multiplier, top_n=3):
    configs = []

    for pll_m in range(1, 9):
        for pll_n in range(1, 64):
            fsb = (XTAL / pll_m) * pll_n
            cpu = fsb * multiplier

            for fsb_div in range(1, 5):
                vco = fsb * fsb_div * 2
                for mem_div in range(1, 5):
                    ram = vco / (mem_div * 2)
                    if not (180 <= ram <= 266):
                        continue

                    delta = abs(cpu - target_cpu_mhz)
                    hexval = f"0x{((mem_div << 4) | fsb_div):02X}{pll_n:02X}{pll_m:02X}".lower()

                    # Only flag known stock config
                    if hexval == "0x230801":
                        note = "🟢 Stock CPU Clock"
                    else:
                        ideal_fsb = target_cpu_mhz / multiplier
                        ideal_vco = ideal_fsb * 3 * 2
                        ideal_ram = ideal_vco / 4

                        if abs(cpu - target_cpu_mhz) <= 2:
                            if ram >= ideal_ram * 0.97:
                                note = "Strong balance"
                            else:
                                note = "Lower RAM (more margin)"
                        elif cpu > target_cpu_mhz + 2:
                            note = "Slightly faster CPU"
                        else:
                            note = "Slightly under target"

                    configs.append({
                        "CPU_MHz": round(cpu, 2),
                        "RAM_MHz": round(ram, 2),
                        "FSB_MHz": round(fsb, 2),
                        "Hex": hexval,
                        "PLL_M": pll_m,
                        "PLL_N": pll_n,
                        "FSB_DIV": fsb_div,
                        "MEM_DIV": mem_div,
                        "Note": note,
                        "Delta": delta
                    })

    configs.sort(key=lambda x: x["Delta"])
    return configs[:top_n]

def generate_nvpll_top3(target_gpu_mhz, top_n=3):
    results = []

    for pll_m in range(1, 5):
        for pll_n in range(1, 64):
            for pll_p in range(0, 4):
                freq = pll_n * XTAL / (2 ** pll_p) / pll_m
                delta = abs(freq - target_gpu_mhz)
                hexval = f"0x{pll_p:02X}{pll_n:02X}{pll_m:02X}".lower()

                note = ""
                if hexval == "0x011c01":
                    note = "🟢 Stock GPU (233 MHz)"

                results.append({
                    "GPU_MHz": round(freq, 2),
                    "PLL_M": pll_m,
                    "PLL_N": pll_n,
                    "PLL_P": pll_p,
                    "Hex": hexval,
                    "Delta": delta,
                    "Note": note
                })

    results.sort(key=lambda x: x["Delta"])
    return results[:top_n]

def decode_cpu_hex(hex_str, multiplier):
    hex_str = hex_str.lower().replace("0x", "")
    if len(hex_str) != 6:
        return {"error": "Hex must be exactly 6 digits (e.g. 0x230801)"}

    try:
        val = int(hex_str, 16)
    except ValueError:
        return {"error": "Hex contains invalid characters."}

    pll_m = val & 0xFF
    pll_n = (val >> 8) & 0xFF
    misc = (val >> 16) & 0xFF
    mem_div = (misc >> 4) & 0xF
    fsb_div = misc & 0xF

    # Safety check to prevent division by zero or clearly invalid config
    if pll_m == 0 or pll_n == 0 or fsb_div == 0 or mem_div == 0:
        return {"error": "Invalid CPU hex format (one or more fields = 0). Possibly a GPU hex value?"}

    fsb = (XTAL / pll_m) * pll_n
    cpu = fsb * multiplier
    vco = fsb * fsb_div * 2
    ram = vco / (mem_div * 2)

    note = ""
    if hex_str == "230801":
        note = "🟢 Stock CPU Clock"

    return {
        "CPU_MHz": round(cpu, 2),
        "RAM_MHz": round(ram, 2),
        "FSB_MHz": round(fsb, 2),
        "PLL_M": pll_m,
        "PLL_N": pll_n,
        "FSB_DIV": fsb_div,
        "MEM_DIV": mem_div,
        "Note": note
    }



def decode_gpu_hex(hex_str):
    hex_str = hex_str.lower().replace("0x", "")
    if len(hex_str) != 6:
        return {"error": "Hex must be exactly 6 digits (e.g. 0x011c01)"}

    try:
        val = int(hex_str, 16)
    except ValueError:
        return {"error": "Hex contains invalid characters."}

    pll_m = val & 0xFF
    pll_n = (val >> 8) & 0xFF
    pll_p = (val >> 16) & 0xFF

    # Basic range validation for GPU PLL
    if pll_m == 0 or pll_n == 0 or pll_m > 4 or pll_n > 63 or pll_p > 3:
        return {"error": "Invalid GPU PLL format (PLL_M/N/P out of range). Possibly a CPU PLL hex?"}

    freq = pll_n * XTAL / (2 ** pll_p) / pll_m

    # Reasonable GPU clock bounds
    if freq < 100 or freq > 400:
        return {"error": f"Decoded GPU frequency {freq:.2f} MHz is out of expected range (100–400 MHz). Possibly a CPU hex?"}

    note = ""
    hex_full = f"0x{pll_p:02x}{pll_n:02x}{pll_m:02x}"
    if hex_full == "0x011c01":
        note = "🟢 Stock GPU Clock (233 MHz)"
    elif freq > 266:
        note = "⚠️ Overclocked GPU - Use with caution"

    return {
        "GPU_MHz": round(freq, 2),
        "PLL_M": pll_m,
        "PLL_N": pll_n,
        "PLL_P": pll_p,
        "Note": note
    }

# === Main Menu Loop ===
if __name__ == "__main__":
    while True:
        print("\n=== Cerbios Clock Coefficient Calculator ===")
        print("1 = Calculate CPUMPLLCoeff (CPU)")
        print("2 = Calculate NVPLLCoeff (GPU)")
        print("3 = Decode a Hex Value")
        print("0 = Exit")
        choice = input("Choose an option: ").strip()

        if choice == "0":
            print("Exiting. Goodbye!")
            break

        elif choice == "1":
            try:
                target_cpu = float(input("Enter target CPU MHz (e.g. 733 or 1400): "))
                multiplier = float(input("Enter CPU multiplier (e.g. 5.5 (Stock) or 10.5 (SL6BY): "))
            except ValueError:
                print("Invalid input. Please enter numeric values.")
                continue

            results = get_top_cpu_configs(target_cpu, multiplier)
            print(f"\nTop {len(results)} PLL configs for target {target_cpu} MHz (multiplier {multiplier}):")
            for i, r in enumerate(results, 1):
                print(f"\n#{i}:")
                print(f"  CPU     : {r['CPU_MHz']} MHz")
                print(f"  RAM     : {r['RAM_MHz']} MHz")
                print(f"  FSB     : {r['FSB_MHz']} MHz")
                print(f"  Hex     : {r['Hex']}")
                print(f"  PLL_M   : {r['PLL_M']}")
                print(f"  PLL_N   : {r['PLL_N']}")
                print(f"  FSB_DIV : {r['FSB_DIV']}")
                print(f"  MEM_DIV : {r['MEM_DIV']}")
                print(f"  Notes   : {r['Note']}")

        elif choice == "2":
            try:
                target_gpu = float(input("Enter target GPU MHz (e.g. 233 or 266): "))
            except ValueError:
                print("Invalid input. Please enter a numeric value.")
                continue

            if target_gpu > 266:
                print("\n⚠️ WARNING: GPU clocks above 266 MHz are considered unstable on most retail Xbox consoles.")
                print("Use only with adequate cooling and at your own risk.\n")

            top3 = generate_nvpll_top3(target_gpu)
            print(f"\nTop {len(top3)} GPU PLL configs for target {target_gpu} MHz:")
            for i, r in enumerate(top3, 1):
                print(f"\n#{i}:")
                print(f"  GPU     : {r['GPU_MHz']} MHz")
                print(f"  Hex     : {r['Hex']}")
                print(f"  PLL_N   : {r['PLL_N']}")
                print(f"  PLL_M   : {r['PLL_M']}")
                print(f"  PLL_P   : {r['PLL_P']}")
                if r["Note"]:
                    print(f"  Notes   : {r['Note']}")

        elif choice == "3":
            print("Select decode mode:")
            print("1 = CPU")
            print("2 = GPU")
            mode_choice = input("Choose mode (1 or 2): ").strip()
            hexval = input("Enter 6-digit hex value (e.g. 0x230801 or 0x011c01): ").strip()

            if mode_choice == "1":
                try:
                    multiplier = float(input("Enter CPU multiplier (e.g. 5.5 (Stock) or 10.5 (SL6BY): "))
                except ValueError:
                    print("Invalid multiplier input.")
                    continue
                result = decode_cpu_hex(hexval, multiplier)

            elif mode_choice == "2":
                result = decode_gpu_hex(hexval)
            else:
                print("Invalid mode selected.")
                continue

            if result:
                if "error" in result:
                    print(f"Error: {result['error']}")
                else:
                    print("\n=== Decoded PLL Coefficient ===")
                    for k, v in result.items():
                        print(f"{k:<10}: {v}")
            else:
                print("Invalid or malformed hex value.")

        else:
            print("Invalid menu option.")

