import sys, os
import subprocess
from pathlib import Path
import queue
import threading
import warnings
from nextnanopy.utils.misc import get_filename, mkdir_if_not_exist
from nextnanopy import defaults

from nextnanopy.utils.formatting import generate_command


def command(
    inputfile,
    exe,
    license,
    database,
    outputdirectory,
    **opt_kwargs,
):
    kwargs = dict(
        inputfile=inputfile,
        exe=exe,
        license=license,
        database=database,
        outputdirectory=outputdirectory,
    )
    kwargs.update(opt_kwargs)
    product = defaults.input_file_type(inputfile)
    cmd = defaults.get_command(product)

    # warn if output path might be too long
    outdir_len = len(str(outputdirectory))
    tooLongPath = (product in ["nextnano3", "nextnano++"]) and outdir_len + 80 > 260  # TODO: how long is the minimal path appended by nn3/nnp simulations?
    tooLongPathNEGF = (product in ["nextnano.NEGF", "nextnano.NEGF_classic"]) and outdir_len + 80 > 260
    if tooLongPath or tooLongPathNEGF:
        warnings.warn(
            "The output path might be too long on Windows 10 (maximum 260 characters). Consider abbreviating your input file name and/or sweep variables..."
        )
    return cmd(**kwargs)


def send(cmd, cwd=os.getcwd()):
    PIPE = subprocess.PIPE
    return subprocess.Popen(
        cmd, stdout=PIPE, stderr=PIPE, close_fds=True, shell=True, cwd=cwd
    )


def read_output(pipe, funcs):
    for line in iter(pipe.readline, b""):
        for func in funcs:
            func(line)
    pipe.close()


def write_output(get, filepath, show=True):
    with open(filepath, "w", newline="") as f:
        for line in iter(get, None):
            line = str(line, "utf-8", errors="ignore")
            if show:
                sys.stdout.write(line)
            f.write(line)


def start_log(process, filepath, show=True, parallel=False):
    q = queue.Queue()
    out, err = [], []
    tout = threading.Thread(
        target=read_output, args=(process.stdout, [q.put, out.append])
    )
    terr = threading.Thread(
        target=read_output, args=(process.stderr, [q.put, err.append])
    )
    twrite = threading.Thread(target=write_output, args=(q.get, filepath, show))
    for t in (tout, terr, twrite):
        t.daemon = False
        t.start()

    if parallel:
        return q, tout, terr
    else:
        process.wait()
        for t in (tout, terr):
            t.join()
        q.put(None)
        return q, tout, terr


def execute(
    inputfile,
    exe,
    license,
    database,
    outputdirectory,
    show_log=True,
    parallel=False,
    **kwargs,
):
    filename = get_filename(inputfile, ext=False)
    inputfile = Path(inputfile).resolve()
    outputdirectory = Path(outputdirectory) / filename
    mkdir_if_not_exist(outputdirectory)
    logfile = outputdirectory / f"{filename}.log"
    cmd = command(inputfile, exe, license, database, outputdirectory, **kwargs)
    cwd = os.getcwd()
    exe = Path(exe)
    wdir, executable = exe.parent, exe.name  # nn3 assumes wdir at one folder upper than the executable

    # validate configuration of executable path
    if executable == "":
        raise FileNotFoundError(f"Executable path is empty! Check nextnanopy.config")

    if (not exe.is_file()) or (not wdir.is_dir()):
        raise FileNotFoundError(
            f"Executable path is invalid: {exe}\nCheck nextnanopy.config"
        )
    process = send(cmd, cwd=wdir)
    queue, tout, terr = start_log(process, logfile, show_log, parallel=parallel)
    os.chdir(cwd)
    info = {
        "process": process,
        "outputdirectory": outputdirectory,
        "filename": filename,
        "logfile": logfile,
        "cmd": cmd,
        "wdir": wdir,
        "queue": queue,
        "tout": tout,
        "terr": terr,
    }
    return info


def run_script(script, kwargs=None, show_log=True):
    """
    The function runs a python script with given arguments. Output is stored in the file script_name.log
    Parameters
    ----------
    script: str
        path to the python script
    kwargs: dict
        optional parameters, {keyword:argument,keyword:argument}
        for a keyword without argument leave argument as an empty string ''

        EXAMPLE: {'-o': 'my_output_folder','-p':''} will be converted to '-o my_output_folder -p'
    show_log - bool
        show the log in console output, default is True

    Returns
    -------
    process: subprocess.POPEN
    """
    args = [[sys.executable, script]]
    if kwargs:
        for key in kwargs.keys():
            args.append([key, kwargs[key]])
    cmd = generate_command(args)
    process = send(cmd)
    logfile = Path.cwd() / f"{Path(script).name}.log"
    start_log(process, logfile, show_log)
    return process
