###########################################################################
# Copyright (c) 2018- Franco (nextime) Lanza <franco@nexlab.it>
#
# Penguidom System client Daemon "penguidomd"  [https://git.nexlab.net/domotika/Penguidom]
#
# This file is part of penguidom.
#
# penguidom is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################

from penguidom import imodules
from zope.interface import implements
from twisted.plugin import IPlugin

from twisted.internet.serialport import SerialPort
from twisted.internet.protocol import BaseProtocol, Protocol, Factory
from twisted.internet import reactor, tcp
from nexlibs.utils import genutils

from importlib import import_module
import serial
import time

import paradox37b as p37b
from mapping import EVENTMAP, REGISTERS


SERIAL_PORT="/dev/ttyS0"
BAUDRATE=9600
BOARDTYPE="MG5050" # Just a default

PKTTIMEOUT=2 # Seconds before discard incorrect lenght packets
REPLYTIMEOUT=1 # Seconds before to start reply checks
SEND_INTERVAL=.5 # Seconds between messages sent
REPLY_INTERVAL=.3 # Seconds between reply checks

class ParadoxProtocol(BaseProtocol):


   packet=[]
   queue=[]
   sendqueue=[]
   replyqueue=[]
   packettimeout = 0
   proxy = False
   zonemap = False
   boardname = ''

   def __init__(self, logger, core, cfg):
      self.log = logger
      self.core = core
      self.cfg = cfg
      self.log.debug(str(self.cfg))
      self.flags = p37b.Flags()
      try:
         self.proxyonly = genutils.isTrue(self.cfg.get('connection', 'proxyonly'))
         self.mapnames = genutils.isTrue(self.cfg.get('panel', 'mapNames'))
         self.autodetect = genutils.isTrue(self.cfg.get('panel', 'autodetect'))
      except:
         self.proxyonly = False
         self.mapnames = False
         self.autodetect = False
      reactor.addSystemEventTrigger('before', 'shutdown', self._terminating)

   def _queueData(self):
      if len(self.packet) >= 37:
         packet="".join(self.packet[:37])
         if p37b.checkSumTest(packet):
            self.log.debug("<< MESSAGE QUEUED: "+''.join( [ "\\x%02X" % ord( x ) for x in packet ] ).strip()) 
            #print packet
            self.queue+=[packet]
            self.packet=self.packet[37:]
         else:
            self.log.debug("Serial message incorrect: shift queue and discard first byte ("+"\\x%02X" % ord( self.packet[0] )+")")
            self.packet=self.packet[1:]
         reactor.callLater(0, self._queueData)
         reactor.callLater(0, self._processQueue)
      elif len(self.packet) > 0:
         self.packettimeout = time.time()
         
   def _queueSendData(self, data, callback=False, expected_reply=False, callback_args=False):
      self.sendqueue+=[{'msg': data, 'cb': callback, 'try': 3, 'expected_reply': expected_reply, 'callback_args': callback_args}]
      reactor.callLater(0, self._processSendQueue)

   def _processQueue(self):

      def _expected_reply(replystart, expected):
         if expected:
            if isinstance(expected, (list, tuple)):
               for i in expected:
                  if p37b.checkCmd(replystart, i, nibble=True):
                     return True
            else:
               if p37b.checkCmd(replystart, expected, nibble=True):
                  return True
         else:
            return True
         return False

      if len(self.queue) > 0:
         if p37b.checkCmd(ord(self.queue[0][0]), p37b.CMD_EVENT, nibble=True):
            reactor.callLater(0, self._processEvent, self.queue[0])
         else:
            if len(self.replyqueue) > 0:
               if _expected_reply(ord(self.queue[0][0]), self.replyqueue[0]['expected_reply']):
                  # XXX Can we check in some way if is the *RIGHT* reply?
                  #     in theory we don't need it as we will not send anything
                  #     until the reply is received, but it would be more error
                  #     prone that way...
                  if self.replyqueue[0]['cb'] and callable(self.replyqueue[0]['cb']):
                     reactor.callLater(0, self.replyqueue[0]['cb'], self.queue[0], self.replyqueue[0]['callback_args'])
                  del self.replyqueue[0]
               else:
                  if isinstance(self.replyqueue[0]['expected_reply'], (list, tuple)):
                     expected = "["+",".join(["\\x%02X" % (x) for x in self.replyqueue[0]['expected_reply']])+"]"
                  else:
                     print self.replyqueue[0]['expected_reply'] << 4
                     expected = "\\x%02X" % (self.replyqueue[0]['expected_reply'])
                  self.log.error("Wrong Expected Reply! (have "+"\\x%02X" % (ord(self.queue[0][0]) >> 4)+" expect "+expected+")")
         del self.queue[0]
    
   def _processSendQueue(self, addToReplyQueue=True):
      if len(self.sendqueue) > 0:
         if not len(self.replyqueue) or (not addToReplyQueue and len(self.replyqueue) > 0):
            packet = self.sendqueue[0]['msg']
            if addToReplyQueue:
               self.replyqueue+=[self.sendqueue[0]]
            del self.sendqueue[0]
            self.log.debug(">> SEND MESSAGE: "+''.join( [ "\\x%02X" % ord( x ) for x in packet ] ).strip())
            self.transport.write(packet)
            reactor.callLater(REPLYTIMEOUT, self._checkReplies)
         else:
            reactor.callLater(SEND_INTERVAL, self._processSendQueue)

   def _checkReplies(self):
      if len(self.replyqueue) > 0:
         # Is there a reply waiting? why?
         if self.replyqueue[0]['try'] > 0:
            self.replyqueue[0]['try']-=1
            self.sendqueue+=[self.replyqueue[0]]
            reactor.callLater(.3, self._processSendQueue, False)
         else:
            self.log.error("FAILED TO SEND MESSAGE: "+''.join( [ "\\x%02X" % ord( x ) for x in self.replyqueue[0]['msg'] ] ).strip())
            del self.replyqueue[0]


   def dataReceived(self, data):
      if self.proxy:
         self.proxy.serialInData(data)
      if len(self.packet) > 0 and (time.time()-self.packettimeout) > PKTTIMEOUT:
         self.log.debug("Serial Timeout: discard packet "+''.join( [ "\\x%02X" % ord( x ) for x in self.packet ] ).strip())
         self.packet = []
      self.packet+=list(data)
      self._queueData()


   def connectionMade(self):
      self.log.info("Serial port correctly connected")
      self.connect()

   def _terminating(self, *a, **kw):
      self.log.debug("Send Disconnect message")
      self.write(p37b.MSG_DISCONNECT)
      self.transport.loseConnection()

   def connectionLost(self, reason):
      self.log.debug("Connection lost for "+str(reason))

   def connect(self):
      if not self.proxyonly:
         self.write(p37b.MSG_CONNECT, self._detectBoard, expected_reply=p37b.REPLY_CONNECT)
         self.write(p37b.MSG_GETSTATUS, expected_reply=p37b.REPLY_GETSTATUS)
         self.write(p37b.MSG_SYNC, self._replySync, expected_reply=p37b.REPLY_SYNC)
      

   def write(self, message, reply_callback=False, expected_reply=False, callback_args=False):
      self._queueSendData(p37b.format37ByteMessage(message), reply_callback, expected_reply, callback_args)
         

   def _replySync(self, reply, cbargs):
      self.log.debug("REPLY SYNC RECEIVED")
      self.write(reply, self._endHandshacke)


   def _endHandshacke(self, reply=None, cbargs=None):
      # XXX Here things starts to get weird.
      #     I have no idea of what exactly those two messages do, 
      #     but they seems to be needed for the initial handshake
      #     when connecting. So, we just send both, hope 
      #     sometime i will fully understand what they do exactly.
      #
      #     After the end of the handshake we proceed to map zone names,
      #     so, callback for last message drive the runflow in that direction.
      self.write(p37b.MSG_UNKWNOWN_HS1, expected_reply=p37b.REPLY_QUERY)
      self.write(p37b.MSG_UNKWNOWN_HS2, self.mapNames, expected_reply=p37b.REPLY_QUERY)


   def _detectBoard(self, reply, cbargs=None):
      board = p37b.getPanelName(reply)
      self.boardname = board
      self.log.info('Detected Panel Board as '+board)
      if self.autodetect:
         try:
            self.log.info("Autoload panel board mapping for "+str(board))
            maps = import_module("penguidom.plugins.paradox.panels."+str(board))
            EVENTMAP.loadEvents(maps.EventMap())
            REGISTERS.loadRegisters(maps.Registers())
            self.log.info("Panel board mapping for "+str(board)+" loaded successfully")
         except:
            self.log.error("Cannot autoload board mapping for "+str(board))

   def _processEvent(self, event):
      self.flags.asByte = ord(event[0])
      alarm = self.flags.alarm

      year = (ord(event[1])*100)+ord(event[2])
      month = ord(event[3])
      day = ord(event[4])
      hour = ord(event[5])
      minute = ord(event[6])
      date = "-".join([str(year), str(month), str(day)])+" "+":".join([str(hour), str(minute)])
      

      event, subevent = EVENTMAP.getEventDescription(ord(event[7]), ord(event[8]))
      event = event.strip()
      subevent = subevent.strip()

      self.log.debug("Event: "+date+" - "+event+": "+subevent+" - In alarm:"+str(alarm))


   def _setNames(self, reply, item):
      if item in REGISTERS.getsupportedItems():
         reglist = getattr(REGISTERS, 'get'+item+'Register')()
         setfunc = getattr(EVENTMAP, 'set'+item)
         reg = reply[1:4]
         if reg in reglist.keys():
            nums = reglist[reg]
            for num in range(0,len(nums)):
               if nums[num] is not None:
                  label = reply[4+(16*num):20+(16*num)].strip()
                  self.log.info('Set label for '+item+' '+str(nums[num])+' as \"'+label+'\"')
                  setfunc(nums[num], label)

   def _mapItemNames(self, item):
      try:
         items = getattr(REGISTERS, 'get'+item+'Register')()
         for i in items.keys():
            self.write(p37b.SEND_QUERY+i, self._setNames, expected_reply=p37b.REPLY_QUERY, callback_args=item )
      except:
         self.log.error("Cannot find item named "+item+" in registers")

   def mapNames(self, reply=None, cbargs=None):
      if self.mapnames:
         self.log.info("Start Mapping names...")
         self.log.info("Supported Items:")
         for i in REGISTERS.getsupportedItems():
            self.log.info("         "+i)
            reactor.callLater(0, self._mapItemNames, i)


class ParadoxTCPProxy(Protocol):

   cid = 0

   def __init__(self, clients, factory):
      self.clients = clients
      self.factory = factory

   def connectionMade(self):
      cid = len(self.clients)
      self.clients[cid] = self
      self.cid = cid
      
   def connectionLost(self, reason):
      if self.cid in self.clients.keys():
         del self.clients[self.cid]

   def dataReceived(self, data):
      self.factory.log.debug("Data FROM TCP Client "+str(self.cid)+": "+''.join( [ "\\x%02X" % ord( x ) for x in data ] ).strip())
      self.factory.serproto.transport.write(data)

class ParadoxTCPFactory(Factory):

   def __init__(self, core, serproto, logger):
      self.clients = {}
      self.serproto = serproto
      self.serproto.proxy = self
      self.log = logger

   def serialInData(self, data):
      for cid in self.clients.keys():
         self.clients[cid].transport.write(data)

   def buildProtocol(self, addr):
      return ParadoxTCPProxy(self.clients, self)


class Paradox(object):
   implements(IPlugin, imodules.IModules)
   serproto = False

   def _openSerial(self, port=SERIAL_PORT, retry=3):
      try:
         dev = self.cfg.get('connection', 'port')
      except:
         dev = port
      try:
         brate = int(self.cfg.get('connection', 'baudrate'))
      except:
         brate = BAUDRATE

      if retry > 0:
         try:
            self.serproto = ParadoxProtocol(self.log, self.core, self.cfg)
            self.port = SerialPort(self.serproto, dev, reactor, baudrate=brate)
         except serial.SerialException as err:
            self.log.info("Serial Port ERROR: "+str(err))
            reactor.callLater(1, self._openSerial, port, retry-1)
         
      else:
         self.log.info("Unable to open Serial Port: retry in 1 minute")
         reactor.callLater(60, self._openSerial, port)

   def initialize(self, callback, logger):
      self.log = logger
      self.core = callback
      self.cfg = self.core.readPluginConfig('paradox')
      logger.info("Initialize Serial Connection...")
      self._openSerial()
      if genutils.isTrue(self.cfg.get("connection", "enableTCPProxy")) and self.serproto:
         self.proxy = ParadoxTCPFactory(self.core, self.serproto, self.log)
         tcpport = self.cfg.get("connection", "tcpport")
         self.log.debug("TCP Port "+str(tcpport)+" OPEN")
         port = tcp.Port(int(tcpport), self.proxy, 50, self.cfg.get("connection", "tcpaddr"), reactor)
         port.startListening()
      try:
         defboard = self.cfg.get("panel", "paneltype")
      except:
         defboard = BOARDTYPE
      try:
         maps = import_module("penguidom.plugins.paradox.panels."+str(defboard))
         EVENTMAP.loadEvents(maps.EventMap())
         REGISTERS.loadRegisters(maps.Registers())
         self.log.info("Loaded registers and event mapping for default panel type "+str(defboard))
      except:
         self.log.error("Cannot import panel mapping for panel type "+str(defboard))
      logger.info("Plugin initialized")


   def getWebHome(self):

      webparts = self.core.getWebParts()
      labelreg = REGISTERS.getsiteNameLabelRegister()
      try:
         if labelreg.items()[0][1][0] is not None:
            colname = str(labelreg.items()[0][1][0])
         else:
            colname = " ".join(['Paradox', self.serproto.boardname])
      except:
         colname = 'paradox'

      partitions = EVENTMAP.getAllpartitionLabel()
      #self.log.info(EVENTMAP.getsiteNameLabel())
      colcont = ''
      for partition in partitions.items():
         partname = partition[1]
         partidx = partition[0]
         colcont+=webparts.getButton(title=str(partname), act=webparts.getOnOffSwitch("".join(['paradox_partition_', str(partidx)]), on='ARMED', off='DISARMED'))
      return webparts.getColumn(title=colname, content=colcont)


   def getWebPage(self):
      return REGISTERS.getsiteNameLabelRegister()