from typing import IO, List, TextIO
import time, sys, threading, subprocess


def initialize_adapters(acronyms: List[str], open_pdc_console_path: str):
    print()
    print("Initializing adapters...")

    # Create a lock for synchronizing I/O operations
    io_lock = threading.Lock()

    # Start the console application with text-based IO
    process = subprocess.Popen(
        open_pdc_console_path,
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
    )

    if process.stdin is None or process.stdout is None or process.stderr is None:
        raise Exception("Failed to start openPDCConsole.exe, check the path and permissions.")

    connected = False

    def print_output(pipe: IO[str], output: TextIO):
        for line in iter(pipe.readline, ""):
            nonlocal connected

            if not connected:
                detail = line.strip().lower()

                # Only show relevant lines from console output
                if "dnp3" in detail or "connect" in detail:
                    with io_lock:
                        output.write(f"        {line}\n")
                        output.flush()

                if "connected" in detail:
                    connected = True

    # Read subprocess stdout and print to console stdout
    stdout_thread = threading.Thread(
        target=print_output,
        args=(
            process.stdout,
            sys.stdout,
        ),
        daemon=True,
    )

    stdout_thread.start()

    # Read subprocess stderr and print to console stderr
    stderr_thread = threading.Thread(
        target=print_output,
        args=(
            process.stderr,
            sys.stderr,
        ),
        daemon=True,
    )

    stderr_thread.start()

    # Wait for connection to be established
    print("Waiting for connection to openPDC...")

    sleep_count = 0

    # Wait until the connection is established or a timeout occurs
    # This loop will check every 100 milliseconds for a maximum of 5 seconds
    while not connected:
        time.sleep(0.1)
        sleep_count += 1

        if sleep_count == 50:
            break

    def flush_commands():
        if not process.stdin:
            return

        try:
            with io_lock:
                process.stdin.flush()
        except (OSError, BrokenPipeError) as e:
            print(f"    WARNING: Failed to flush stdin: {e}")

    if connected:
        init_count = 0

        for device_acronym in acronyms:
            print()
            print(f"    Initializing adapter for device: {device_acronym}...")
            print()

            try:
                # Write the command text to initialize adapter to console's standard input
                with io_lock:
                    process.stdin.write(f"init {device_acronym}\n")
            except (OSError, BrokenPipeError) as e:
                print(f"    WARNING: Failed to send command: {e}")
                break

            init_count += 1

            # Flush the output every 20 commands and wait for a second to allow processing
            if init_count % 20 == 0:
                flush_commands()
                time.sleep(1.0)

        # Send final flush to ensure all commands are sent
        if init_count % 20 != 0:
            flush_commands()
            time.sleep(1.0)

    try:
        with io_lock:
            process.stdin.close()
    except (OSError, BrokenPipeError):
        # stdin might already be closed
        pass

    # Signal the application to terminate and wait for it to close
    process.terminate()
    process.wait()

    stdout_thread.join()
    stderr_thread.join()

    process.stdout.close()
    process.stderr.close()

    print()

    if connected:
        print(f"Initialized {len(acronyms):,} adapters.")
    else:
        print("WARNING: Never detected connection to openPDC, adapter initializations may not have succeeded.")
