import os
import sys
import code
import glob
import importlib
from importlib.metadata import metadata
if sys.platform == "win32":
    import msvcrt
else:
    import termios
    import tty
import platform
import shutil
import subprocess
from tkinter import Tk, filedialog
from tkinter.filedialog import askdirectory
import pyautogui
import pyfiglet
from IPython.terminal.prompts import Prompts, Token
import json

# Scope connector allows passing some variables by reference to Python's interactable mode environment.
scope_connector = {
                    'selected_module':{},
                    'header':{},
                    'modules':{},
                    'help':{},
                }

def easyspec_cli():
    
    if ipython_available():
        import IPython
        # IPython statement to allow functions to be called without parenthesis.
        load_meta_file('modules')
        load_meta_file('help')
        load_meta_file('header')
        fullscr = 'fullscreen()'   
        autocall = '%autocall 2'
        prompt_settings = "set_ipython_prompt('easySpec')"
        header_initialize = 'print_header(__package__)'
        imports = 'from easyspec_mods.cli import *'
        setup_commands = [imports, fullscr, autocall, prompt_settings, header_initialize]
        # Start IPython shell quietly if available
        IPython.start_ipython(argv=[], exit_code=0, user_ns=globals(), display_banner=False, exec_lines=setup_commands )
    else:
        # Fall back to regular Python shell
        code.interact(banner='', local=globals())


# The package map file (modules.json) is created during the build and placed in the first-level modules directory.
# It maps all the modules composing the package, giving the name, current version and description.
def load_meta_file(meta_file_name):
    modules_path = os.path.join(os.path.dirname(__file__), f"{meta_file_name}.json")
    with open(modules_path, 'r') as f:
        global scope_connector
        scope_connector[f"{meta_file_name}"] = json.load(f)

def modules():
    resetUI()
    display_modules(scope_connector['modules'])
    module_selection(globals())
globals()['m'] = modules

def help():
    resetUI()
    global scope_connector
    display_help(scope_connector['help'])
globals()['h'] = help

def procedures():
    resetUI()
    print_procedures(globals())
globals()['p'] = procedures

def ipython_available():
    try:
        import IPython
        return True
    except ImportError:
        return False
    
# Function to center text with padding
def center_text(text, width):
    return text.center(width)

def move_cursor(row, col):
    """Moves the cursor to the specified row and column."""
    sys.stdout.write(f"\033[{row};{col}H")
    

def print_at_position(data_string, row, col):
    """Print the string at the specified row and column."""
    move_cursor(row, col)
    print(data_string)
    sys.stdout.flush()

def read_char():
    """Read a single character from input, handling both Windows and Unix."""
    if sys.platform == "win32":
        return msvcrt.getwch()  # Windows version
    else:
        # Unix-based system: set terminal to raw mode to read a single char
        fd = sys.stdin.fileno()
        old_settings = termios.tcgetattr(fd)
        try:
            tty.setraw(fd)
            char = sys.stdin.read(1)
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
        return char

def get_cursor_position():
    """Retrieve the current cursor position (row, col) in Windows without printing artifacts."""
    
    # Send the escape sequence to query the cursor position
    sys.stdout.write("\033[6n")
    sys.stdout.flush()

    response = ""
    
    while True:
        char = read_char()   # Read one character at a time
        response += char
        if char == "R":  # End of response
            break

    # Parse the response
    if response.startswith("\x1b[") and response.endswith("R"):
        try:
            position = response[2:-1]  # Remove '\x1b[' and 'R'
            row, col = map(int, position.split(";"))
            return [row, col]
        except ValueError:
            raise Exception(f"Failed to parse cursor position: {response}")
    else:
        raise Exception(f"Unexpected response: {response}")

def hex_to_rgb(hex_color):
    """Convert hex color code to RGB tuple."""
    hex_color = hex_color.lstrip('#')
    return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))

def rgb_to_ansi_rgb(r, g, b):
    """Convert RGB values to ANSI RGB code."""
    return 16 + (36 * (r // 51)) + (6 * (g // 51)) + (b // 51)

def print_tag(data_string, start_position=None, color_hex=None):
    if start_position is None:
        start_position = get_cursor_position()

    row = start_position[0]
    col = start_position[1]

    horizontal_padding = ' ' * 1
    tag_width = len(data_string) + 2 * len(horizontal_padding)
    tag_height = 1
    right_vertical_bars = ['\u23B9']
    left_vertical_bars = ['\u23B8']

    tw = tag_width
    th = tag_height
    hp = horizontal_padding
    rvb = right_vertical_bars[0]
    lvb = left_vertical_bars[0]

    # If a color is provided, convert it to ANSI escape code
    if color_hex:
        r, g, b = hex_to_rgb(color_hex)
        ansi_color_code = rgb_to_ansi_rgb(r, g, b)
        color_code = f"\033[38;5;{ansi_color_code}m"  # Set the color
        reset_code = "\033[39m"  # Reset to default color
    else:
        color_code = ""
        reset_code = ""

    # Print the tag with the specified color
    print_at_position(f"{color_code}{' ' * 1}{'\u005F' * tw}", row, col)
    print_at_position(f"{rvb}{hp}{data_string}{hp}{lvb}", row + 1, col)
    print_at_position(f"{' ' * 1}{'‾' * tw}{reset_code}", row + 2, col)


def print_tag_list(tag_list, start_position = [0,0], color_hex=None):
    if tag_list:
        tag_string = tag_list.pop()
        print_tag(tag_string, start_position, color_hex)
        start_position[1] += len(tag_string) + 6
        print_tag_list(tag_list, start_position, color_hex)    
    return

def print_app_name(app_name, color_hex = None):
    # Generate ASCII art from the input text
    ascii_art = pyfiglet.figlet_format(app_name)

    # Get the width of the terminal
    terminal_width = os.get_terminal_size().columns

    # Split the ASCII art into lines
    lines = ascii_art.splitlines()

    # If a color is provided, convert it to ANSI escape code
    if color_hex:
        r, g, b = hex_to_rgb(color_hex)
        ansi_color_code = rgb_to_ansi_rgb(r, g, b)
        color_code = f"\033[38;5;{ansi_color_code}m"  # Set the color
        reset_code = "\033[39m"  # Reset to default color
    else:
        color_code = ""
        reset_code = ""

    # Print each line centered
    for line in lines:
        centered_line = line.center(terminal_width)
        print(f"{color_code}{centered_line}{reset_code}")   

def print_info_box():
    pass

def print_startup_info():
    pass

def print_header(package_name):
    
    # Fetch pyproject header section
    header = globals()['scope_connector']['header']
    brand_name = header['brand_name']
    slogan = header['slogan']
    creators = header['creators']

    # Fetch pyproject project section
    project_section = metadata(package_name)
    pkg_author = project_section.get("Author", next((value for key, value in project_section.items() if key.lower() == "author-email"), "Unknown Email")).strip()
    pkg_description = project_section.get("Summary", "No Description Available").strip()
    
    python_version = sys.version.split(" ")[0].strip()  # Get the version of Python in use
    
    #Declaring existing commands to be displayed in the navbar
    easyspec_commands = {
                    'new project' : 'initializes a new project directory',
                    'open' : 'working directory selection',
                    'files' : 'opens the project files manager',
                    'modules' : 'module selection',
                    'procedures' : 'lists procedures for current module',
                    # 'run script' : 'run python script',
                    # 'save' : 'save iPython session as script',
                    'clear'   : 'clear interface',
                    'help' : 'extra information on commands',
                    'exit' : 'exit easySpec cli',
                    }
    
    terminal_width = os.get_terminal_size().columns
    header_width = terminal_width
    
    #Defining tags contents
    python_tag = f"Python {python_version}" 
    license_tag = f"{project_section.get("License", "-").strip().upper()}" 
    OS_tag = f"{platform.platform()}".strip().upper() 

    print("\033[H\033[J\033[3J", end="")  # Clear screen and scrollback buffer
    # print_app_name(package_name, '#1B3B6F')
    print_app_name(brand_name, '#1B3B6F')
    print()
    print(center_text(slogan, header_width))
    print()
    for creator in creators:
        print(center_text(f"{creator['name']} <{creator['email']}>", header_width))
    print()
    print_navbar(easyspec_commands)
    print_tag_list([license_tag, OS_tag, python_tag], get_cursor_position(), '#FF9C80')    
    # Current directory
    print_tag(f"{os.getcwd()}", color_hex='#E0E0E0')    

def module_selection(global_scope):
    print(f"\nTo leave the module selector, press enter with the field empty.")
    module_name = input("\n   Enter the module name: ").strip()
    if len(module_name) == 0:
        resetUI()
        return
    module_path = (__package__.strip() + '.' + module_name.strip()).strip().lower()
    imported_objects = {}

    try:
        # Dynamically import the module
        module = importlib.import_module(module_path)
        # Use the passed global scope to assign imported attributes
        for attr_name in dir(module):
            if not attr_name.startswith("_"):  # Ignore private/protected attributes
                global_scope[attr_name] = getattr(module, attr_name)
                
                # Add to the dictionary for return
                imported_objects[attr_name] = getattr(module, attr_name)
                
                # Print the attribute being imported
                # print(f"Imported: {attr_name}")

        resetUI()

        global_scope['scope_connector']['selected_module'] = {'module_name':module_name, 'module_version':module.__version__, 'objects':imported_objects }
        
        # print(f"All functions and variables from '{module_path}' imported successfully.")
        print_procedures(global_scope) 
        return imported_objects  # Return the dictionary with all imported attributes
    
    except ModuleNotFoundError:
        print(f"Error: Module '{module_path}' not found.")
    except Exception as e:
        print(f"Error importing from module '{module_path}': {e}")
        return {}
    
def print_procedures(global_scope):

    width=os.get_terminal_size().columns

    print_section_title('procedures')
    
    module_selected = global_scope['scope_connector']['selected_module']
    if len(module_selected) == 0:
        print(f'''{' '*4}No module selected.''')
        return 
    
    module_name  = module_selected['module_name']
    module_version  = module_selected['module_version']
    objects  = module_selected['objects']
    # Selects only functions defined in the package's files.
    function_procedures = {
        k: v
        for k, v in objects.items()
        if callable(v) 
        and getattr(v, "__module__", "").startswith(f"{__package__}.{module_name}")
    }
    
    terminal_width = os.get_terminal_size().columns
    indent = ' ' * 4
    col_width = 25  # Adjust the column width as needed
    max_cols = max(1, terminal_width // col_width)  # Calculate max columns based on terminal width

    # Get function names
    procedure_names = list(function_procedures.keys())

    # Split names into rows based on available columns
    rows = [procedure_names[i:i + max_cols] for i in range(0, len(procedure_names), max_cols)]

    # Print current module name
    print(f"{indent}{module_name.upper()} MODULE {module_version}\n")

    # Print each row with equal spacing
    for row in rows:
        formatted_row = "".join(f"{name:<{col_width}}" for name in row)
        print(indent + formatted_row)

    print(f"\n{'-' * terminal_width}".center(terminal_width))
    print(f"\nFor procedure's signature, enter name without arguments.")


def print_navbar(commands):

    project_section = metadata(__package__)
    pkg_version = project_section.get("Version", "Unknown Version").strip()

    # Console window size
    terminal_width = os.get_terminal_size().columns
    
    header_width = terminal_width

    fillers = ['░', '█']
    decoration_left = f"{fillers[0]*4}"
    title = f"{' '*1}easySpec {pkg_version}{' '*6}"
    # Format the command list
    print(f"{'_' * header_width}".center(header_width))
    command_line = '| ' + ' | '.join(commands.keys()).upper() + ' |'
    formatted_keys = decoration_left + title + command_line
    decoration_right = f"{fillers[0]*(header_width-len(formatted_keys))}"
    formatted_keys = formatted_keys + decoration_right
    print(formatted_keys)
    print(f"{'‾' * header_width}".center(header_width))


def display_modules(modules):
    width=os.get_terminal_size().columns
    print_section_title('modules')

    for idx, (module, details) in enumerate(modules.items(), start=1):
        # print(f"[{idx}] {module.capitalize()} v{details['version']}")
        print(f" ° {module.capitalize()} v{details['version']}")
        print(f"   {details['description']}\n")

def display_help(help_info):
    resetUI()
    print_elegant_json(help_info)

def print_elegant_json(data, indent=2, width=os.get_terminal_size().columns, level=2):
    """
    Elegantly prints a hierarchical structure from a JSON-like object.

    Args:
        data (dict or list): The data to print (can be nested).
        indent (int): Number of spaces for indentation per level.
        width (int): The total width of the display (optional).
        level (int): Used internally for recursion to track depth.

    Returns:
        None
    """
    
    print_section_title('HELP')

    def recursive_print(obj, level):
        """Recursively print dictionary or list with indentation."""
        prefix = " " * (level * indent)  # Indentation based on level
        if isinstance(obj, dict):
            for key, value in obj.items():
                print(f"\n{prefix} {key}:")
                recursive_print(value, level + 1)
        elif isinstance(obj, list):
            for item in obj:
                recursive_print(item, level)
        else:
            # Print leaf node (e.g., str, int, float)
            print(f"\n{prefix}  {obj}")

    recursive_print(data, level)

def resetUI():
    print_header(__package__)
globals()['c'] = resetUI

def easy_exit():
    """Custom exit function that works for both IPython and Python."""
    print_header(__package__)
    'Thank you!'

    # Clear the console
    os.system('cls' if os.name == 'nt' else 'clear')

    try:
        from IPython import get_ipython
        ipython = get_ipython()
        if ipython is not None:
            # Print a message and exit IPython
            print("Exiting IPython...")
            os._exit(0)  # Force exit without cleanup
    except ImportError:
        pass

    # Exit standard Python
    os._exit(0)
globals()['e'] = easy_exit

def change_working_directory_explorer():
    """Change the working directory using a file explorer."""
    resetUI()

    # Suppress the Tkinter root window
    root = Tk()
    root.withdraw()  # Hide the main Tkinter window
    root.attributes('-topmost', True)  # Make the dialog appear on top

    # Open the file explorer to select a directory
    new_dir = askdirectory(title="Select New Working Directory")

    # Validate the directory selected
    if new_dir:
        try:
            os.chdir(new_dir)
            working_directory = os.getcwd()
            root.destroy()  # Ensure the root window is properly destroyed and control is given back to the terminal window
            print(f"Successfully changed the working directory to: {working_directory}")
            resetUI()
            if check_project_file():
                manage_project()
        except FileNotFoundError:
            print(f"Error: The directory '{new_dir}' does not exist.")
        except PermissionError:
            print(f"Error: You do not have permission to access '{new_dir}'.")
    else:
        print("No directory selected.")
        root.destroy() 
globals()['o'] = change_working_directory_explorer



def create_new_project():
    resetUI()
    print_section_title('project creation')
    working_directory = os.getcwd()
    # print(f"Current working directory: {working_directory}")
    
    # Ask for the project name
    project_name = input(f"Enter the project name: ").strip()
    project_path = os.path.join(working_directory, project_name)

    # Define the fixed folder structure
    folders = {
        "bias": "bias .fits files here.",
        "dark": "dark .fits files here (optional).",
        "flat": "flat .fits files here.",
        "lamp": "lamp .fits files here.",
        "std_star": "standard star .fits files here."
    }

    # Get targets
    targets = []
    while True:
        target_name = input("\nEnter the name of a target: ").strip()
        if target_name:
            targets.append(target_name)
        another = input("\nDo you want to add another target? (y/n): ").strip().lower()
        if another != 'y':
            break

    # Create the project folder
    os.makedirs(project_path, exist_ok=True)

    # Create the fixed directories with placeholder files
    for folder, description in folders.items():
        folder_path = os.path.join(project_path, folder)
        os.makedirs(folder_path, exist_ok=True)
        # with open(os.path.join(folder_path, "README.txt"), "w") as f:
        #     f.write(description)

    # Create target folders
    for index, target in enumerate(targets, start=1):
        folder_path = os.path.join(project_path, target)
        os.makedirs(folder_path, exist_ok=True)
        # with open(os.path.join(folder_path, "README.txt"), "w") as f:
        #     f.write(f"{star} .fits files here.")
    os.chdir(project_path)

    # Create project.easy file
    with open(os.path.join('./', "project.easy"), "w") as f:
        f.write(f"easySpec Project {project_name}")

    resetUI()
    print(f"\nProject '{project_name}' created successfully:\n")
    manage_project()
globals()['n'] = create_new_project

def check_project_file(folder_path = './'):

    easy_files = glob.glob(os.path.join(folder_path, "*.easy"))

    if easy_files:
        return True
    else:
        print(f"\n{' '*4}No '.easy' project file found.")
        return False

def manage_project():

    resetUI()
    if check_project_file():
        width=os.get_terminal_size().columns
        print_section_title('file manager')
        
        print_project_structure('./')
        
        print("\nEnter a folder name to select its files using the explorer.")
        print("To leave the manager, press enter with the field empty.\n")

        project_folders = [folder for folder in os.listdir('.') if os.path.isdir(folder)]

        folder_name = input("Enter folder name: ").strip()

        if folder_name.lower() == '':
            print("Exiting project management.")
            resetUI()
            return

        if folder_name in project_folders:
            folder_path = os.path.join('.', folder_name)
            print(f"\nOpening {folder_name} for file uploads...")
            upload_files(folder_path)
            manage_project()
        else:
            print("Invalid folder name. Please enter a valid folder or type 'exit' to leave.")
            manage_project()
globals()['f'] = manage_project

def print_project_structure(path, indent=4, is_root=True):
    """ Recursively prints the directory structure, including the root folder. """
    
    if is_root:
        print(f"|-- {os.path.basename(os.path.abspath(path))}\n")
    
    for item in os.listdir(path):
        item_path = os.path.join(path, item)
        if item != 'project.easy':
            print(' ' * indent + '|-- ' + item)
        if os.path.isdir(item_path):
            print_project_structure(item_path, indent + 4, is_root=False)

def open_project_directory(path="."):
    path = os.path.abspath(path)
    system = platform.system()

    if system == "Windows":
        os.system(f'explorer "{path}"')
    elif system == "Darwin":  # macOS
        os.system(f'open "{path}"')
    elif system == "Linux":
        os.system(f'xdg-open "{path}"')
    else:
        print("Unsupported operating system")

def upload_files(destination_folder):
    # Oculta a janela principal do Tkinter
    root = Tk()
    root.withdraw()

    # Abre a janela para selecionar múltiplos arquivos
    file_paths = filedialog.askopenfilenames(title="Select files to upload")

    if not file_paths:
        print("No files selected.")
        return

    # Certifique-se de que a pasta de destino existe
    os.makedirs(destination_folder, exist_ok=True)

    # Copia os arquivos selecionados para a pasta de destino
    for file_path in file_paths:
        file_name = os.path.basename(file_path)
        destination_path = os.path.join(destination_folder, file_name)
        shutil.copy(file_path, destination_path)
        # print(f"Uploaded: {file_name} -> {destination_folder}")
    
    root.destroy()  # Ensure the root window is properly destroyed and control is given back to the terminal window.

    # print("All selected files have been uploaded successfully.")

def fullscreen():
    """ Force terminal window to fullscreen based on the operating system. """
    
    current_os = platform.system()

    if current_os == "Windows":
        # Maximize the terminal window using keyboard shortcuts (Alt + Space, then X).
        pyautogui.hotkey('alt', 'space')  # Open the window menu
        pyautogui.press('x')  # Press 'x' to maximize

    elif current_os == "Linux":
        # For Linux, we use `wmctrl` to maximize the terminal window.
        subprocess.run(["wmctrl", "-r", ":ACTIVE:", "-b", "add,maximized_vert,maximized_horz"])

    elif current_os == "Darwin":
        # For macOS, use AppleScript to resize the terminal window.
        applescript = """
        tell application "Terminal"
            activate
            set bounds of front window to {0, 0, 1920, 1080}  -- Adjust the coordinates as needed
        end tell
        """
        subprocess.run(["osascript", "-e", applescript])

    else:
        print("Unsupported OS for fullscreen terminal window.")

def print_section_title(title):
    terminal_width = os.get_terminal_size().columns    
    width = terminal_width
    print(" ")
    print(" " * ((width - 10) // 2) + f"{title.upper()}")
    # print("=" * width)
    print(" ")

class MyPrompt(Prompts):
    def __init__(self, shell, text=''):  # Default: Gold
        super().__init__(shell)
        self.text = text

    def in_prompt_tokens(self, cli=None):
        return [
            (Token.PromptNum, f"[{self.shell.execution_count}]"),
            (Token.Prompt, f"{' '*1}{self.text}:"),
            
        ]

def set_ipython_prompt(text):
    ip = get_ipython()
    if ip is None:
        print("Error: Not running inside an IPython session.")
        return
    ip.prompts = MyPrompt(ip, text)