__author__ = 'Markus Rosenstihl <markus.rosenstihl@physik.tu-darmstadt.de>'

import re
import serial
import time
import logging
import os
logfile = os.path.expanduser("~/.goniometer.log")
logging.basicConfig(filename=logfile,level=logging.INFO)
logger = logging.getLogger()
logger.name = "goniometer"

class goniometer:
    """
    This class is meant to control the goniometer probe head from Marco Braun (AXYs)
    """
    def __init__(self, dev="/dev/ttyUSB0",
                 baudrate=38400,
                 timeout=1,
                 return_to_origin=True,
                 loglevel=2,
                 logangle=False):
        """
        Upon creating an instance the goniometer will be initialised and the current angle defined as being 0.
        The goniometer log can be found in `~/.goniometer.log`.

        :param return_to_origin: Set to true if you want the stepper to return to the beginning when
        the class instance is deleted (for example with "del" ). Default: True
        :type return_to_origin: bool
        :param loglevel: This sets the log level 0=DEBUG,1=INFO,2=WARNING. Default: 2
        :type loglevel: int
        :param logangle: Should a history of the angles be kept? The log can be accessed in the `_angle_history`
          variable. Default: False
        :type logangle: bool
        """
        self.serial = serial.Serial(port=dev, baudrate=baudrate, timeout=timeout)
        logger.setLevel([logging.DEBUG,logging.INFO,logging.WARNING][loglevel])
        self.serial.readlines() # empty serial buffer
        self.current_angle = 0.0
        self._angle_history = []
        self.initialise()
        self.return_to_origin = return_to_origin
        self.logangel = logangle

    def __del__(self):
        if self.return_to_origin:
            logging.info("Return to origin")
            self._send("g1")
            self._wait()
        self.serial.close()

    def _send(self, string_to_send):
        formatted_string = "%s>\r\n"%string_to_send
        logging.debug("send: %s"%(repr(formatted_string)))
        self.serial.write(formatted_string)

    def _recv(self):
        retstr = self.serial.readline().strip().replace("\x00","")
        search_result = re.search(r"<(\d+)>", retstr)
        if search_result != None:
            angle = float(search_result.group(1))/36.0
            self.current_angle = angle
            if self.logangle:
                self._angle_history.append(angle)
            logging.debug("%.2f"%angle)
            return angle
        else:
            return None

    def _wait(self):
        logging.debug("Wait until rotation finnished (current: %.2f)"%(self.current_angle))
        time.sleep(4)
        while True:
            retstr =  self._recv()
            if not retstr:
                logging.debug("Rotation finnished (current: %.2f)"%(self.current_angle))
                break

    def initialise(self):
        logging.info("Initialize goniometer")
        self._send("R000")
        self.current_angle = 0.0

    def stop(self):
        """
        Stops the current movement, not possible while doing :func:`step` and :func:`angle`
        """
        logging.info("Stop goniometer")
        self._send("S456")

    def step(self, sample_rotation_degree=1.0):
        """
        How many degrees should we step further. Negative values are not allowed, the stepper would return to 0.
        A ValueError exception is raised in this case.
        :param sample_rotation_degree: How many degrees to rotate. Default: 1
        :type sample_rotation_degree: float
        """
        if sample_rotation_degree <= 0:
            raise ValueError("This does not what you expect, would move to the start position whatever the value")
        logging.info("Step goniometer: %.2f"%sample_rotation_degree)
        direction = "g" if (sample_rotation_degree < 0) else "G"
        sample_rotation_degree = -1*sample_rotation_degree if sample_rotation_degree < 0 else sample_rotation_degree
        steps = int(sample_rotation_degree%360)
        self._send("%s%i"%(direction, steps ))
        self._wait()

    def angle(self, angle_degree):
        """
        To what angle, in degrees, should we rotate. Mod 360 is calculated for `angle_degree`.
        If angle is smaller than current angle an exception will be raised.
        (From  the underlying :func:`step` function)

        TODO: rotate either back or over to the wanted position.

        :param angle_degree: position in degree
        :type angle_degree: float
        """
        angle_degree %= 360
        delta_degree = angle_degree - self.current_angle
        logging.info("Rotate tp %.2f (from %.2f, i.e. delta=%.2f)"%(angle_degree, self.current_angle, delta_degree))
        self.step(sample_rotation_degree=delta_degree)




