import json
import os
import shutil
import subprocess
import sys
import threading
import traceback



##############################################################################################################
#
# NOTE !!!
#
# THIS FILE MAY BE AUTOGENERATED.
# ONLY MODIFY THE ORIGINAL FILE IN THE /sharedData/ FOLDER!
# OTHER INSTANCES OF THIS FILE ARE COPIES!
#
# NOTE !!!
#
##############################################################################################################


def debugging_message(source, message):
    """
    A debugging function.
    takes a message from a given source, identified by a string, and prints it.
    This function is supposed to be modified as needed to make debugging easier, and serves no purpose beyond that.
    """
    show = True
    if show:
        msg = "%s: %s: %s" % (threading.get_ident(), source, message,)
        print(msg, flush=True)


def get_error_message_details(exception=None):
    """
    get a nicely formatted string for an error message collected with sys.exc_info().
    """
    if exception is None:
        exception = sys.exc_info()
    exc_type, exc_obj, exc_trace = exception
    trace = traceback.extract_tb(exc_trace)
    error_msg = "Traceback is:\n"
    for (file,linenumber,affected,line) in trace:
        error_msg += "\t> Error at function %s\n" % (affected)
        error_msg += "\t  At: %s:%s\n" % (file,linenumber)
        error_msg += "\t  Source: %s\n" % (line)
    error_msg += "%s\n%s" % (exc_type, exc_obj,)
    return error_msg


def execute_terminal_command(cmd, get_output=False, ignore_errors=False, in_folder=None):
    """
    executes a terminal/console command.
    Returns the console output if get_output=True, else returns a result object of subprocess.run()
    Raises an error if something goes wrong.
    # TODO later:
    use a single method instead of using different ones depending on the argument?
    This method is intended as a helper function, and to make it easier later to reroute Docker commands into virtual machines instead of executing them directly.
    """
    with cd(in_folder):
        error_message = None
        if get_output:
            try:
                res = subprocess.check_output(cmd)
                res = res.decode("utf-8")
            except subprocess.CalledProcessError as e:
                error_message = "failed to execute terminal/console command with output:\n%s\n%s" % (cmd, vars(e))
        else:
            res = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
            if res.returncode != 0:
                error_message = "failed to execute terminal/console command:\n%s\n%s" % (cmd, vars(res))
        if error_message is not None and not ignore_errors:
            raise Exception(error_message)
        return res


class cd:
    """
    Context manager for changing the current working directory
    """
    def __init__(self, newPath):
        self.newPath = None if newPath is None else os.path.expanduser(newPath)

    def __enter__(self):
        self.savedPath = os.getcwd()
        if self.newPath is not None:
            os.chdir(self.newPath)

    def __exit__(self, etype, value, traceback):
        if self.newPath is not None:
            os.chdir(self.savedPath)


def delete_file_or_folder(path):
    """
    deletes a file or a folder if it exists.
    Doesn't raise an Error if that file or folder does not exist.
    """
    if os.path.exists(path):
        if os.path.isfile(path):
            os.remove(path)
        else:
            shutil.rmtree(path)


def copy_and_overwrite(from_path, to_path):
    """
    copy and overwrite a file or a directory structure.
    """
    if os.path.exists(to_path):
        if os.path.isfile(to_path):
            os.remove(to_path)
        else:
            shutil.rmtree(to_path)
    if os.path.isfile(from_path):
        shutil.copy(from_path, to_path)
    else:
        shutil.copytree(from_path, to_path)


def zipdir(path, zip_handle, includeEmptyDIr=True):
    """
    zip a directory
    """
    def get_relative_path(root, file):
        return os.path.relpath(os.path.join(root, file), os.path.join(path, '..'))
    empty_dirs = []
    for root, dirs, files in os.walk(path):
        empty_dirs.extend([dir for dir in dirs if os.listdir(os.path.join(root, dir)) == []])
        for file in files:
            zip_handle.write(os.path.join(root, file), get_relative_path(root, file))
        if includeEmptyDIr:
            for dir in empty_dirs:
                zif = zipfile.ZipInfo(get_relative_path(root, dir) + "/")
                zip_handle.writestr(zif, "")
        empty_dirs = []


def smart_truncate(content, length=100, suffix='...'):
    """
    truncate a text so that the last word is removed, and it doesn't cut out in the middle of a word.
    """
    if len(content) <= length:
        return content
    else:
        return ' '.join(content[:length+1].split(' ')[0:-1]) + suffix
