from functools import partial
from types import FunctionType
from typing import Callable, Optional

import numpy as np
from simple_term_menu import TerminalMenu
from typing_extensions import Concatenate

from simulator import BooleanNetwork


def ShowMenu(options: list[str], title: str = "", highlight_entry: int = 0) -> int:
    menu = TerminalMenu(
        menu_entries=options,
        title=title,
        multi_select=False,
        cursor_index=highlight_entry,
    )

    selectedIndex = menu.show()

    if selectedIndex is None:
        selectedIndex = len(options) - 1

    return selectedIndex  # pyright: ignore[reportReturnType]


def Continue(title="") -> None:
    ShowMenu(title=title, options=[None, "Continue"], highlight_entry=1)
    print()


# def MainMenu() -> None:
#     global currentFunction
#     options = ["Simulator", "Calculator", "Quit"]
#     actions: list[Callable[[], None]] = [SimulatorMenu, StopApplication]

#     selectedIndex = ShowMenu(
#         options, title="Boolean Network Simulator\nby Tom Zuidberg"
#     )

#     currentFunction = actions[selectedIndex]


def StopApplication() -> None:
    quit()


def MainMenu() -> None:
    global currentFunction

    def SelectExampleMenu() -> None:
        def bn4nodeSync() -> None:
            functions = [
                lambda x1, x2, x3, x4: not x2,
                lambda x1, x2, x3, x4: x1,
                lambda x1, x2, x3, x4: x1 ^ x4,
                lambda x1, x2, x3, x4: x3,
            ]
            bn = BooleanNetwork(4).UseSynchronousScheme().SetFunctions(functions)
            global currentFunction
            funcStrings = ["not x2", "x1", "x1 ^ x4", "x3"]
            currentFunction = partial(BooleanNetworkMenu, bn, functions, funcStrings)

        def bn4nodeSeq() -> None:
            functions = [
                lambda x1, x2, x3, x4: not x2,
                lambda x1, x2, x3, x4: x1,
                lambda x1, x2, x3, x4: x1 ^ x4,
                lambda x1, x2, x3, x4: x3,
            ]
            bn = (
                BooleanNetwork(4)
                .UseSequentialScheme([1, 2, 3, 4])
                .SetFunctions(functions)
            )
            global currentFunction
            funcStrings = ["not x2", "x1", "x1 ^ x4", "x3"]
            currentFunction = partial(BooleanNetworkMenu, bn, functions, funcStrings)

        def bn3nodeSync() -> None:
            functions = [
                lambda x1, x2, x3: not x3,
                lambda x1, x2, x3: not x1,
                lambda x1, x2, x3: not x2,
            ]
            bn = BooleanNetwork(3).UseSynchronousScheme().SetFunctions(functions)
            global currentFunction
            funcStrings = ["not x3", "not x1", "not x2"]
            currentFunction = partial(BooleanNetworkMenu, bn, functions, funcStrings)

        def bn3nodeSeq() -> None:
            functions = [
                lambda x1, x2, x3: not x3,
                lambda x1, x2, x3: not x1,
                lambda x1, x2, x3: not x2,
            ]
            bn = (
                BooleanNetwork(3).UseSequentialScheme([1, 2, 3]).SetFunctions(functions)
            )
            global currentFunction
            funcStrings = ["not x3", "not x1", "not x2"]
            currentFunction = partial(BooleanNetworkMenu, bn, functions, funcStrings)

        def ReturnHelper() -> None:
            global currentFunction
            currentFunction = MainMenu

        title = "Select an example boolean network:"
        options = [
            "4-node network with synchronous update",
            "4-node network with sequential update",
            "3-node repressilator network with synchronous update",
            "3-node repressilator network with sequential update",
            "Return to main menu",
        ]
        actions = [bn4nodeSync, bn4nodeSeq, bn3nodeSync, bn3nodeSeq, ReturnHelper]

        actions[ShowMenu(options=options, title=title)]()

    options = [
        "Set up a new boolean network",
        "Set up a boolean network from existing ones",
        "Quit",
    ]

    actions: list[Callable[[], None]] = [
        BooleanNetworkMenu,
        SelectExampleMenu,
        StopApplication,
    ]

    selectedIndex = ShowMenu(
        options, title="Boolean Network Simulator\nby Tom Zuidberg"
    )

    currentFunction = actions[selectedIndex]


def BooleanNetworkMenu(
    bn: Optional[BooleanNetwork] = None,
    funcs: Optional[list[Callable[Concatenate[bool, ...], bool]]] = None,
    funcStrings: Optional[list[str]] = None,
) -> None:
    global currentFunction

    boolNetwork: BooleanNetwork = BooleanNetwork(3)
    writeToFile: bool = False
    done: bool = False
    functions: list = [None for _ in range(3)]
    functionStrings: list = [None for _ in range(3)]
    functionsDirtyFlag = False
    current_highlight = 0

    if bn is not None and funcStrings is not None and funcs is not None:
        boolNetwork = bn
        functions = funcs
        functionStrings = funcStrings

    def Menu() -> None:
        nonlocal current_highlight
        title: str = f"Boolean Network: {boolNetwork} (timeStep | state)"
        options = [
            f"Set size (current: {boolNetwork.size})  WARNING: this will reset all other options!",
            f"Set state (current: {str(boolNetwork)[-boolNetwork.size :]})",
            f"Set update scheme (current: {boolNetwork.updateScheme})",
            *[
                f"Set update function of node x{i} (current: {functionStrings[i - 1]})"
                for i in range(1, boolNetwork.size + 1)
            ],
            None,
            "Update once (hold ENTER for continuous updates)",
            "Update multiple times",
            f"Write to file (works only with multi-update) (current: {writeToFile})",
            "Get Markov-Chain matrix (resets timeStep and state)",
            "Get stationairy distribution (resets timeStep and state)",
            None,
            "Return to main menu",
        ]

        actions = [
            SetSizeHelper,
            SetStateHelper,
            SetUpdateSchemeHelper,
            *[partial(SetFunctionHelper, i) for i in range(boolNetwork.size)],
            None,
            UpdateHelper,
            MultiUpdateHelper,
            ToggleWriteToFileHelper,
            GetMarkovChainHelper,
            GetStableDistrHelper,
            None,
            ReturnHelper,
        ]

        if boolNetwork.updateScheme in ("synchronous", "sequential", None):
            options.pop(-3)
            actions.pop(-3)

        selectedIndex = ShowMenu(
            options=options, title=title, highlight_entry=current_highlight
        )
        current_highlight = selectedIndex
        actions[selectedIndex]()

    def SetSizeHelper() -> None:
        nonlocal boolNetwork, functions, functionStrings
        while True:
            size = input("Set new size (Leave empty to cancel):\n").strip()
            if size == "":
                print("Cancelled")
                return
            try:
                size = int(size)
            except ValueError:
                print("Please enter only integers.")
                continue

            boolNetwork = BooleanNetwork(size)
            functions = [None for _ in range(size)]
            functionStrings = [None for _ in range(size)]
            return

    def SetStateHelper() -> None:
        nonlocal boolNetwork
        while True:
            state = input(
                "Set new state (Leave empty to cancel).\nAccepted format example: '01001'\n"
            ).strip()
            if state == "":
                print("Cancelled")
                return
            try:
                boolNetwork.SetState(state)
            except AssertionError as e:
                print("Invalid input:", e)

    def SetUpdateSchemeHelper() -> None:
        nonlocal boolNetwork

        def SetSequentialHelper() -> None:
            while True:
                seq = input(
                    "Set sequence. (Leave empty to cancel)\nFormat example: '1,4,3,2'\n"
                ).strip()
                if seq == "":
                    print("Cancelled")
                    return

                seq = seq.split(",")
                try:
                    seq = [int(i) for i in seq]
                    boolNetwork.UseSequentialScheme(seq)
                    return
                except Exception as e:
                    print("Invalid input:", e)

        def SetProbabilisticHelper() -> None:
            while True:
                chance = input(
                    "Set flip chance as float between 0.0 and 1.0. (Leave empty to cancel)\n"
                ).strip()
                if chance == "":
                    print("Cancelled")
                    return

                try:
                    chance = float(chance)
                    boolNetwork.UseProbabilisticScheme(chance)
                    return

                except Exception as e:
                    print("Invalid input:", e)

        title = "Select update scheme:"
        options = [
            "Synchronous update scheme",
            "Sequential update scheme",
            "Probabilistic update scheme",
            "Asynchronous random update scheme",
            "Cancel",
        ]
        actions = [
            lambda: boolNetwork.UseSynchronousScheme(),
            SetSequentialHelper,
            SetProbabilisticHelper,
            lambda: boolNetwork.UseAsynchronousRandomScheme(),
            lambda: None,
        ]

        actions[ShowMenu(options=options, title=title)]()

    def UpdateHelper() -> None:
        nonlocal functions, boolNetwork, functionsDirtyFlag
        if functionsDirtyFlag:
            try:
                boolNetwork.SetFunctions(functions)
                functionsDirtyFlag = False
            except Exception as e:
                print("Error while updating the functions:", e)
                return

        try:
            boolNetwork.Update()
        except Exception as e:
            print("Error while simulating once:", e)

    def MultiUpdateHelper() -> None:
        nonlocal boolNetwork, writeToFile, functions, functionsDirtyFlag
        if functionsDirtyFlag:
            try:
                boolNetwork.SetFunctions(functions)
                functionsDirtyFlag = False
            except Exception as e:
                print("Error while updating functions:", e)

        n = 0
        while True:
            n = input(
                "Enter amount of time steps to be simulated. (Leave empty to cancel)\n"
            ).strip()
            if n == "":
                print("Cancelled")
                return

            try:
                n = int(n)
                if n < 0:
                    raise ValueError
                break
            except ValueError:
                print("Invalid input")
        try:
            boolNetwork.Update(
                n, verbose=not writeToFile or n < 1000, writeToFile=writeToFile
            )
            Continue("Writing to file completed") if writeToFile else Continue()
        except Exception as e:
            print("Error while simulating:", e)

    def GetMarkovChainHelper() -> None:
        nonlocal boolNetwork
        print(boolNetwork.GetTransitionMatrix())
        Continue()
        boolNetwork.SetState([False for _ in range(boolNetwork.size)])

    def GetStableDistrHelper() -> None:
        print(boolNetwork.GetStableProbabilityDistribution())
        Continue()
        boolNetwork.SetState([False for _ in range(boolNetwork.size)])

    def SetFunctionHelper(index: int) -> None:
        nonlocal boolNetwork, functions, functionStrings, functionsDirtyFlag
        while True:
            funcString = input(
                f"Set new function for node x{index + 1}. The function will receive all nodes in form of x1, x2, ..., x[size]. (Leave empty to cancel)\n"
            )
            if funcString == "":
                print("Cancelled")
                return
            try:
                func = (
                    "lambda "
                    + ",".join(f"x{i}" for i in range(1, boolNetwork.size + 1))
                    + ":"
                    + funcString
                )
                func = eval(func)
                if not isinstance(func, FunctionType):
                    print("Please enter a valid function. Got: " + func)
                    continue
                functions[index] = func
                functionStrings[index] = funcString
                functionsDirtyFlag = True
                return
            except Exception as e:
                print("Error while parsing function:", e)

    def ToggleWriteToFileHelper():
        nonlocal writeToFile
        writeToFile = not writeToFile

    def ReturnHelper():
        nonlocal done
        done = True

    while not done:
        Menu()

    currentFunction = MainMenu


def main() -> None:
    global currentFunction
    currentFunction = MainMenu
    np.set_printoptions(precision=5, linewidth=300, sign=" ")
    while True:
        currentFunction()


if __name__ == "__main__":
    main()
