import logging
import os
import shutil
import threading
import glob
import time
from copy import deepcopy
from functools import partial
from typing import List, Dict, Optional
import simplejson as json

# logging
logger = logging.getLogger("3d-reconstruction-service")
logger.setLevel(logging.DEBUG)
# create file handler which logs even debug messages
fh = logging.FileHandler('web_service.log')
fh.setLevel(logging.DEBUG)
# log errors to stdout
ch = logging.StreamHandler()
ch.setLevel(logging.ERROR)
# create formatter and add it to the handlers
formatter = ColorFormatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
fh.setFormatter(formatter)
# add the handlers to logger
logger.addHandler(ch)
logger.addHandler(fh)


class StoppingThread(threading.Thread):

    def __init__(self, name, shared_data):
        """ constructor, setting initial variables """
        self._stop_event = threading.Event()
        self._sleep_period = 1.0
        self.shared_data = shared_data

        threading.Thread.__init__(self, name=name)

    def run(self):
        """ Example main control loop """
        count = 0
        while not self._stop_event.isSet():
            count += 1
            self._stop_event.wait(self._sleep_period)

    def join(self, timeout=None):
        """ Stop the thread. """
        self._stop_event.set()
        print("set stop event")
        threading.Thread.join(self, timeout)


def clean_up_folder(path_to_dir: str):
    files = glob.glob(os.path.join(path_to_dir, "*"))
    for f in files:
        if os.path.isdir(f):
            shutil.rmtree(f)
        else:
            os.remove(f)


def create_folder(path_to_dir: str):
    if os.path.isdir(path_to_dir) or os.path.isfile(path_to_dir):
        pass
    else:
        os.mkdir(path_to_dir)


# First thread
class ComputationThread(StoppingThread):
    def run(self):
        logger.debug("Starting computation thread...")

        # This thread will run the computation using a subprocess call (assuming we want to run an executable)
        # Alternatively, one could run a Python service in the background.

        logger.info("Running computation...")

        # run SfM
        computation_service = ComputationService(
            path_to_executable_root=EXECUTABLE_DIR,
            path_to_input_dir=INPUT_DIR,
            path_to_output_dir=OUTPUT_DIR,
            path_to_cache_dir=CACHE_DIR, )
        # If process returns exit code 0, it has completed successfully

        process = computation_service.start()
        logger.info("After start!!!!!")

        # Get stuck in this loop until stopevent flag is set, otherwise reach end of execution and terminate thread
        while not self._stop_event.isSet():
            self._stop_event.wait(self._sleep_period)

        #######################################
        # Code for parsing STDOUT of process and logging it
        #
        # output = ""
        # for line in iter(process.stdout.readline, ""):
        #     logger.info(line)
        #     output += str(line)
        #
        # process.wait()
        # exit_code = process.returncode
        #
        # if exit_code == 0:
        #     return output
        # else:
        #     raise Exception(command, exit_code, output)
        #
        ######################################

        # If execution gets here before computation is finished, it kills the process

        # TODO: This works but should not kill process like that as it might kill other sessions on the server,
        #  unless dockerization is used for every client connection instance

        computation_service.kill()

        logger.info("Exiting computation thread...")


# Second thread
class LogParserThread(StoppingThread):
    def run(self):
        # this thread will do the log parsing, and conversion into an appropriate metadata format
        logger.info("Starting log parsing...")

        # while the stopflag is not set, keep parsing the logs generated by the computation process
        while not self._stop_event.isSet():
            time.sleep(1.0)
            try:
                print("parsing...........")
                shared_data.logs = batch_parse_logs(
                    path_to_cache_dir=CACHE_DIR)
            except FileNotFoundError as err:
                logger.error(msg=err)
                continue
            except Exception as err:
                logger.error(msg=err)
                break

            self._stop_event.wait(self._sleep_period)

        logger.info("Exiting log parsing...")


# Third thread
class UpdatesThread(StoppingThread):
    def run(self):

        logger.info("Starting update thread...")

        # while the stopflag is not set, keep posting updates to server
        while not self._stop_event.isSet():
            try:
                if shared_data.logs:
                    for step in shared_data.logs:
                        logger.info("Sending mock POST request with data: {}".format(step))
                        # TODO: Need to create a REST service that sends updates to the server

            except Exception as err:
                logger.error(msg=err)
                break

            self._stop_event.wait(self._sleep_period)

        logger.info("Stopping update thread...")


# Shared data container
class SharedData:
    def __init__(self, logs: Optional[List[ComputationStep]]):
        self._logs = logs

    @property
    def logs(self):
        return self._logs

    @logs.setter
    def logs(self, logs: List[ComputationStep]):
        self._logs = logs


# Shared data between threads
shared_data = SharedData(None)
# Share threads among routes
threads = []

computation_thread = ComputationThread(name="computation_thread", shared_data=shared_data)
# rest and log-parsing threads run infinitely and does not exit on its own, so it should be run in a daemonic thread
post_updates_thread = UpdatesThread(name="post_updates_thread", shared_data=shared_data)
log_parser_thread = LogParserThread(name="log_parsing_thread", shared_data=shared_data)


def construct_metadata_json(items: list):
    d = {
        "property_1": 0.0,
        "property_2": 0.0,
        "property_3": 0.0
    }

    for item in items:
        b = {s: str(getattr(item, s, None)) for s in dir(item) if isinstance(getattr(type(item), s, None), property)}
        b["id"] = item.__class__.__name__
        d["children"].append(b)

    return d


def start():
    computation_thread = ComputationThread(name="computation_thread", shared_data=shared_data)
    # rest and log-parsing threads run infinitely and does not exit on its own, so it should be run in a daemonic thread
    post_updates_thread = UpdatesThread(name="post_updates_thread", shared_data=shared_data)
    logparser_thread = LogParserThread(name="log_parsing_thread", shared_data=shared_data)
    logparser_thread.start()
    threads.append(logparser_thread)
    computation_thread.start()
    threads.append(computation_thread)
    post_updates_thread.start()
    threads.append(post_updates_thread)
    message = "Started StructureFromMotion module..."
    print(message)

    return message


def stop():
    if len(threads) == 3:
        threads[2].join()
        threads[1].join()
        threads[0].join()

        # TODO: Clean all files after module is stopped
        return "Stopped StructureFromMotion module..."

    else:
        return "No running instance of StructureFromMotion module..."


def send_something_to_host_server(ws, ws_client):
    d = format_status_json()
    message = {'status': d["percentageOverallProgress"]}
    print(message)
    ws_client.send_message(ws, json.dumps(message))


def clean_up_data_model(data: Dict) -> Dict:
    data_modified = deepcopy(data)
    data_modified.pop("id", None)
    data_modified.pop("type", None)
    return data_modified


def format_status_json():
    if shared_data.logs:
        status = construct_metadata_json(shared_data.logs)
    else:
        status = {}
        status["percentageOverallProgress"] = 0.0
        status["outputFiles"] = None
    return status


def on_message(ws, message: str, host: str, port: str):
    d = json.loads(message)
    if d["message"] == "start":
        # get the files from the server
        print("Downloading files from the server ({}) to {}...".format(host, IMAGES_DIR))
        get_files(host=host, httpPort=port,
                  path_to_dir=FILES_DIR)  # TODO: implement utility function to download files via HTTP from server
        print("Starting process...")
        start()
    elif d["message"] == "stop":
        print("Stopping SfM...")

        stop()


def wrap_send_files(route: str, output_files: List[str]) -> bool:
    try:
        send_files(route, output_files)
    except:
        pass
    else:
        return True


def is_output_files_valid(output_files: List[str]) -> bool:
    is_valid = True  # TODO: write utility function to check validity of files

    return is_valid


def noop_fn(*args, **kwargs):
    pass


def main(host: str, endpoint_type: str, port: str):
    # init websocket client
    wsClient = ws_client.getClient("ws://" + host + ":3001/" + endpoint_type)
    wsClient.on_message = partial(on_message, host=host, port=3000)
    wst = threading.Thread(target=wsClient.run_forever)
    wst.daemon = True
    wst.start()

    running = True
    is_sent_files = False
    time.sleep(1)
    while (running):
        time.sleep(2)
        send_something_to_host_server(wsClient)

        outputFiles = format_status_json()["outputFiles"]

        if is_output_files_valid(outputFiles):
            if is_sent_files:
                pass
            else:
                print(outputFiles)
                url = "http://" + str(host) + ":3000/cache_files"
                print("Uploading files to {}...".format(url))
                is_sent_files = wrap_send_files(url, outputFiles)
                print("Uploaded files to {}...".format(url))


if __name__ == '__main__':
    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument('--host', required=True,
                        help="Host on which the roboweldar server is running")
    parser.add_argument('--port', required=True,
                        help="Port on which the Orion Context Broker is running")

    args = parser.parse_args()
    main(host=args.host, endpoint_type="endpoint", port=args.port)
