#https://currentmillis.com/time/seconds-since-unix-epoch.php
#https://docs.micropython.org/en/latest/library/esp32.html#the-ultra-low-power-co-processor
import usocket
import gc
import os
import _thread
import machine
import ucryptolib
import network
from time import localtime
from time import time
from uio import StringIO

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 excel_time():  
  #Returns the time in Excel format (for .csv files) 
  return (epoch()+2208988800)/86400;  
  
def tree():
  #Returns a sorted array of all files - recursively
  root=os.listdir('/')
  for x in root:
    if not '.orig' in x:
      if os.stat(x)[0]!=32768: #File
        for y in os.listdir(x):
          root.append(x + '/' + y)
  root.sort()
  return root
  
def exists(f):
  #Test if a file exists and has a file size
  try:
    return os.stat(f)[6] >0
  except:
    return False
      
def wget(file,ow=False,prt=True):
    #Method to pull support files from git.microide.com
    if not exists(file) or ow:  #Overwrite protection!!! - comment line to disable
      while not network.WLAN(network.STA_IF).isconnected():
        pass #Locks until a network connection is made
      if prt:
        print("Getting ",file,end=' ')
      addr = usocket.getaddrinfo('git.microide.com', 80)[0][-1]
      s = usocket.socket()
      s.connect(addr)
      s.send(bytes('GET /%s HTTP/1.0\r\nHost: git.microide.com\r\n\r\n' % file, 'utf8'))
      data=s.readline()
      if prt:
        print("- connected ",end='')
      while not '200 OK' in data: #Locks up the function - on purpose
        data=s.readline()
      while data != b'\r\n':
        data=s.readline()
      if prt:
        print(" - found ",end='')
      f=open(file,'w')
      while True:
        data = s.recv(4096)
        if data:
          f.write(data)
        else:
          break
      f.close()
      if prt:
        print(" - saved!")
      s.close()
      
def factory_reset():
    #Erase all non-essential files and full reset of the chip
    _tree=tree()
    _tree.sort(reverse=True)
    import os , machine
    for x in _tree:
      #.origs always get deleted - Some files are ignored
      if (not ('boot.py' in x or 'main.py' in x or 'config.json' in x)):
        try:
          os.remove(x) #File
        except:
          try:
            os.rmdir(x) #Folder
          except: 
            pass
    wget('microIDE.py',True)
    #Reset the chip
    machine.reset() 

def upgrade(ow = False):
	wget('chart.js.gz',ow)
	wget('mode-python.js.gz',ow)
	wget('mode-css.js.gz',ow)
	wget('mode-html.js.gz',ow)
	wget('mode-javascript.js.gz',ow)
	wget('mode-json.js.gz',ow)
	wget('worker-html.js.gz',ow)
	wget('worker-javascript.js.gz',ow)
	wget('worker-css.js.gz',ow)
	wget('worker-json.js.gz',ow)    

  
class webServer:

  _debug = True
  _lp = None
  _origFlag = '.orig' #Original document flag
  _port = 80
  _ap_en = True
  _size = 0
  _led = 22
  _lines = 30
  _mem = 0
  _boot=0
  _id = ''.join('{:02X}'.format(xx) for xx in bytearray(machine.unique_id()))
  def w_conf(self):
    import ujson
    f=open("config.json",'w')
    ujson.dump(self, f)
    f.close()
    
  def exists(self,f):
    #Test if a file exists and has a file size
    try:
      return os.stat(f)[6] >0
    except:
      return False
  def search(self,file):
    #Looks for a filename and returns the full path
    if len (file)<=2:
      return file
    for x in tree():
      if x.find(file.decode("utf-8"))>=0:
        return "/" + x
    return file

  
  def append(self,file,str):
    #Appends a line to the end of a file
    f=open(file,'a')
    f.write('{}\r\n'.format(str))
    #f.write(str)
    f.close()
    
  def _encrypt(self,inStr,key=None):
    try:
      padding = bytearray(int((len(inStr)+5)/16+1)*16)
      padding[0:len(inStr)]=bytearray(inStr, 'utf-8')
      from ucryptolib import aes
      from machine import unique_id
      if key==None:
        key = uos.urandom(10) + unique_id()
      container=aes(key,1)
      out=container.encrypt(padding[:16]);
      if len(inStr)>16:

        return  ''.join('{:02x}'.format(xx) for xx in out)  + self._encrypt(inStr[16:len(inStr)],key)
      else:
        return  ''.join('{:02x}'.format(xx) for xx in out) + ''.join('{:02x}'.format(xx) for xx in key[0:10]) + ''.join('{:02x}'.format(xx) for xx in uos.urandom(6))
    except:
      return None

  def _decrypt(self,inStr):
    try:
      from ubinascii import unhexlify
      from ucryptolib import aes
      from machine import unique_id
      padding = bytearray(20)
      padding[0:]=unhexlify(inStr[:32])

      key = unhexlify(inStr)[-16:-6] + unique_id()
      container=aes(key,1)
      out=container.decrypt(padding[:16]);
      if len(inStr)>64:
        return out.decode("utf-8")  + self._decrypt(inStr[32:len(inStr)])
      else:
        return out.decode("utf-8") 
    except:
      return None
    
      
  def touch(self,file):
    #Ensure a file exists
    f=open(file,'a')
    f.close()
  
  def wget_min(self,ow=False):
    wget('microIDE.py',ow)
    wget('microIDE.html',ow)
    wget('microIDE.js',ow)
    wget('ace.js.gz',ow)
    wget('favicon.ico.gz',ow)


  def _sendStatic(self,c_s,file):
    if file == b'/run.log' and self._lp == None:
      c_s.sendall(b'\r\n HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n')
      c_s.sendall(self.ioStr.getvalue())
      return True;
    else:
      return False;
      
  def _sendFile(self,c_s,file):
    f=open(file,'r')
    c_s.sendall(b'\r\n HTTP/1.1 200 OK\r\nConnection: close\r\nFilename:{}\r\n'.format(file.decode()))
    if '.gz' in file:
      c_s.sendall(b'Content-Encoding: gzip\r\nCache-control: max-age=3600\r\n')
    c_s.sendall(b'\r\n')
    tmp=f.read(2048)
    if '.py' in file and not file in tmp.encode('utf-8'): # File is python and file name is not in headers - adds comment header
      c_s.sendall(b'# microIDE Filename: {}\r\n'.format(file.decode()))
    while len(tmp)>1:
      gc.collect()
      c_s.sendall(tmp)
      tmp=f.read(2048)
    f.close()

  def _srvReq(self):
    c_s,client_addr = self.s.accept()
    cmd = c_s.readline()
    
    if "GET" in cmd:

      file=cmd[4:-11].split(b'?')[0]
      c_s.recv(4096) #dump the rest
      if self._sendStatic(c_s,file):
        pass #Static file is already sent
      elif exists(file+'.gz'):
        self._sendFile(c_s,file+'.gz')
      elif exists(file):
        self._sendFile(c_s,file)
      elif exists(self.search(file)):
        self._sendFile(c_s,self.search(file))
      elif file == b'/':
        c_s.sendall(b'\r\n HTTP/1.1 302 FOUND\r\nLocation:/microIDE.html\r\n')
      else:
        c_s.send(b"\r\n HTTP/1.1 404 Not Found\r\nConnection: close\r\nCache-Control: no-cache\r\n\r\n")
      
      
    elif "PUT" in cmd:
      file=cmd[4:-11].split(b'?')[0]
      line = c_s.readline()
      while not "Content-Length:" in line:
        line = c_s.readline()
      length=int(line.split(b':')[1])
      if self._debug:
      	print("Upload:",file,"-",length,"B")
      while len(line)>2:
        line = c_s.readline()
      content=c_s.recv(length)
      if len(content) == length and not '.gz' in file and not self._origFlag in file:
        try:
          if self._debug:
          	print("Received",len(content),"B saving")
          c_s.sendall(b'\r\n HTTP/1.1 200 OK\r\nConnection: close\r\n')
          if exists(file) and not exists(file+self._origFlag) :
            os.rename(file,file+self._origFlag)
          f=open(file,'w')
          f.write(content)
          f.close()
        except:
          c_s.sendall(b'\r\n HTTP/1.1 500 Write Failure\r\nConnection: close\r\n')
      else:
        if self._debug:
        	print("Received",len(content),"B error")
        c_s.sendall(b'\r\n HTTP/1.1 500 Write Failure\r\nConnection: close\r\n')
      c_s.sendall(b'\r\n')
    
    elif "CMD" in cmd:
      file=cmd[4:-11].split(b'?')[0]
      print("CMD: ",file)
      try:
        exec(open("." + run_cmd.decode("utf-8"),'r'),globals())
        c_s.write('HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n')
      except:
		c_s.send(b"HTTP/1.1 500 Failure\r\nConnection: close\r\nCache-Control: no-cache\r\n\r\n")
    
    elif "RST" in cmd:
      import machine
      machine.reset()

      

    elif "LOG" in cmd:
      c_s.recv(4096) #dump the rest
      c_s.write('HTTP/1.1 200 OK\r\nConnection: close\r\nCache-Control: no-cache\r\n\r\n{}'.format(self.ioStr.getvalue()))

      
    elif "DIR" in cmd:
      line = c_s.readline()
      while (not "Time:" in line) and (len(line)>5):
        line = c_s.readline()
      if "Time:" in line:
        epoch(int(line.split(b': ')[1][:-5]))
      c_s.recv(4096) #dump the rest
      from machine import reset_cause
      c_s.write('HTTP/1.1 200 OK\r\nConnection: close\r\nCache-Control: no-cache\r\n\r\n')
      c_s.write('Running parameters at {} unix epoch\r\n'.format(epoch()))
      c_s.write('Disk: {} / {} kb = {} % used\r\n'.format((os.statvfs("/")[2]-os.statvfs("/")[3])*os.statvfs("/")[1]/1000,os.statvfs("/")[2]*os.statvfs("/")[1]/1000,(os.statvfs("/")[2]-os.statvfs("/")[3])/os.statvfs("/")[2]*100.0))
      gc.collect()
      c_s.write('Mem: {} / {} kb = {} % used\r\n'.format(gc.mem_alloc()/1000,self._mem/1000,gc.mem_alloc()/self._mem*100.0))
      c_s.write('RST: {}\r\n'.format(reset_cause()))
      c_s.write('ID: {};\r\n'.format(self._id))
      gc.collect()
      for x in tree():
        if not (self._origFlag in x): #or 'microIDE' in x or '.gz' in x)  :
          if os.stat(x)[0]==32768: #File
            c_s.write('{},{},{};\r\n'.format(x,os.stat(x)[6],exists(x+self._origFlag)))
          else:
            c_s.write('{},DIR,{};\r\n'.format(x,exists(x+self._origFlag)))


    else:
      if self._debug:
        print("CODE 501")
        print(client_addr)
      	print(cmd)
      	print(c_s.recv(4096))
      c_s.send(b"\r\nHTTP/1.1 501 Not Implemented\r\nConnection: close\r\nCache-Control: no-cache\r\n\r\n")
    c_s.close()
    gc.collect()

  def _flush(self,init=False):
  	if init:
  	  self.ioStr=StringIO()
  	  if self._lp != None:
	    f=open(self._lp,'w')
	    f.write("Begin file logging at unix epoch: {}\r\nLog Start\r\n".format(epoch()))
	    f.close()
  	  else:
	    self.ioStr.write("Begin ram logging at unix epoch: {}\r\nLog Start\r\n".format(epoch()))   
  	else:
  	  os.dupterm(None)
  	  if self._lp != None:
	      f=open(self._lp,'a')
	      f.write(self.ioStr.getvalue())
	      f.close()
	      self.ioStr.close()
	      gc.collect()
	      self.ioStr=StringIO()
  	  else:
	      if len(self.ioStr.getvalue().split('\n'))>self._lines:
	        lines=self.ioStr.getvalue().split('\n')
	        self.ioStr.close()
	        gc.collect()
	        self.ioStr=StringIO()
	        self.ioStr.write("Boot: {} / Cleared: {}\r\n--------------------------------------\r\n".format(self._boot,epoch()))   
	        for i in range(4, len(lines)):
	          self.ioStr.write("{}".format(lines[i]))
	os.dupterm(self.ioStr)
  	gc.collect()
  	
  def _run(self):
    while True:
      try:
        self._srvReq()
      except KeyboardInterrupt:
        self.s.close()
        return
      except:
        pass
        
      try:
        self._flush()
      except:
        pass
        
      gc.collect()
      
      
  def __init__(self) :
    gc.collect()
    self._mem=gc.mem_alloc()+gc.mem_free()
    self._boot=epoch()
    self.s = usocket.socket()
    self._flush(True)
    
    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(10)
    self.s.settimeout(0)
    self.touch('boot.py')
    self.touch('main.py')
    _thread.start_new_thread(self.wget_min,())
    _thread.start_new_thread(self._run,())

webServer()
del webServer