# -*- coding: utf-8; -*-
#
# Copyright (c) 2016 Álan Crístoffer
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

import datetime

import numpy as np
import math
import threading
import time

import sys
import traceback

from moirai.database import DatabaseV1
from moirai.hardware.configured_hardware import ConfiguredHardware
from moirai.hardware.timer import Timer, Finished


class Controller(object):
    def __init__(self, controller_id):
        self.db = DatabaseV1()
        cs = self.db.get_setting("controllers")
        self.cs = next((c for c in cs if c["id"] == controller_id), None)
        if self.cs is None:
            raise Exception("Controller not found")
        self.hardware = ConfiguredHardware()
        configuration = self.db.get_setting("hardware_configuration")
        self.locks = [self.interlock(lo) for lo in configuration["interlocks"]]
        self.off_values = {
            p["alias"]: float(p["defaultValue"])
            for p in configuration["ports"]
            if p["type"] & (8 | 16)
        }
        self.running = True
        self.lock = threading.Lock()
        self.after = None

    def interlock(self, lock):
        try:
            code = compile("y=%s" % lock["expression"], "_string_", "exec")
        except SyntaxError as err:
            error = err.__class__.__name__
            detail = err.args[0]
            line = err.lineno
            scope = "interlock"
            error_string = "%s on %s:%s: %s" % (error, scope, line, detail)
            print(error_string)
            self.db.set_setting("test_error", error_string)
            self.running = False
            return

        def f(code=code, lock=lock):
            try:
                value = self.hardware.read(lock["sensor"])
                scope = {"x": value}
                exec(code, None, scope)
            except Exception as err:
                error = err.__class__.__name__
                detail = err.args[0]
                cl, exc, tb = sys.exc_info()
                tb = self.stringify_tb(traceback.extract_tb(tb))
                error_string = "%s: %s\n%s" % (error, detail, tb)
                print(error_string)
                self.db.set_setting("test_error", error_string)
                raise Exception("Interlock")
            if scope["y"]:
                self.hardware.write(lock["actuator"], lock["actuatorValue"])
                self.db.set_setting("test_error", "Interlock")
                raise Exception("Interlock")

        return f

    def lock_forever(self):
        interval = float(self.cs["tau"])
        while self.running:
            time.sleep(interval)
            self.lock.acquire()
            try:
                for lock in self.locks:
                    lock()
            except:  # noqa: E722 pylint: disable=E722
                try:
                    self.running = False
                    if self.after is not None:
                        ins = self.cs["inputs"]
                        inputs = {s: self.hardware.read(s) for s in ins}
                        plocals = {
                            "inputs": inputs,
                            "outputs": dict(),
                            "np": np,
                            "math": math,
                        }

                        exec(self.after, plocals, plocals)

                        for actuator, value in plocals["outputs"].items():
                            self.hardware.write(actuator, value)
                except Exception as err:
                    error = err.__class__.__name__
                    detail = err.args[0]
                    cl, exc, tb = sys.exc_info()
                    tb = self.stringify_tb(traceback.extract_tb(tb))
                    error_string = "%s: %s\n%s" % (error, detail, tb)
                    print(error_string)
                    self.db.set_setting("test_error", error_string)
            self.lock.release()

    def run(self):
        self.db.set_setting("current_test", self.cs["name"])
        self.db.set_setting("test_error", None)

        after = None
        self.running = True

        try:
            scope = "before"
            before = compile(self.cs["before"], "before", "exec")
            scope = "controller"
            controller = compile(self.cs["controller"], "controller", "exec")
            scope = "after"
            after = compile(self.cs["after"], "after", "exec")
            self.after = after

            thread = threading.Thread(target=self.lock_forever)
            thread.start()
            thread.isDaemon = True

            inputs = {s: self.hardware.read(s) for s in self.cs["inputs"]}
            plocals = {
                "inputs": inputs,
                "outputs": dict(),
                "s": dict(),
                "dt": float(self.cs["tau"]),
                "np": np,
                "math": math,
            }

            for k, v in self.off_values.items():
                self.hardware.write(k, v)

            exec(before, plocals, plocals)

            state = plocals["s"]

            for k, v in plocals["outputs"].items():
                self.hardware.write(k, v)

            run_time = int(self.cs["runTime"])
            interval = float(self.cs["tau"])
            t = Timer(run_time, interval)
            start_time = datetime.datetime.utcnow()
            time = 0
            graph_id = self.db.save_test(self.cs["name"], start_time)

            while self.db.get_setting("current_test") is not None and self.running:
                self.lock.acquire()
                inputs = {s: self.hardware.read(s) for s in self.cs["inputs"]}
                self.lock.release()

                plocals = {
                    "inputs": inputs,
                    "outputs": dict(),
                    "s": state,
                    "log": dict(),
                    "t": time,
                    "dt": interval,
                    "np": np,
                    "math": math,
                }

                exec(controller, plocals, plocals)

                plocals["log"] = {
                    **plocals["log"],
                    **plocals["inputs"],
                    **plocals["outputs"],
                }

                self.lock.acquire()
                for k, v in plocals["outputs"].items():
                    self.hardware.write(k, v)

                for k, v in plocals["log"].items():
                    self.db.save_test_sensor_value(graph_id, k, v, time)
                self.lock.release()

                state = plocals["s"]

                t.sleep()
                time = t.elapsed()
        except Finished:
            pass
        except SyntaxError as err:
            error = err.__class__.__name__
            detail = err.args[0]
            line = err.lineno
            error_string = "%s on %s:%s: %s" % (error, scope, line, detail)
            print(error_string)
            self.db.set_setting("test_error", error_string)
        except Exception as err:
            error = err.__class__.__name__
            detail = err.args[0]
            cl, exc, tb = sys.exc_info()
            tb = self.stringify_tb(traceback.extract_tb(tb))
            error_string = "%s: %s\n%s" % (error, detail, tb)
            print(error_string)
            self.db.set_setting("test_error", error_string)

        try:
            if after is not None:
                inputs = {s: self.hardware.read(s) for s in self.cs["inputs"]}
                plocals = {"inputs": inputs, "outputs": dict(), "np": np, "math": math}

                exec(after, plocals, plocals)

                for actuator, value in plocals["outputs"].items():
                    self.hardware.write(actuator, value)

            for k, v in self.off_values.items():
                self.hardware.write(k, v)
        except Exception as err:
            error = err.__class__.__name__
            detail = err.args[0]
            cl, exc, tb = sys.exc_info()
            tb = self.stringify_tb(traceback.extract_tb(tb))
            error_string = "%s: %s\n%s" % (error, detail, tb)
            print(error_string)
            self.db.set_setting("test_error", error_string)

        self.db.set_setting("current_test", None)
        self.db.close()
        self.running = False

    def stringify_tb(self, tb):
        msg = "Traceback:\n\t"
        msg += "\n\t".join(["%s:%s in %s" % (t.filename, t.lineno, t.name) for t in tb])
        return msg
