from myfab.log import info, warn, error
from myfab.model import (
    PrinterQueue,
    PrintRequest,
    QueueElement,
    Queue,
    Printer,
    PrinterQueue,
    Event,
    User
)
from typing import List
from multiprocessing import Lock
import time
import requests as r
import json
import myfab.log as log
import peewee
import traceback
import os
import datetime

QUEUE_ELEMENT_STATE_INQUEUE = 0
QUEUE_ELEMENT_STATE_DONE = 1
QUEUE_ELEMENT_STATE_REMOVED = 2

FILE_SAVE_PATH = "/data" # ! fixme doubled

PRINTERS = None
printers_pulled = False

DEFAULT_PRINTER_STATE = {
    "temperatures": {"bed": "??", "tip": "??"},
    "job": None,
    "state": {
        "text": "Unknown",
        "flags": {
            "operational": False,
            "paused": False,
            "printing": False,
            "cancelling": False,
            "pausing": False,
            "sdReady": False,
            "error": False,
            "ready": False,
            "closedOrError": False,
        },
    },
}

# Possibly borked
def add_request_event(req: PrintRequest, desc: str, op: User = None):
    log.info("Req %s: %s" % (req.id, desc))
    req.last_modification = datetime.datetime.now()
    req.save()
    Event.create(request=req, description=desc, operator=op)


# region QUEUE UTILS


"""
    Get queues in the DB
"""


def get_queues():
    try:
        return Queue.select()
    except:
        return []


def get_printers_for_queue(q: Queue):
    printers = []
    for pq in PrinterQueue.select().where(PrinterQueue.queue == q):
        printers.append(pq.printer)

    return printers


"""
    Remove queue
    TODO: what append to the items in the queue ?
"""


def remove_queue(qid):
    if not get_queue_by_id(qid):
        return
    log.info(f'Deleting queue with id {qid}')
    PrintRequest.update(queue_element = None).where(PrintRequest.queue_element == qid).execute()
    QueueElement.delete().where(QueueElement.queue == qid).execute()
    PrinterQueue.delete().where(PrinterQueue.queue == qid).execute()
    Queue.delete().where(Queue.id == qid).execute()


def create_queue(name, weight, meta):
    Queue.create(name=name, weight=weight, meta=meta)

def get_queue_elements_list(qid):
    elems = []
    for ql in PrintRequest.select().join(QueueElement, on=(PrintRequest.queue_element == QueueElement.id)).where(QueueElement.queue == qid, QueueElement.state == QUEUE_ELEMENT_STATE_INQUEUE).order_by(QueueElement.id):
       elems.append({
            "id": ql.id,
            "author": ql.author.fullname,
            "username": ql.author.username,
            "title": ql.title
        })
    return elems

"""
  Add request to the end of the queue
"""


def enqueue(req: PrintRequest, queue: Queue):
    elem: QueueElement = QueueElement.create(queue=queue)
    req.queue_element = elem
    req.save()
    add_request_event(req, "Enqueued request", None)
    return elem


"""
  Peek the immediate next queue item
"""


def get_next_request(queue):
    try:
        return PrintRequest.select().join(QueueElement, on=(PrintRequest.queue_element == QueueElement.id)).where(QueueElement.queue == queue, QueueElement.state == QUEUE_ELEMENT_STATE_INQUEUE).order_by(QueueElement.id).limit(1).get()
        # qe = QueueElement.select().where(QueueElement.state == QUEUE_ELEMENT_STATE_INQUEUE).where(QueueElement.queue == queue).order_by(QueueElement.id).limit(1).get()
        # if qe == None:
        #     return None
        # return PrintRequest.select().where(PrintRequest.queue_element == qe).get()
    except:
        traceback.print_exc()
        return None


def rm_queue_top(queue, reason=QUEUE_ELEMENT_STATE_REMOVED):
    e: PrintRequest = get_next_request(queue)
    if e != None:
        e.queue_element.state = reason
        e.queue_element.save()


"""
  Number of elements before queue_element.
  0 means queue_element is the current top of the queue and next dequeued object
"""


def get_queue_element_pos(queue_element: QueueElement):
    return (
        QueueElement.select()
        .where(
            QueueElement.queue == queue_element.queue
            and QueueElement.state == QUEUE_ELEMENT_STATE_INQUEUE,
            QueueElement.id < queue_element.id,
        )
        .count()
    )


def get_queue_by_id(qid):
    r = Queue.select().where(Queue.id == qid)
    if not r.count():
        return None
    return r.get()


# endregion

# region PRINTER UTILS & class

def get_printer_for_request(req: PrintRequest):
    for p in get_printers():
        if p.current_print != None and p.current_print.id == req.id:
            return p
    return None

def get_printers_from_db():
    printers = []
    for p in Printer.select().execute():
        # try:
        printers.append(build_printer(p))
        # except OctoException:
        #     log.error(f"Cannot build OctoPrinter {p.name}")
    return printers


"""
    Get printer 
"""


def build_printer(p: Printer):
    if p.model == "fake":
        # fake printer
        return FakePrinter(p)
    elif p.model == "octoprint":
        # octo printer
        return OctoPrinter(p)
    else:
        raise Exception("Unknown printer model")


def get_printer_by_id(printer_id):
    printer_id = int(printer_id)
    for p in get_printers():
        if p.id == printer_id:
            return p
    return None


def get_printer_by_name(printer_name):
    for p in get_printers():
        if p.name == printer_name:
            return p
    return None


def pull_printers():
    # Called once to fetch printers from db
    global PRINTERS
    global printers_pulled
    if printers_pulled:
        return
    PRINTERS = get_printers_from_db()
    printers_pulled = True


def register_printer(printer):
    global PRINTERS
    if get_printer_by_id(printer.id) != None:
        raise Exception("Printer already exists")
    PRINTERS.append(build_printer(printer))


def get_printers():
    global PRINTERS
    pull_printers()

    return PRINTERS


def delete_printer(printer_id):
    global PRINTERS
    PRINTERS = list(filter(lambda printer: printer.id() != printer_id, PRINTERS))


class PrinterBase:
    def __init__(self, printer_obj):
        self.printer_obj: Printer = printer_obj
        # self.id = printer_obj.id
        # self.name = printer_obj.name
        self.queues = []
        self.load_queues()

    @property
    def id(self):
        return self.printer_obj.id

    @property
    def name(self):
        return self.printer_obj.name

    @property
    def config(self):
        return json.loads(self.printer_obj.config)

    @property
    def current_print(self):
        return self.printer_obj.current_print

    def load_queues(self):
        self.queues = []
        for q in PrinterQueue.select().where(PrinterQueue.printer == self.printer_obj):
            self.queues.append(q.queue)

    def serialized_queues(self):
        s = []
        for q in self.queues:
            s.append({
                'id': q.id,
                'name': q.name,
                'weight': q.weight,
                'enabled': q.enabled
            })
        return s

    def set_print(self, pr: PrintRequest):
        raise NotImplementedError()

    def printer_type(self):
        raise NotImplementedError()

    def version(self):
        raise NotImplementedError()

    def printer_model(self):
        return self.printer_obj.model

    def refresh(self):
        raise NotImplementedError()

    def status(self):
        raise NotImplementedError()


# Printer not linked to Octoprint but can represent a printer
"""
Deprecated
"""


class FakePrinter(PrinterBase):
    def __init__(self, printer_obj):
        super().__init__(printer_obj)

    def refresh(self):
        pass

    def status(self):
        return "printing" if self.current_print != None else "idle"

    def infos(self):
        return {}

    def print_progress(self):
        return -1

    def remaining_print_time(self):
        return -1

    def printer_type(self):
        return "fake"

    def version(self):
        return "N/A"

class OctoException(Exception):
    pass

RESET_SUCCESS = 0
RESET_CANCEL  = 1
RESET_ERROR   = 2
RESET_FAILURE = 3
RESET_GENERIC = 4

# Printer with Octoprint backend
class OctoPrinter(PrinterBase):
    def __init__(self, printer_obj):
        super().__init__(printer_obj)
        self.cancel_flag = False
        self.printer_lock = Lock()
        self.ready_for_printing = False  # can accept new print
        self.clear_plate_flag = True
        self.printer_state = DEFAULT_PRINTER_STATE
        try:
            self.reload_printer_connection_settings()
            # self.reset_current_print(RESET_GENERIC) #NONONONO
        except OctoException:
            self.log(f"Cannot connect to octoprint")

    @property 
    def enabled(self):
        return self.printer_obj.enabled

    @enabled.setter
    def enabled(self, value):
        self.printer_obj.enabled = value
        self.printer_obj.save()

    def log(self, msg, back=log.info):
        back(f"Printer #{self.id} ({self.name}) @ {self.hostname}: {msg}")

    def toggle(self):
        self.enabled = not self.enabled
        self.log(f"Enabled set to {self.enabled}")
        self._refresh_ready_state()

    def reload_printer_settings(self):
        self.printer_obj = type(self.printer_obj).get(self.printer_obj._pk_expr())
        self.log(f"Reloaded base object")
        self.reload_printer_connection_settings()

    def _get_next_print(self):
        self.queues.sort(key=lambda x: x.weight, reverse=True)
        for queue in self.queues:
            req = get_next_request(queue)
            if req is not None:
                return req, queue
        return None, None

    def set_plate_clear_flag(self, value):
        if not value:
            self.log("/!\\ PLATE IS MARKED AS CLEAR /!\\")
        self.clear_plate_flag = bool(value)
        self._refresh_ready_state()

    """
    Note: this is the only function that changes the print while keeping the queues order consistent.
    Direct call to start_print(PrintRequest) will not update the queues
    """

    def cycle_print(self):
        self.log("Cycling print")
        self._refresh_printer_status()
        self._refresh_ready_state()
        if not self.ready_state:
            return
        np, queue = self._get_next_print()
        if np is None:
            self.log(f"Nothing to print")
            return
        self.start_print(np)
        # After the print is started and set as the current print, the request is removed from queue
        rm_queue_top(queue)

    def reload_printer_connection_settings(self):
        config = json.loads(self.printer_obj.config)
        """
            {
            'hostname': 'hostname or ip',
            'port': port
            'key': the api-key,
            }
        """
        self._version = "Unknown"
        self.hostname = config['hostname']
        self.port = config['port']
        self.key = config['key']
        # self.name = config['name']
        self.log(f"Reloaded connection settings ({self.hostname})")
        self.test_and_pull_printer_global_info()

    def cancel_print(self):
        self.cancel_flag = True
        if not self.printer_state['state']['flags']['printing']:
            self.log(f"Nothing to cancel")
            return
        self.log(f"Cancelling print")
        self._make_octoprint_raw_request(
            "/api/job", method="post", data={"command": "cancel"}
        )  # First cancel octoprint job
        while self.printer_state['state']['flags']['printing']:
            self._refresh_printer_status()
            time.sleep(.3)
        self._make_octoprint_raw_request(
            "/api/printer/printhead",
            data={"command": "jog", "z": 15, "absolute": False},
        )
        self._make_octoprint_raw_request(
            "/api/printer/printhead",
            data={"command": "jog", "y": 100, "absolute": False},
        )  # move nozzle up and bed forward
        add_request_event(self.printer_obj,"Canceled print", None)
        self.reset_current_print(RESET_CANCEL)

    def toggle_pause(self):
        if self.current_print == None:
            self.log(f"Nothing to pause")
            return
        self.log(f"Toggling print pause")
        self._make_octoprint_raw_request(
            "/api/job", method="post", data={"command": "toggle"}
        )  # First cancel octoprint job

    """
    Check printer status and if it ready for printing
    then sends the gcode to the printer and starts printing
    """

    def start_print(self, print_obj: PrintRequest):
        self._refresh_printer_status()
        self._refresh_ready_state()
        if not self.ready_state:
            raise Exception("Printer is not ready")
        self.set_plate_clear_flag(True)
        
        gcode_path = os.path.join(FILE_SAVE_PATH, print_obj.gcode)
        i = 0
        while True:
            try:            
                with open(gcode_path) as fh:   
                    self.log(f"[+] Starting print #{print_obj.id} ({print_obj.title})")
                    self._refresh_ready_state()

                    self.log(f"[+] Deleting file on printer...")
                    self._make_octoprint_raw_request(
                        "/api/files/local/"+print_obj.gcode,
                        method="delete",
                        silent=True
                    )

                    self.log(f"[+] Uploading gcode...")
                    self._make_octoprint_raw_request(
                        "/api/files/local",
                        method="post",
                        data=None,
                        files={"file": fh}
                    )

                    self.log("[+] Initiating print...")
                    self._make_octoprint_raw_request(
                        "/api/files/local/"+print_obj.gcode,
                        method="post",
                        data={"command": "select", "print": True}
                    )
                    break
            except:
                traceback.print_exc()
                i+=1
                if i == 3:
                    self.log("[-] Cannot start print")
                    raise Exception("Cannot start Print")
                

        self.printer_obj.current_print = print_obj
        self.printer_obj.save()
    
        print_obj.status = 3 # REQUEST_STATUS_PRINTING
        print_obj.save()
        add_request_event(print_obj, "Started Printing", None)
        self.log(f"[+] Print started")

    @property
    def accept_control(self):
        return self.printer_state['state']['flags']['ready'] and self.printer_state['state']['flags']['operational'] and not self.printer_state['state']['flags']['printing']

    """
    Can accept new print 
    """

    @property
    def ready_state(self):
        return self.ready_for_printing

    def _refresh_ready_state(self):
        self.ready_for_printing = (
            self.printer_state["state"]["flags"]["ready"]
            and self.printer_state["state"]["flags"]["operational"]
            and self.current_print == None
            and self.enabled
            and not self.clear_plate_flag
        )
        self.log(f"Ready state is {self.ready_for_printing}")

    """
    Object was removed from printing bed, printer is ready to operate again (unless octoprint says otherwise)
    """

    def reset_current_print(self, reason):
        self.cancel_flag = False
        if(self.current_print == None):
            return
        if reason == RESET_CANCEL or reason == RESET_ERROR or reason == RESET_FAILURE:
            self.printer_obj.current_print.status = 11 # REQUEST_STATUS_PRINT_FAILED
        if reason == RESET_SUCCESS:
            # add_request_event(self.printer_obj.current_print, "Normal Print Ending", None)
            self.printer_obj.current_print.status = 7 # REQUEST_STATUS_INSPECTING
        self.printer_obj.current_print.save()
        # TODO v3 : Printer statistics
        # save printer in request
        self.printer_obj.current_print = None
        self.printer_obj.save()
        # self._refresh_printer_status()
        # self._refresh_ready_state()

    def _make_octoprint_raw_request(self, path, method="get", data=None, files=None, silent=False):
        try:
            headers = {"X-Api-Key": self.key}
            if data != None:
                headers['Content-Type'] = "application/json"
                method = "post"

            if type(data) != type("") and data != None:
                data = json.dumps(data)
            resp = r.request(
                    method,
                    f"http://{self.hostname}:{self.port}{path}",
                    headers=headers,
                    data=data,
                    files=files,
                )
            if resp.status_code >= 400 and not silent:
                self.log(resp.text, back=log.warn)
            resp = resp.text
            return json.loads(resp) if resp != "" and resp != None else {}
        except:
            if silent:
                return
            self.printer_state = DEFAULT_PRINTER_STATE
            self.printer_state['state']['text'] = "No Connection"
            self.log("Error while making request", back=log.error)
            # traceback.print_exc()
            raise OctoException("Error while making octoprint request")

    """
    Home 3 axis
    """

    def goto_home(self):
        if not self.accept_control:
            raise Exception("Printer is not ready")
        self.log("Homing 3 axis")
        self._make_octoprint_raw_request(
            "/api/printer/printhead",
            method="post",
            data=json.dumps({"axes": ["x", "y", "z"], "command": "home"}),
        )

    def _refresh_printer_status(self):
        self.log(f"Refreshing status")
        if self.version == "Unknown":
            self.test_and_pull_printer_global_info()
        status = self._make_octoprint_raw_request("/api/printer")
        self.printer_state = {
            "temperatures": {
                "bed": status["temperature"]["bed"]["actual"],
                "tip": status["temperature"]["tool0"]["actual"],
            },
            "state": status["state"],
            "job": None
        }
        self.log(f"Status is \"{status['state']['text']}\"")

        if status["state"]["flags"]["error"]:
            if self.current_print != None:
                self.log("Printer Error")
                self.reset_current_print(RESET_ERROR)
            return

        if not status["state"]["flags"]["printing"]:
            if self.current_print != None:
                # We just finished a print, check the reason;
                # set the print status and remove current print
                if not self.cancel_flag:
                    self.log("Normal print end")
                    self.reset_current_print(RESET_SUCCESS)  
            return

        job = self._make_octoprint_raw_request("/api/job")
        self.log(job)
        self.printer_state["job"] = {
            "printTime": job["progress"]["printTime"],
            "printTimeLeft": job["progress"]["printTimeLeft"],
            "progress": job["progress"]["completion"],
            "print_length": job['job']["filament"]["tool0"]["length"] if job['job']['filament'] else -1,
        }

    def test_and_pull_printer_global_info(self):
        versions = self._make_octoprint_raw_request("/api/version")
        self.log(f"Connected to octoprint" )
        self.log(f"Version info: {versions['text']}")
        self._version = versions["text"]

    def print_progress(self):
        if not 'job' in self.printer_state or not self.printer_state['job']:
            return -1
        return self.printer_state["job"]["progress"]

    def remaining_print_time(self):
        if not self.printer_state["job"]:
            return -1
        return self.printer_state["job"]["printTimeLeft"]

    def infos(self):
        return self.printer_state

    def version(self):
        return self._version

    def printer_type(self):
        return "Octoprint"

    def status(self):
        return self.printer_state["state"]["text"]


# endregion


# Octoprint api example

"""

Print:

POST /api/files/local/whistle_v2.gcode HTTP/1.1
Host: example.com
Content-Type: application/json
X-Api-Key: abcdef...

{
  "command": "select",
  "print": true
}


Upload & Print

POST /api/files/sdcard HTTP/1.1
Host: example.com
X-Api-Key: abcdef...
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryDeC2E3iWbTv1PwMC

------WebKitFormBoundaryDeC2E3iWbTv1PwMC
Content-Disposition: form-data; name="file"; filename="whistle_v2.gcode"
Content-Type: application/octet-stream

M109 T0 S220.000000
T0
G21
G90
...
------WebKitFormBoundaryDeC2E3iWbTv1PwMC
Content-Disposition: form-data; name="select"

true
------WebKitFormBoundaryDeC2E3iWbTv1PwMC
Content-Disposition: form-data; name="print"

true
------WebKitFormBoundaryDeC2E3iWbTv1PwMC--


Currently printing

GET /api/job HTTP/1.1
Host: example.com
X-Api-Key: abcdef...


HTTP/1.1 200 OK
Content-Type: application/json

{
  "job": {
    "file": {
      "name": "whistle_v2.gcode",
      "origin": "local",
      "size": 1468987,
      "date": 1378847754
    },
    "estimatedPrintTime": 8811, # in seconds
    "filament": {
      "length": 810,
      "volume": 5.36
    }
  },
  "progress": {
    "completion": 0.2298468264184775,
    "filepos": 337942,
    "printTime": 276,
    "printTimeLeft": 912
  },
  "state": "Printing" ### can be  “Operational”, “Printing”, “Pausing”, “Paused”, “Cancelling”, “Error” or “Offline”.
}

"""
