from twisted.protocols.basic import LineReceiver
from twisted.internet.protocol import ServerFactory
from twisted.web import client
from twisted.internet import main, reactor, task
from twisted.internet import defer
from zope.interface import Interface, implements
import clientmsgs as msg
import logging
import md5, random
import time
import auth

log = logging.getLogger('Clients')


changeClient = """
Abbiamo rilevato che stai utilizzando
SkyProMega o SkyliveNG versione 
antecedente alla versione 0.1.4. 
le connessioni da questi client sono bloccate 
definitivamente, sostituite con 
il nuovo SkyliveNG 0.1.4.
Scarica subito la nuova versione
del client skylive, scarica 
SkyliveNG da www.skylive.it!

We detect you are using SkyProMega 
or SkyliveNG pre 0.1.4 version.
Those clients are now 
blocked, superseded by the new 
SkyliveNG 0.1.4. Please, upgrade to the 
new client, Download SkyliveNG 
from www.skylive.it!

Skylive staff.
"""

changeClient2 = """
Stai usando una versione di test che presenta
alcuni bug non risolti. Per favore aggiorna
alla versione 0.1.4.

You are using a test version of the client 
with some know bugs. Pleas upgrade to 
0.1.4 version.
"""

MAXCLIENT="""
Siamo spiacenti, ma il sistema ha raggiunto 
il massimo numero di utenti connessi gestibile
e non possiamo connetterti.  Riprova piu' tardi.
Grazie.

Sorry, the system as reached the max client 
connections limit and we cannot let you join
in our network at the moment. Please try later.
Thanks

Skylive staff.
"""



class ISkyLiveClients(Interface):

   def antani(self):
      """nothing"""

class OldClientProtocol(LineReceiver):

   version="3500"   
   line_mode = 1
   __buffer = ''
   delimiter = '\r\n'
   MAX_LENGTH = 16384
   state='UNAUTH'
   perms = {}
   ccall = False
   pingcron = False
   setcmds = ['COMANDO', 'Focus', 'SYNC']
   authtry = 0
   ntcomm = ['COMANDO', 'MSGPRIVATO']
   clientversion = 'ProMega'
   osversion = 'Unknown OS'
   username="Unknown"
   crondelta=3.0
   idle=0
   meteocalled=False
   coordinates='00:00:0+00.00'
   status=''
   temp=''
   photorun = False
   maxlimit = False
   lastrecycles = 0

   def connectionMade(self):
      log.info("Connection received")
      self.state='UNAUTH'
      #maxclient = self.on_callback({'checkMaxClient': None})
      #if maxclient:
      #   if maxclient == 'Low':
      #      self.sendServerMessage(MAXCLIENT)
      #      reactor.callLater(10, self.StopAndClose)
      #   else:
      #      self.sendServerMessage(MAXCLIENT)
      #      self.StopAndClose()
      #      self.maxlimit = True
      #else:
      self.write(msg.prewelcome(self.version, self.getNumTelescopes()))
      self.tnum = '1'
      self.uid = self.factory.mkuid()
      self.myip = self.transport.getPeer()
      self.lastReceived=time.time()
      #maxclient = self.on_callback({'checkMaxClient': None})
      self.lastreccall = reactor.callLater(60, self.checkLastRec)


   def StopAndClose(self):
      log.warning("StopAndClose %s version %s" % (self.username, str(self.clientversion)))
      try:
         self.on_sendStop()
      except:
         pass
      reactor.callLater(1, self.close, 'force')

   def checkLastRec(self):
      now=time.time()
      if now - self.lastReceived > 60:
         log.warning("CHECKLASTREC CLOSE "+str(self.username))
         self.close('force')
      if self.state == 'UNAUTH' and self.lastrecycles < 5:
         self.lastrecycles = self.lastrecycles+1
         reactor.callLater(5, self.checkLastRec)

   def connectionLost(self, reason):
      log.warning("Connection lost for user %s with version  %s" % (self.username, str(self.clientversion)))
      self.state='UNAUTH'
      try:
         self.delFromTelescopeList()
         self.on_callback({'removeUser': self})
      except:
         pass
      try:
         self.stopCronCalls()
      except:
         pass

   def dataReceived(self, data):
      self.lastReceived=time.time()
      """Protocol.dataReceived.
      Translates bytes into lines, and calls lineReceived (or
      rawDataReceived, depending on mode.)
      This is modified to skylive cause we have the fucking private message 
      command that isn't line terminated, shit.
      """
      self.idle=0
      self.__buffer = self.__buffer+data
      try:
         # A crude hack to solve the problem of private messages without terminations
         if(self.__buffer.split("#^#")[2] in self.ntcomm):
            if(len(self.__buffer.split("#^#")) >= 5):
               tmpbuf=self.__buffer.split("#^#")
               # Work around to prevent line splitting on privmsg newlines 
               tmpbuf[3] = tmpbuf[3].replace("\n", chr(220)).replace("\r", '')
               if tmpbuf[4][len(tmpbuf[4])-2:] != "\r\n":
                  self.__buffer="#^#".join(tmpbuf[:5])+self.delimiter
                  if len(tmpbuf) > 5:
                     log.warning("Buffer too long!")
                     self.__buffer=self.__buffer+"#^#".join(tmpbuf[5:])
      except:
         pass
      while self.line_mode and not self.paused:
         try:
            line, self.__buffer = self.__buffer.split(self.delimiter, 1)
         except ValueError:
            if len(self.__buffer) > self.MAX_LENGTH:
               line, self.__buffer = self.__buffer, ''
               return self.lineLengthExceeded(line)
            break
         else:
            linelength = len(line)
            if linelength > self.MAX_LENGTH:
               exceeded = line + self.__buffer
               self.__buffer = ''
               return self.lineLengthExceeded(exceeded)
            why = self.lineReceived(line)
            if why or self.transport and self.transport.disconnecting:
               return why
      else:
         if not self.paused:
            data=self.__buffer
            self.__buffer=''
            if data:
               return self.rawDataReceived(data)

   def lineReceived(self, line):
      self.lastReceived=time.time()
      log.debug("Received: "+line)
      getattr(self, 'state_' + self.state)(line)


   def state_UNAUTH(self, line):
      try:
         user, pwd, cmd, ver, osver = line.split("#^#")[:5]
      except:
         user, pwd, cmd, ver = line.split("#^#")[:4]
         osver = 'NULL'
      self.canping = False
      if(cmd=='InvioDati'):
         if(ver.lower() != 'null'):
            self.clientversion = int(ver)
            self.canping = True
         if(osver.lower().replace("\r", "").replace("\n", "") != 'null'):
            self.osversion = str(osver)
         self.username = user
         self.factory.getAuth(user, pwd
                  ).addErrback( self.on_Error
                  ).addCallback( self.on_AuthDone )
      else:
         self.authtry = self.authtry+1
         self.write(msg.reqauth())

   def state_OLDCLIENT(self, line):
      pass

   def state_AUTH(self, line):
      user, pwd, cmd = line.split("#^#")[:3]
      if(cmd=='InvioDati'):
         #self.state = 'UNAUTH'
         try:
            self.connectionLost('reauth requested')
         except:
            pass
         self.state_UNAUTH(line)
      else:
         iscommand=False
         if line.split("#^#")[2] in self.setcmds:
            iscommand=True
            try:
               canset = self.perms[self.tnum]['canset']
            except:
               canset = False
            try:
               reserved = self.on_callback({'TelescopeIsReserved': self.tnum})
               if reserved and (len(reserved) > 0) and not self.perms['general']['admin']:
                  # telescope is reserved
                  cuser = self.username
                  if self.username[:1] == "~":
                     cuser = self.username[1:]
                  if cuser not in reserved:
                     # User not in reserved list
                     reservedpres = self.on_callback({'Telescope_ReserverIsHere': self.tnum})
                     if reservedpres:
                        # A reserved user is present
                        log.info("User try to send "+line+" but the telescope "+str(self.tnum)+"is now reserved")
                        return
                  else:
                     #User is reserved
                     log.info("User reserved command from "+self.username+" on telescope "+str(self.tnum))
                     # enabling the user even if it isn't by default because
                     # it is reserved
                     canset = True
               if not canset and not self.perms['general']['admin']:
                  # The user isn't enabled and isn't an admin
                  log.info("User try to send "+line+" but it isn't enabled on telescope "+str(self.tnum))
                  return
            except:
               if not self.perms['general']['admin']:
                  # The user isn't an admin
                  log.debug("Exception on accepting command")
                  log.info("User try to send "+line+" but it isn't enabled on telescope "+str(self.tnum))
                  return
         act = self.factory.parser.parseMsg(line)
         if act:
            try:
               if (((not 'MakePhoto' in act.keys()) or (act['MakePhoto'][0] != '001')) and iscommand):
                  if not self.on_callback({'getIsTeleFree': self.tnum}):
                     log.debug("Command received but telescope isn't free (%s)" % str(self.username+":"+str(act)))
                     return
            except:
               pass
            log.debug(self.username+": "+str(act))
            self.on_callback(act)

   def on_callback(self, cmd, *args, **kwargs):
      for key in cmd:
         f=getattr(self, 'on_'+key, None)
         if f and callable(f):
            return f(cmd[key])
         else:
            return self.factory.command(self.username, key, cmd[key], *args, **kwargs)

   def rawDataReceived(self, data):
      pass

   def write(self, msg):
      log.debug('WRITE: '+msg)
      self.transport.write(msg)
      if self.idle  > 12800:
         log.warning("More than 4 hours of idle for %s" % self.username)
         # more than four hours without receiving anything.
         # disconnect the client.
         #self.close('force')
         self.StopAndClose()

   def close(self, *args):
      log.warning("CLOSING %s CONNECTION (version %s)" % (str(self.username), str(self.clientversion)))
      try:
         self.transport.loseConnection()
         if len(args) > 0:
            if args[0] == 'timeout' or args[0] == 'force':
               self.connectionLost('force')
               self.__ignoreBuffer = True
               self.transport.connectionLost(main.CONNECTION_DONE)
      except:
         pass

   def on_Error(self, err='Unknown'):
      log.warning('ERRORE: '+err)


   def on_AuthDone(self, res):
      try:
         self.lastreccall.cancel()
      except:
         log.warning("CANCELLING LASTRECCALL FAILED %s" % self.username)
      self.state = 'UNAUTH'
      if res:
         self.state = 'AUTH'
         self.perms = res
      log.debug(self.perms)
      if self.state=='UNAUTH':
         if self.authtry > 3:
            log.warning('Terzo Tentativo')
            self.close()
         else:
            self.authtry = self.authtry+1
         log.info("Wrong auth from "+self.username)
      else:
         if not self.on_callback({'isDuplicatedRequest': self.uid}):
            #XXX Fai si che le connessioni dal vecchio client siano SEMPRE con ~ davanti per il momento...
            self.username = self.username = '~'+self.username

            if self.on_callback({'isSecondClient': self.username}):
               if self.username[:1] != '~':
                  self.username = '~'+self.username
               else:
                  log.warning(self.username+" E' alla terza connessione!")
                  self.StopAndClose()
                  return
            self.authtry = 0

            #self.startCronCalls()
            #log.warning("AUTHDONE STARTCRONCALLS")
            ## creating a reference to this client on the core
            #self.on_callback({'setUserConnection': self})
            #self.on_callback({'getTelescopeWhiteLists': True})
            #self.on_changeTelescope()
            #self.getStarList()
            #self.getDoubleList()
            #self.write(msg.welcome())
            close = False
            adv = True
            if self.username in self.factory.advised.keys():
               if self.factory.advised[self.username] > 5:
                  adv = False
            if not self.canping:
               if adv:
                  self.sendServerMessage(changeClient)
               log.debug("UN PROMEGA")
               close=True
            else:
               if int(self.clientversion) < 123:
                  if adv:
                     self.sendServerMessage(changeClient)
                  log.debug("UN VECCHIO NG")
                  close=True
               if int(self.clientversion) >= 123 and int(self.clientversion) < 134:
                  if adv:
                     self.sendServerMessage(changeClient)
                  close=True
               else:
                  if int(self.clientversion) >= 134  and int(self.clientversion) < 140:
                     self.sendServerMessage(changeClient2)
                  # CLIENT AGGIORNATO
                  close = False
                  adv = True
                  if self.username in self.factory.advised.keys():
                     del self.factory.advised[self.username]

            if close:
               if self.username in self.factory.advised.keys():
                  self.factory.advised[self.username] = self.factory.advised[self.username]+1
               else:
                  self.factory.advised[self.username] = 1
               if not adv:
                  self.state = 'OLDCLIENT'
                  self.on_changeTelescope()
                  self.write(msg.welcome())
                  self.on_callback({'setUserConnection': self})
                  self.on_callback({'getTelescopeWhiteLists': True})
                  self.on_changeTelescope()
                  self.getStarList()
                  self.getDoubleList()
                  self.write(msg.welcome())
                  self.on_sendStatus("PLEASE UPGRADE THE CLIENT! - ATTENZIONE, AGGIORNA IL CLIENT!", self.coordinates)
               reactor.callLater(10, self.StopAndClose)
               return 

            self.startCronCalls()
            # creating a reference to this client on the core
            self.on_callback({'setUserConnection': self})
            self.on_callback({'getTelescopeWhiteLists': True})
            self.on_changeTelescope()
            self.getStarList()
            self.getDoubleList()
            self.write(msg.welcome())
            self.write(msg.setUsername(self.username))
            log.info("User "+self.username+" logged in with "+str(self.clientversion)+" version")



            maxclient = self.on_callback({'checkMaxClient': None})
            if maxclient and not self.perms['general']['admin']:
               # MAX CLIENT REACHED
               if maxclient == 'Low':
                  self.sendServerMessage(MAXCLIENT)
                  reactor.callLater(10, self.StopAndClose)
                  log.info("User "+self.username+" disconnected for low maxclient")
                  return
               else:
                  self.sendServerMessage(MAXCLIENT)
                  log.info("User "+self.username+" disconnected for high maclient")
                  self.StopAndClose()
                  return
            else:
               if maxclient and self.perms['general']['admin']:
                  log.info("User "+self.username+" logged in because is an admin, but maxclient is "+str(maxclient))
               if not close:
                  self.write(msg.loginmsg(self.state, self.perms))
               else:
                  self.write(msg.chatMessage(changeClient))
         else:
            log.debug('Duplicated AUTH request for %s' % self.username)


   def startCronCalls(self):
      if not self.ccall:
         self.ccall = task.LoopingCall(self.cronCalls)
         self.ccall.start(self.crondelta)
      if not self.pingcron:
         self.pingcron = task.LoopingCall(self.pingCron)
         self.pingcron.start(60)


   def stopCronCalls(self):
      try:
         self.ccall.stop()
         self.ccall = False
      except:
         pass
      try:
         self.pingcron.stop()
         self.pingcron = False
      except:
         pass

   def pingCron(self):
      if not self.canping:
         return
      try:
         if((time.time() - self.lastpong) > 240):
            log.warning("PING CLOSE %s with version %s, lastpong %s, now %s" 
                  % (str(self.username), str(self.clientversion), str(self.lastpong), str(int(time.time()))))
            self.close('timeout')
      except:
         pass
      self.lastping = time.time()
      self.write(msg.ping())

   def cronCalls(self, *args):
      if self.on_callback({'getPhotoRun': self.tnum}):

         # TODO remove old clients
         if int(self.clientversion) < 131:
            #coordinates, status, temp = self.on_callback({'getTelescopePhotoStatus': self.tnum})
            pass
         else:
            # FINE REMOVE OLD CLIENTS
            photorun = True
            self.photorun = True
            coordinates, status, temp = self.on_callback({'getTelescopeStatus': self.tnum})

         if len(args) == 0:
            reactor.callLater(1, self.cronCalls, True)
            reactor.callLater(2, self.cronCalls, True)
      else:
         photorun = False
         coordinates, status, temp = self.on_callback({'getTelescopeStatus': self.tnum})
      # TODO remove old clients
      if int(self.clientversion) < 131:
         #self.on_sendStatus(status ,coordinates)
         #self.on_sendCCDtemp(temp)
         pass
      else:
         # FINE REMOVE OLD CLIENTS
         if not photorun and self.photorun:
            self.photorun = photorun
            self.on_sendStatus(status ,coordinates)
         if coordinates != self.coordinates or status != self.status:
            self.on_sendStatus(status ,coordinates)
            self.status = status
            self.coordinates = coordinates
         if temp != self.temp:
            self.on_sendCCDtemp(temp)
            self.temp = temp
      self.idle=self.idle+self.crondelta

   def getStarList(self):
      return self.write(msg.sendlist('availableStars', self.on_callback({'getStarList': True})))

   def getDoubleList(self):
      return self.write(msg.sendlist('availableDoubles', self.on_callback({'getDoubleList': True})))

   def getNumTelescopes(self):
      return self.on_callback({'getNumTelescopes': True})

   def on_sendStatus(self, status, coordinates):
      log.info("send Status to %s, %s, %s" % (self.username, status, coordinates))
      self.write(msg.statusmsg(status, coordinates))
   
   def on_sendFWHM(self, fwhm):
      self.write(msg.fwhm(fwhm))

   def on_sendCCDtemp(self, temp):
      self.write(msg.ccdtempmsg(temp))

   def on_sendUserList(self, *args):
      self.write(msg.userlist(args[0]))

   def on_sendWheather(self, wheat):
      self.write(msg.WheatherStatus(wheat))

   def on_sendObject(self, object):
      self.write(msg.setObject(object))

   def on_sendDome(self, dome):
      self.write(msg.DomeStatus(dome))

   def on_sendStreaming(self, stream):
      self.write(msg.StreamStatus(stream))

   def on_sendForceStreaming(self, stream):
      self.write(msg.StreamStatus(stream, forced=True))

   def on_sendLONLAT(self, lon, lat):
      self.write(msg.setLonLat(lon, lat))

   def on_sendTelescopeFeatures(self, tname, ccdname, filters, fov, focal):
      self.write(msg.telescopeFeatures(tname, ccdname, filters, fov, focal))

   def on_sendChatMessage(self, *args):
      log.debug('CHAT: '+str(args))
      self.write(msg.chatMessage(args[0], args[1]))

   def on_sendPrivMessage(self, *args):
      self.write(msg.privMessage(args[0], args[1]))
   
   def on_sendHtmlMessage(self, *args):
      self.write(msg.htmlMessage(args[0], args[1], args[2], args[3]))

   def on_sendBigLive(self, *args):
      self.write(msg.bigLive())

   def on_sendOpenLink(self, *args):
      self.write(msg.openLink(args[0], args[1]))

   def on_sendLivePointer(self, x, y):
      self.write(msg.Pointer(x, y))

   def on_sendMeteoUrl(self, url):
      self.write(msg.meteo(url))
      if not self.meteocalled and not self.canping:
         self.clientversion="ProMega"
         self.sendServerMessage(changeClient)
         self.write(msg.welcome())
         self.meteocalled = True


   def on_sendPhotoYes(self, *args):
      self.write(msg.photoYes())

   def on_sendPhotoNo(self, *args):
      self.write(msg.photoNo())

   def on_sendGuideErr(self, *args):
      Xerr = args[0]
      Yerr = args[1]
      self.write(msg.guideErr(Xerr, Yerr))

   def on_sendTelescopeOnOff(self, status):
      if status == "ON":
         self.write(msg.teleOn())
      else:
         self.write(msg.teleOff())

   def on_changeTelescope(self, tnum=None):
      if tnum:
         self.delFromTelescopeList()
         self.tnum=tnum
      self.putInTelescopeList()
      self.cronCalls()

   def on_MsgToAll(self, msg):
      self.on_callback({'ChatMessageForTelescope': self.tnum}, msg)

   def on_MoveCARDINAL(self, where):
      self.on_callback({'TelescopeMoveCARDINAL': self.tnum}, where)

   def on_MovePlanets(self, *args):
      # Qui finiscono pianeti, stelle, doppie
      self.on_callback({'TelescopeMovePlanets': self.tnum}, args[0][0], args[0][1], args[0][2])

   def on_MoveARDEC(self, *args):
      self.on_callback({'TelescopeMoveARDEC': self.tnum}, args[0][0], args[0][1])

   def on_MoveMessier(self, *args):
      self.on_callback({'TelescopeMoveMessier': self.tnum}, args[0][0], args[0][1])

   def on_MoveCngc(self, *args):
      self.on_callback({'TelescopeMoveCngc': self.tnum}, args[0][0], args[0][1])

   def on_MakePhoto(self, *args):
      exp = args[0][0]
      codestr = args[0][1]
      self.on_callback({'TelescopeMakePhoto': self.tnum}, exp, codestr)
   
   def on_Center(self, center):
      self.on_callback({'TelescopeCenter': self.tnum}, center)

   def on_MeteoUrl(self, *args):
      self.on_callback({'getMeteoUrl': self.tnum})

   def on_setPointer(self, *args):
      x = args[0][0]
      y = args[0][1]
      self.on_callback({'livePointer': self.tnum}, x, y)

   def on_PongReceived(self, *args):
      self.lastpong = time.time()
   
   def on_isTeleFree(self, *args):
      if self.on_callback({'getIsTeleFree': self.tnum}):
         self.write(msg.TeleFREE())
      else:
         self.write(msg.TeleBUSY())

   def on_getStreamingStatus(self, *args):
      self.on_callback({'sendStramingUri': self.tnum})

   def on_sendStop(self):
      log.debug("Stopping Client")
      self.write(msg.stopClient())

   def sendTeleBinning(self, binning):
      self.write(msg.TeleBINNING(binning))

   def putInTelescopeList(self):
      self.on_callback({'putInTelescopeList': self.tnum})

   def delFromTelescopeList(self):
      self.on_callback({'delFromTelescopeList': self.tnum})

   def on_sendErroneusBinning(self, binlist, bin=False):
      errBin = "Questo telescopio e' limitato all'uso del binning a %s!\n" % (binlist)
      errBin += "This telescope is limited to binning %s!\n" % (binlist)
      self.sendServerMessage(errBin)

   def sendServerMessage(self, mesg):
      self.write(msg.privMessage(mesg))

   def sendTelescopeLiveUri(self, turi, tint):
      self.write(msg.TelescopeLiveUri(turi, tint))

   def sendExposureRunning(self, rem, filter, bin, exp, nick):
      log.debug("sendExposureRunning")
      self.write(msg.ExposureRunning(rem, filter, bin, exp, nick))

   def sendExposureUpload(self, rem, filter, bin, exp, nick):
      log.debug("sendExposureUpload")
      self.write(msg.ExposureUpload(rem, filter, bin, exp, nick))

   def sendExposureNone(self):
      log.debug("sendExposureNone")
      self.write(msg.ExposureNone())

class OldClientFactory(ServerFactory):
   
   protocol = OldClientProtocol
   counter = 0
   advised = {}

   def __init__(self):
      self.parser = msg.clientParser()

   def mkuid(self):
      """ make an unique id to recognize a single session """
      self.counter = self.counter+1
      if(self.counter > 999999999):
         self.counter = 1
      t = time.time()
      return md5.new("%s_%s_%s" % (str(random.random()), str(self.counter), str(t))).hexdigest()

   def setConfig(self):
      self.protocol.cfg = self.cfg
      #self.auth = auth.clientAuth(self.cfg.get("general", "authuri"))
      self.auth = auth.DBClientAuth()

   def getAuth(self, user, pwd):
      return self.auth.getAuth(user, pwd)


class OldClientHTTPTunnelFactory:

   protocol = OldClientProtocol
   counter = 0

   def __init__(self):
      self.parser = msg.clientParser()

   def mkuid(self):
      """ make an unique id to recognize a single session """
      self.counter = self.counter+1
      if(self.counter > 999999999):
         self.counter = 1
      t = time.time()
      return md5.new("httptunnel_%s_%s_%s" % (str(random.random()), str(self.counter), str(t))).hexdigest()

   def setConfig(self):
      self.protocol.cfg = self.cfg
      #self.auth = auth.clientAuth(self.cfg.get("general", "authuri"))
      self.auth = auth.DBClientAuth()

   def getAuth(self, user, pwd):
      return self.auth.getAuth(user, pwd)


