from __future__ import (division, absolute_import, print_function,
                       unicode_literals)
from collections import OrderedDict
from math import ceil, floor
import logging
import struct
from time import sleep

logger = logging.getLogger("dstat.simulator")

import serial
import numpy as np

try:
    import gi
    gi.require_version('Gtk', '3.0')
    from gi.repository import Gtk
except ImportError:
    print("ERR: GTK not available")
    sys.exit(1)

def menu_setup(builder):
    """Take Gtk.builder instance, append menu item, and return item."""
    
    parent_menu_name = 'menu_developer'
    menu_label = 'Simulate DStat'
    menu_class = Gtk.CheckMenuItem
    
    menu = menu_class.new_with_label(menu_label)
    menu.set_active(False)
    
    builder.get_object(parent_menu_name).get_submenu().append(menu)
    return menu

class Simulator(object):                    
    def __init__(self):
        self.current_state = self.main
        self.output = b""
        
        self.settings_dict = OrderedDict([('max5443_offset', 0),
                         ('tcs_enabled', 1),
                         ('tcs_clear_threshold', 10000),
                         ('r100_trim', 0),
                         ('r3k_trim', 0),
                         ('r30k_trim', 0),
                         ('r300k_trim', 0),
                         ('r3M_trim', 0),
                         ('r30M_trim', 0),
                         ('r100M_trim', 0)
                          ])            
    
    def _get_input(self, size=1):
        string = self.input_str[0:size]
        self.input_str = self.input_str[size:]
        return string
    
    def _get_params(self, n):
        params = self.input_str.split(None, n)
        if len(params) > n:
            self.input_str = params.pop()
        else:
            self.input_str = b""
        return params
    
    def input(self, string):
        logger.debug("input: {}".format(string))
        self.input_str = string
        while len(self.input_str) > 0:
            self.current_state()
    
    def main(self):
        char = self._get_input()
        
        if char == '!':
            self.current_state = self.command
            self.output += b'C\r\n'
            
    def command(self):
        def restart():
            logger.info("USB restart received")
            return
        def version():
            logger.info("Version Check")
            self.output += b'V1.2\n\r'
            return
        
        command_map = {'E' : self.experiment,
                       'S' : self.settings,
                       'R' : restart,
                       'V' : version}
            
        char = self._get_input()
        
        try:
            command_map[char]()
        except KeyError:
            logger.warning("Unrecognized command {}".format(char))
            self.output += b"#ERR: Command {} not recognized\n\r".format(char)
        
        if self.current_state.__name__ != self.abort_wait.__name__:
            self.output += b"no\n\r"
            self.current_state = self.main
        else:
            logger.info("abort_wait state")
        logger.info("Command {} Finished".format(char))

    def abort_wait(self):
        logger.info("abort_wait() called")
        self.current_state = self.abort_wait
        if self._get_input() == 'a':
            logger.info("Abort signal received")
            self.output += b"no\n\r"
            self.current_state = self.main

    def settings(self):
        def reset():
            logger.info("Settings reset")
            self.settings_dict = OrderedDict([('max5443_offset', 0),
                             ('tcs_enabled', 1),
                             ('tcs_clear_threshold', 10000),
                             ('r100_trim', 0),
                             ('r3k_trim', 0),
                             ('r30k_trim', 0),
                             ('r300k_trim', 0),
                             ('r3M_trim', 0),
                             ('r30M_trim', 0),
                             ('r100M_trim', 0)
                              ])
                              
        def firmware():
            logger.info("Firmware update mode")
        
        def read():
            self.output += "S"
            for key, value in self.settings_dict.items():
                self.output += "{}.{}:".format(key, value)
            self.output = self.output.rstrip(':')    
            self.output += "\n\r"
            
        def write():
            params = self._get_params(10)
            for n, i in enumerate(self.settings_dict):
                i = int(params[n])
        
        settings_map = {'D' : reset,
                        'F' : firmware,
                        'R' : read,
                        'W' : write
                        }
        
        char = self._get_input()
        
        try:
            settings_map[char]()
        except KeyError:
            logger.warning("Settings control {} not found".format(char))
        
    def experiment(self):
        def ads1255():
            params = self._get_params(3)
            logger.info("ADS1255 params: {}".format(params))
            self.output += b"#A: {} {} {}\r\n".format(*params)
            
        def gain():
            params = self._get_params(2)
            logger.info("IV gain : {}".format(params))
            self.output += b"#G: {} {}\r\n".format(*params)
            
        def lsv():
            params = self._get_params(7)
            start = ceil(int(params[-3])*(65536/3000)+32768)
            stop = ceil(int(params[-2])*(65536/3000)+32768)
            slope = params[-1]
            
            logger.info("LSV params: {}".format(params))
            
            for i in np.arange(start, stop, 1):
                self.output += b"B\n"
                self.output += struct.pack('<Hl', i, 5000*i)
                self.output += b"\n"
                
        def cv():
            params = self._get_params(9)
            start = ceil(int(params[-3])*(65536/3000)+32768)
            v1 = ceil(int(params[-5])*(65536/3000)+32768)
            v2 = ceil(int(params[-4])*(65536/3000)+32768)
            scans = int(params[-2])
            slope = params[-1]
            
            logger.info("CV params: {}".format(params))
            
            for scan in range(scans):
                for i in np.arange(v1, v2, 1):
                    self.output += b"B\n"
                    self.output += struct.pack('<Hl', i, 5000+100*scan)
                    self.output += b"\n"
                self.output += b"S\n\r"
            self.output += b"D\n\r"
            
        def swv():
            params = self._get_params(10)
            start = ceil(int(params[-6])*(65536/3000)+32768)
            stop = ceil(int(params[-5])*(65536/3000)+32768)
            scans = int(params[-1])
        
            logger.info("SWV params: {}".format(params))
            
            if scans < 1:
                scans = 1
                
            for scan in range(scans):
                for i in np.arange(start, stop, 1):
                    self.output += b"B\n"
                    self.output += struct.pack('<Hll', i, 5000+100*scan, 0)
                    self.output += b"\n"
                self.output += b"S\n\r"
            self.output += b"D\n\r" 
            
        def dpv():
            params = self._get_params(10)
            start = ceil(int(params[-6])*(65536/3000)+32768)
            stop = ceil(int(params[-5])*(65536/3000)+32768)
        
            logger.info("DPV params: {}".format(params))
        
            for i in np.arange(start, stop, 1):
                self.output += b"B\n"
                self.output += struct.pack('<Hll', i, 5000+100*scan, 0)
                self.output += b"\n"
                
            self.output += b"D\n\r"
        
        def pmt():
            logger.info("PMT Idle mode entered")
            self.abort_wait()
        
        def pot():
            params = self._get_params(2)
            seconds = int(params[0])
            logger.info("POT params: {}".format(params))
            
            if seconds == 0:
                self.abort_wait()
            else:
                for i in range(seconds):
                    self.output += b"B\n"
                    self.output += struct.pack('<Hll', i, 0, 100)
                    sleep(1)
        
        def ca():
            params = self._get_params(1)
            steps = int(params[0])
            params = self._get_params(steps*2)
            times = [int(i) for i in params[steps:]]
            seconds = sum(times)
        
            for i in range(seconds):
                logger.info(i)
                self.output += b"B\n"
                self.output += struct.pack('<HHl', i, 0, 100*i)
                self.output += b"\n"
                sleep(1)
        
        def sync():
            params = self._get_params(1)
            logger.info("Shutter Sync {} Hz".format(params[0]))
        
        def shut_off():
            logger.info("Shutter Sync Off")
        
        def shut_close():
            logger.info("Shutter closed")
        
        def shut_open():
            logger.info("Shutter open")
            
        experiment_map = {'A' : ads1255,
                          'G' : gain,
                          'L' : lsv,
                          'C' : cv,
                          'S' : swv,
                          'D' : dpv,
                          'M' : pmt,
                          'P' : pot,
                          'R' : ca,
                          'Z' : sync,
                          'z' : shut_off,
                          '1' : shut_close,
                          '2' : shut_open
                      }
        
        char = self._get_input()
        
        try:
            experiment_map[char]()
        except KeyError:
            logger.warning("Unrecognized exp command {}".format(char))
            self.output += b"#ERR: Command {} not recognized\n\r".format(char)
                       
class SerialSim(object):
    def __init__(self, *args, **kwargs):
        self.sim = Simulator()
        self.is_open = True
    def open(self):
        self.is_open = True
    def close(self):
        self.is_open = False
        self.flushInput()
    def write(self, string):
        self.sim.input(string)
    def read(self, size=1):
        output = self.sim.output[0:size]
        self.sim.output = self.sim.output[size:]
        return output
    def flushInput(self):
        self.sim.output = b""   
    def next(self):
        if self.sim.output == b"":
            raise StopIteration
            
        parted = self.sim.output.partition(b'\n')
        output = parted[0]
        
        if parted[2] == b"":
            self.sim.output = b""
        else:
            self.sim.output = parted[2]
            
        return output
        
    def __iter__(self):
        return self