#This verions needs all web files to be downloaded seperatly - see https://git.microIDE.com/setup.py
# microIDE Filename: /microIDE.py
#This verions needs all web files to be downloaded seperatly - see https://git.microIDE.com/setup.py
import usocket, gc, uos,  machine,  network, json
from time import localtime, time, sleep_ms , ticks_ms
from uio import StringIO

#User functions
def millis():
  return ticks_ms()
def epoch(epoch=0,ow=False):
  #Returns HTML Epoch time, can also be used to set the epoch time
  if epoch == 0:
    return time()+946710000;
  else:
    if ow or (time()+946710000 < epoch):
      tm = localtime(epoch-946710000)
      tm = tm[0:3] + (0,) + tm[3:6] + (0,)
      machine.RTC().datetime(tm)
def exists(f):
    try:
      return uos.stat(f)[0] == 32768
    except:
      return False
def sync():
  try:
    #get time form API  -   http://worldtimeapi.org/
    while not network.WLAN(network.STA_IF).isconnected():
      sleep_ms(10) #Wait on Network
    addr = usocket.getaddrinfo('worldtimeapi.org', 80)[0][-1]
    s = usocket.socket()
    s.connect(addr)
    s.send(bytes('GET /%s HTTP/1.0\r\nHost: worldtimeapi.org\r\n\r\n' % "/api/timezone/Europe/London", 'utf8'))
    data=s.readline()
    while not '200 OK' in data:
      data=s.readline()
      sleep_ms(10)
    while data != b'\r\n': #Strip the Header
      data=s.readline()
      sleep_ms(10)
    e=json.loads(s.recv(4096))["unixtime"]
    tm = localtime(e-946710000)
    tm = tm[0:3] + (0,) + tm[3:6] + (0,)
    machine.RTC().datetime(tm)
    s.close()
  except:
    pass
  gc.collect()
#Self-Contained Class for Webserver and Operations
class webServer:
  _con_gz  = b'Content-Encoding: gzip\r\nCache-control: max-age=86400\r\n'
  _con_101 = b'\r\n HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {}\r\n\r\n'
  _con_200 = b'\r\n HTTP/1.1 200 OK\r\nConnection: close\r\n'
  _con_302 = b'\r\n HTTP/1.1 302 FOUND\r\nLocation:{}\r\n\r\n'
  _con_404 = b'\r\n HTTP/1.1 404 NOT FOUND\r\nConnection: close\r\nCache-Control: no-cache\r\n\r\n'
  _con_500 = b'\r\n HTTP/1.1 500 FAIL\r\nConnection: close\r\nCache-Control: no-cache\r\n\r\n'

  _reply=b'\r\n{}\r\n'
  _tmp = b'.tmp'
  _port = 80
  _id = ''.join('{:02X}'.format(xx) for xx in bytearray(machine.unique_id()))
  
  
  def tree(self):
    root=uos.listdir('/')
    for x in root:
        if uos.stat(x)[0]!=32768: #File
          for y in uos.listdir(x):
            root.append(x + '/' + y)
    root.sort()
    return root
  def search(self,file):
    #Looks for a filename and returns the full path
    if len (file)<=2:
      return file
    for x in self.tree():
      if x.find(file.decode("utf-8"))>=0:
        return "/" + x
    return file
  def _sendMime(self,f,file):
    if '.js' in file:
      f.write(b'Content-Type: text/javascript\r\n')
    elif '.ico' in file:
      f.write(b'Content-Type: image/x-icon\r\n')
    elif '.html' in file:
      f.write(b'Content-Type: text/html\r\n')
    elif '.css' in file:
      f.write(b'Content-Type: text/css\r\n')    
    elif '.png' in file:
      f.write(b'Content-Type: image/png\r\n')
    elif '.svg' in file:
      f.write(b'Content-Type: image/svg+xml\r\n')
    elif '.gif' in file:
      f.write(b'Content-Type: image/gif\r\n')
    else:
      f.write(b'Content-Type: text/plain\r\n')
  def _sendStatic(self,f,file):
    # THESE ARE HARD-CODED FILES
    #sub_pack.py will insert files here 
  	#<STAICFILEGLAG>
    return False; #let the calling function know that the file was not found
  def _sendFile(self,c_s,file):
    f=open(file,'r')
    c_s.write(self._con_200)
    self._sendMime(c_s,file)
    if '.gz' in file:
     c_s.write(self._con_gz)

    c_s.write(b'\r\n')
    tmp=f.read(1024)
    if '.py' in file and not file in tmp.encode('utf-8'): # File is python and filename is not in headers - adds commented header
      c_s.write(b'# microIDE Filename: {}\r\n'.format(file.decode()))
    while len(tmp)>1:
      gc.collect()
      c_s.write(tmp)
      tmp=f.read(4092)
    f.close()
  def _head(self,c_s):
    h={}
    while True:
      l = c_s.readline()
      if l == b'\r\n':
        break
      i,v=l.split(b': ', 1)
      h[i.strip()]=v.strip()
    if b'Time' in h:
      epoch(int(h[b'Time'][:-3]))
    return h
  def _load(self):
    from esp32 import NVS

    try:
      json_str = bytearray(NVS('config').get_blob("json",bytearray()))
      NVS('config').get_blob("json",json_str)
    except:
      json_str=bytearray(b'{"wifi":[],"proj":{}}')
    
    self.config=json.loads(json_str)
    #make sure wifi is a variable in the conifg
    try:
      self.config['wifi']
    except:
      self.config['wifi']=[]
    try:

      self.config['ap']
    except:
      self.config['ap']={'ssid': 'microIDE', 'pw': 'Password','auth': 'false','enable':'true'}
    #make sure proj is a variable in the conifg
    try:
      self.config['proj']
    except:
      self.config['proj']={}
      self.config['proj']['name']="microIDE"
      
  def _save(self):
    from esp32 import NVS
    #prepare to store the object in a string again
    json_str=json.dumps(self.config)
    NVS('config').set_blob("json",bytearray(json_str))
    return json_str
  def _srvReq(self,s):
    try:
      c_s,client_addr = s.accept()
      cmd = c_s.readline()
      file=cmd[4:-11].split(b'?')[0]
      head = self._head(c_s)
      #SOCKET Method
      if b'Sec-WebSocket-Key' in head:
        import websocket,hashlib, binascii
        #Handshake
        d = hashlib.sha1(head[b'Sec-WebSocket-Key'])
        d.update(b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
        c_s.write(self._con_101.format(binascii.b2a_base64( d.digest())[:-1].decode("utf-8")))
        
        
        self.ws = websocket.websocket(c_s, True)
        c_s.settimeout(0)
        # notify REPL on socket incoming data
        c_s.setsockopt(usocket.SOL_SOCKET, 20, uos.dupterm_notify)
        uos.dupterm(self.ws)
        
        
      #RST Method
      elif "RST" in cmd:
      	c_s.write(self._con_200)
        c_s.write('\r\n\r\n\r\n')
        c_s.close()
        machine.reset()
        
      #GET Method
      elif "GET" in cmd:
        # Priority of files:
        # 1. filename.gz
        if exists(file+'.gz'):
          self._sendFile(c_s,file+'.gz')
        # 2. filename
        elif exists(file):
          self._sendFile(c_s,file)
        # 3. filename.html
        elif exists(file+'.html'):
          self._sendFile(c_s,file+'.html')
        # 4. static version of filename
        elif self._sendStatic(c_s,file):
          pass #Static file is already sent
        # 5. static version of filename.html
        elif self._sendStatic(c_s,file+'.html'):
          pass #Static file is already sent
        # 6. any file that contains filename
        elif exists(self.search(file)):
          self._sendFile(c_s,self.search(file))
        # 7. if no file was requested send index.html or redirect to microIDE.html
        elif file == b'/':
          if exists('index.html'):
            self._sendFile(c_s,'index.html') #send index
          else:
            c_s.write(self._con_302.format("microIDE.html")) #redirect to editor
        # 8. 404 Error
        else:
          c_s.write(self._con_404)
        c_s.close()  
    #PUT Method
      elif "PUT" in cmd:
        # '/' will just execute whatever code is sent
        if file == b'/':
          prev = uos.dupterm(None)
          try:
            ioStr=StringIO()
            uos.dupterm(ioStr)
            data=c_s.recv(4096)
            exec(data,self._vars)
            c_s.write(self._con_200)
            c_s.write(self._reply.format(ioStr.getvalue()))
          except:
            c_s.write(self._con_500)
          uos.dupterm(prev)
        # any other file name will be saved to disk
        else:

          content_len=0
          if exists(file + self._tmp):
            uos.remove(file + self._tmp)
          f=open(file + self._tmp,'w')
          while True:
            data = c_s.recv(512)
            content_len += len(data)
            if len(data) == 512:
              f.write(data)
              sleep_ms(50)
              gc.collect()
            else:
              f.write(data)
              break
          f.close()
          #check to see if the full file was sent
          if content_len == int(head[b'Content-Length']):
            c_s.write(self. _con_200)
            #REmove Old file
            if  exists(file):
              uos.remove(file)
            #Rename new file
            uos.rename(file+ self._tmp,file)
          else:
            c_s.write(self._con_500)
        c_s.write(b'\r\n')
        c_s.close()
        
    #DIR Method
      elif "DIR" in cmd:
        c_s.recv(4096) #dump the rest
        if "tree" in file:
          c_s.write(self._con_200)
          c_s.write('\r\n')
          c_s.write('Time: {}\r\n'.format(epoch()))
          c_s.write('Disk: {} / {}\r\n'.format((uos.statvfs("/")[2]-uos.statvfs("/")[3])*uos.statvfs("/")[1],uos.statvfs("/")[2]*uos.statvfs("/")[1]))
          gc.collect()
          c_s.write('Mem: {} / {} \r\n'.format(gc.mem_alloc(),self._mem))
          c_s.write('RST: {}\r\n'.format(machine.reset_cause()))
          c_s.write('ID: {};\r\n'.format(self._id))
          gc.collect()
          for x in self.tree():
              if uos.stat(x)[0]==32768: #File
                c_s.write('{},{};\r\n'.format(x,uos.stat(x)[6]))
              else:
                c_s.write('{},DIR;\r\n'.format(x))
          c_s.close()
          gc.collect()
        elif "scan" in file:
          c_s.write(self._con_200)
          c_s.write('\r\n')
          for wifi in network.WLAN(network.STA_IF).scan():
            mac = ''.join('{:02X}'.format(xx) for xx in bytearray(wifi[1]))
            c_s.write('{},{},{}\r\n'.format(wifi[0],mac,wifi[3]))
          c_s.write('\r\n')
          c_s.close()
          gc.collect()
        elif "net" in file:
          c_s.write(self._con_200)
          c_s.write('\r\n')
          for i in {network.STA_IF,network.AP_IF}:
            sta_if = network.WLAN(i)
            c_s.write('{},{},{}\r\n'.format(sta_if.active(),sta_if.isconnected(),sta_if.ifconfig()[0]))
          c_s.write('\r\n')
          c_s.close()
          gc.collect()
          
        elif "config" in file:
          self._load()
          if b'Add-SSID' in head:
            wifi={}
            wifi['ssid']=head[b'Add-SSID']
            wifi['pw']=head[b'Add-Password']
            self.config['wifi'].append(wifi)
          if b'Purge-SSID' in head:
            self.config['wifi'].pop(int(head[b'Purge-SSID']))
          if b'Project-Name' in head:
            self.config['proj']['name']=  head[b'Project-Name']
          c_s.write(self._con_200)
          c_s.write('\r\n')
          #return the config file to the user
          c_s.write(self._save())
          c_s.close()
          gc.collect()
    #Error Method
      else:
        c_s.write(self._con_500)
        c_s.close()
    except:
      pass
    gc.collect()
  def __init__(self,vars={}):
    gc.collect()
    self._mem=gc.mem_alloc()+gc.mem_free()
    self._vars=vars
  def start(self):  
    self.s = usocket.socket()
    self.s.setsockopt(usocket.SOL_SOCKET, usocket.SO_REUSEADDR, 1)
    self.s.bind(usocket.getaddrinfo("0.0.0.0", self._port)[0][-1])
    self.s.listen(8)
    self.s.settimeout(0)
    self.s.setsockopt(usocket.SOL_SOCKET, 20, self._srvReq)
    self._wifi()
  def _wifi(self):
    self._load()
    self.sta=network.WLAN(network.STA_IF);
    self.sta.active(True)
    for i in range(3): # try 3 times
      for x in self.config['wifi']: # try ever saved wifi
        if not self.sta.isconnected(): 
           self.sta.active(True)
           self.sta.connect(x['ssid'],x['pw']) 
           sleep_ms(15000)  
           if self.sta.isconnected():   
             print(self.sta.ifconfig())
    if not self.sta.isconnected(): # connect AP if not able to use STA
      self.ap = network.WLAN(network.AP_IF)
      self.ap.active(True)
      self.ap.config(essid="microIDE", password="password")
    return self.sta.isconnected()
