--
--   Version 1.1
--
--   Copyright 2016-2018 Lee Djavaherian
--
--   This file is part of TrillSat.
--
--   TrillSat 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.
--
--   TrillSat 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 TrillSat.  If not, see <http://www.gnu.org/licenses/>.
-----------------------------------------------------------------------------------

srv=net.createServer(net.TCP)
espbot_status = 'Just started' -- show mode, however is always set to '' (available) - never set status to blank

uart.setup(0, 9600, 8, 0, 1, 0)
-- don't use multiple conn:send in series, since they can be sent out of order
netmode = 'CLIENT'

-- must stop using minicom terminal when testing
-- since it interferes with serial transfer to Pi
-- and must turn off prints (since they also go to serial)

function client_connect()
     netmode = 'CLIENT'
     wifi.ap.dhcp.stop()
     wifi.setmode(wifi.STATION)
     wifi.sta.config(routerssid,routerpass)
     wifi.sta.autoconnect(1)
end

-- the AP connect mode doesn't work with Android 6.0 since it requires a DNS server to connect
-- am trying to find workaround
function ap_connect()
     netmode = 'AP'
     wifi.sta.autoconnect(0)
     wifi.sta.disconnect()
     cfg={}
     cfg.ip=espip 
     cfg.netmask="255.255.255.0"
     cfg.gateway=espip
     wifi.setmode(wifi.SOFTAP)
     wifi.setphymode(wifi.PHYMODE_N)
     wifi.ap.setip(cfg)
     cfg={}
     cfg.ssid=espssid
     cfg.pwd=esppass 
     cfg.auth=AUTH_WPA2_PSK
     cfg.channel = 6
     cfg.hidden = 0
     wifi.ap.config(cfg)
     cfg={}
     cfg.start=phoneip 
     wifi.ap.dhcp.config(cfg)
     wifi.ap.dhcp.start()
end


function change_net_mode(level)
  if netmode == 'AP' then
    client_connect()
  else
    ap_connect()
  end
  tmr.delay(250*1000)
end

-- this is gpio 12 pi power
-- keep in float mode on startup
gpio.mode(6, gpio.INPUT, gpio.FLOAT)

-- this is gpio 14 light
-- sink to keep light off on boot
gpio.mode(5, gpio.OUTPUT, gpio.FLOAT)
gpio.write(5, gpio.LOW)

-- this is actually gpio 0 (but nodemcu maps to its own IO index number)
gpio.mode(3, gpio.INT)
gpio.trig(3, 'both', change_net_mode) -- however, no pullup here 

uart_commands = ""
pingflag = 0

srv:listen(xmppport,function(conn)

-- XMPP PING VALIDATOR
-- every 5 minutes, see if pingflag has been cleared at least once by other functions
-- is leveraging the fact that Xabber already sends pings

-- if unsuccesful check, do a manual ping before going to AP mode
-- if successful check, set pingflag for another cycle

tmr.alarm(0, 300000, tmr.ALARM_AUTO, function()
   if netmode == 'CLIENT' then
       if pingflag == 1 then
            -- print ('doing final ping')
            send_ping() -- send my own ping to Xabber
            tmr.delay(1000*1000) -- allow enough time for quick response

            if pingflag == 1 then
              -- set status to away and go to AP mode
              -- print('changing to ap mode')
              if presence ~= 'away' then
                uart.write(0, "PRESENCE: away\n")
                presence = 'away'
              end
              ap_connect()
              -- also add code to set presence to away
              pingflag = 0
            else
              pingflag = 1
            end

       else
         pingflag = 1 -- if successful, set flag for another 5 minutes
       end
   end
end)

  function send_ping()
    conn:send ([[<iq from='esp' to=']] .. xmppusername .. [[@esp/MyResource' id='sldjfd' type='get'><ping xmlns='urn:xmpp:ping'/></iq>]])
  end

  function send_xmpp(msg) 
    conn:send([[<message to=']] .. xmppusername .. [[@esp' from='espbot@esp/MyResource' type='chat' xml:lang='en'><body>]] .. msg .. [[</body></message>]])
  end
  
  function pres_line(pres_text) -- show is assumed to be "", which means available
     return [[<presence from='espbot@esp/MyResource' to=']] .. xmppusername .. [[@esp/MyResource' xml:lang='en'><show></show><status>]] .. pres_text .. [[</status></presence>]]
  end   
  
  
  -- RECEIVE COMMAND FROM PI, use \n as delimiter
  -- Pi can set the presence status for espbot@esp
  -- for some reason, a .5 to 1 second delay between messages and presence from bot must be added
  -- I don't know yet if this is a problem with bot threading or with the xmpp timing on esp
  uart.on("data", 0,
    function(data)
       uart_commands = uart_commands .. data  -- accumuate global buffer until it gets at least 1 command (maybe more)
       local command_piece = ""
         for i=1, string.len(uart_commands) do
            local letter = string.sub(uart_commands, i, i)
            if letter ~= "\n" then
               command_piece = command_piece .. letter -- try to accumuate a single command
            else
              -- now you have a full command, so process it
              _, _, pi_command, pi_value = string.find(command_piece, "^.-(.-: )(.-)$")

              if command_piece == "esp sleep" then
                 send_xmpp("Pi wants me to go to sleep")

              -- status_command cannot be blank or concat .. will crash later
              elseif pi_command == 'STATUS: ' then
                 -- send_xmpp("Status received from Pi")
                 conn:send (pres_line(pi_value))
                 espbot_status = pi_value
              elseif pi_command == 'SET_TIME: ' then
                 -- send_xmpp("Time received from Pi")
                 rtctime.set(tonumber(pi_value), 0) -- set seconds since epoch as set by Pi
              elseif pi_command == 'GET_TIME: ' then
                 epoch_secs, _ = rtctime.get()
                 uart.write(0, "SET_TIME: " .. epoch_secs .. "\n") -- now send time back to Pi
              else
                send_xmpp("Pi said: " .. command_piece)
              end
              command_piece = "" -- now erase that command since you processed it

            end   
         end   
         uart_commands = command_piece -- rest is put in buffer for next time
          
  end, 0)

  -- received from user
  conn:on("receive",function(conn,payload)
   -- print(payload) -- only turn this on to view responses for testing.  It interferes with Pi Zero.

      
   -- OPENING STREAMS AND ENCRYTPION MECHANISM (PLAIN) - make this fuzzier
   if payload == [[<stream:stream xmlns='jabber:client' to='esp' xmlns:stream='http://etherx.jabber.org/streams' version='1.0' xml:lang='en'>]] then
      -- print('received opening stream')
      conn:send ([[<?xml version='1.0'?><stream:stream xmlns:stream='http://etherx.jabber.org/streams' version='1.0' from='esp' id='12345678' xml:lang='en' xmlns='jabber:client'>]] .. "\n".. [[<stream:features><mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><mechanism>PLAIN</mechanism></mechanisms></stream:features>]])
   end

   -- encoder does not exist on NodeMCU .96, and it doesn't work in latest version I tried, and crypto doesn't have a base64 decoder, only an encoder
   -- PASSWORD AUTHORIZATION
   -- xmlns='urn:ietf:params:xml:ns:xmpp-sasl"
   _, _, tag, password = string.find(payload, "^<(auth).+>(.-)</auth>") -- does a double-capture, very convenient
   if tag == "auth" then
      if password == xmpppassword then
        -- print("authorized")
        conn:send ([[<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl'></success>]])
      else         
        conn:send ([[<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl'></failure>]])
      end

   -- OPEN NORMAL STREAMS  -- make this fuzzier
   elseif payload == [[<stream:stream xmlns='jabber:client' to='esp' xmlns:stream='http://etherx.jabber.org/streams' version='1.0' id='12345678' xml:lang='en'>]] then
     -- print('normal stream initiated')
     conn:send ([[<?xml version='1.0'?><stream:stream xmlns:stream='http://etherx.jabber.org/streams' version='1.0'> from='esp' id='12345678' xml:lang='en' xmlns='jabber:client'>]] .. "\n" .. [[<stream:features><c hash='sha-1' ver='1.0' node='http://esp' xmlns='http://jabber.org/protocol/caps'/><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/></stream:features>]])
     
     -- xabber won't work without bind statement
     -- <session xmlns='urn:ietf:params:xml:ns:xmpp-session'><optional/></session>
     -- <ver xmlns='urn:xmpp:features:rosterver'/>
     
   end


   -- BIND TO RESOURCE if Xabber asks via set, and also send espbot@esp initial presence
   -- makes the assumption that the value in <resource> tags is set to "MyResource"
   -- xmlns='urn:ietf:params:xml:ns:xmpp-bind"
   if payload:match("<bind ") ~= nil then
     _, _, id = string.find(payload, "id=\'(.-)\' type=\'set\'")
     -- print ('binding MyResource resource to id: ' .. id)
     conn:send ([[<iq id=']] .. id .. [[' type='result'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><jid>]] .. xmppusername .. [[@esp/MyResource</jid></bind></iq>]] .. pres_line(espbot_status))
     -- print('initial presence sent')
  
     -- now send presence every 30 seconds - not really needed
     -- tmr.alarm(1, 30000, tmr.ALARM_AUTO, function()
     --    conn:send(pres_line(espbot_status))
     -- end)
     
   end

   -- RECEIVE PRESENCE (SHOW ONLY, NOT STATUS) FROM xmppusername@esp (XMPP) to tell server (away, chat, dnd, xa, and "" is available)
   if payload:match("<presence ") ~= nil then
     _, _, pres = string.find(payload, "<show>(.-)</show>")
     if pres ~= nil then
        if pres ~= presence then
          -- now tell bot, too
          uart.write(0, "PRESENCE: " .. pres .. "\n")
          presence = pres -- to make sure repeat xabber messages aren't sent to bot
        end
     end
   end

   -- SUPPLY A ROSTER when xmppusername@esp in Xabber asks via get (it seems to require roster)
   -- pattern = "<query xmlns=\"jabber:iq:roster"
   
   if payload:match("roster") ~= nil then  
     _, _, id = string.find(payload, "id=\'(.-)\' type=\'get\'")
     -- print ('roster requested for id: ' .. id)
     conn:send ([[<iq id=']] .. id .. [[' type='result' to=']] .. xmppusername .. [[@esp/MyResource'><query xmlns='jabber:iq:roster'><item jid='espbot@esp' name='espbot' subscription='both'/></query></iq>]])   
   end

   -- RETURN XMPP PINGS FROM XABBER (prevents it from disconnecting you!)
   if payload:match("<ping ") ~= nil then  
     _, _, id = string.find(payload, "id=\'(.-)\' type=\'get\'")
     -- print('returning ping')
     conn:send ([[<iq from='esp' to=']] .. xmppusername .. [[@esp/MyResource' id=']] .. id .. [[' type='result'></iq>]])

     -- SEND PERIODIC PRESENCE -  a good time to send presence again, since network is already active
     -- print('also sending a presence')
     conn:send(pres_line(espbot_status))

     -- ALSO CLEAR PING FLAG (my own connection checking)
     pingflag = 0
   end

   -- these 2 service-unavailable messages may be worse - Xabber doesn't seem to respond well
   pattern = "disco:text"
   if payload:match(pattern) ~= nil then  
     _, _, id = string.find(payload, "id=\'(.-)\' type=\'get\'")
     conn:send ([[<iq from='esp' to=']] .. xmppusername .. [[@esp/MyResource' id=']] .. id .. [[' type='error'><service-unavailable/></iq>]])   
   end
   
   pattern = "<vCard"
   if payload:match(pattern) ~= nil then  
     _, _, id = string.find(payload, "id=\'(.-)\' type=\'get\'")
     conn:send ([[<iq from='esp' to=']] .. xmppusername .. [[@esp/MyResource' id=']] .. id .. [[' type='error'><service-unavailable/></iq>]]) 
   end
   
   -- RECEIVE COMMAND FROM xmppusername@esp in Xabber
   if payload:match("message") ~= nil then
      -- add better filtering since some xabber messages are slipping through as commands
      _, _, phonemessage = string.find(payload, "<body>(.-)</body>")
      -- first, intercept any messages meant only for ESP
      if phonemessage == "esp hi" then
         send_xmpp("Hi, this is the esp responding.")        
      elseif phonemessage == "switchnet" then
         if netmode == 'AP' then
           client_connect()
         else
           ap_connect()
         end
      elseif phonemessage == "restart" then
         node.restart()
      elseif phonemessage == "pres" then
         conn:send(pres_line('Hello'))
      elseif phonemessage == "pres2" then
         conn:send(pres_line('There'))
      elseif phonemessage == "pi on" then
         -- this is really gpio 12, but index is 6
         gpio.mode(6, gpio.OUTPUT, gpio.FLOAT)
         gpio.write(6, gpio.HIGH)
      elseif phonemessage == "pi off" then
         gpio.mode(6, gpio.INPUT, gpio.FLOAT)  
      elseif phonemessage == "esp light on" then
         -- this is really gpio 14, but index is 5
         -- dont make high or it will overcurrent through transistor!
         -- only comes on if hall and attiny are not sinking
         gpio.mode(5, gpio.INPUT, gpio.FLOAT)
      elseif phonemessage == "esp light off" then
         -- sink to keep LED transistor off
         gpio.mode(5, gpio.OUTPUT, gpio.FLOAT)
         gpio.write(5, gpio.LOW)
      elseif phonemessage == "blink" then
         gpio.mode(3, gpio.OUTPUT) -- pins on NodeMCU different from board!
         gpio.write(3, gpio.LOW) -- ON
         tmr.delay(1000000)
         gpio.write(3, gpio.HIGH) -- OFF

         -- reset input triggers for button presses
         gpio.mode(3, gpio.INT)
         gpio.trig(3, 'both', change_net_mode) -- however, no pullup here 

      else
         -- command is meant for Pi, pass directly
         uart.write(0, phonemessage .. "\n") 
      end
   -- check for my timed ping response from xmppusername@esp in Xabber
   elseif payload == [[<iq to='esp' id='sldjfd' type='result'></iq>]] then
      -- tell my timer ping code that xabber is responding by clearing ping flag
      pingflag = 0

   elseif payload == [[</stream:stream>]] then
     -- print('disconnecting...')
     conn:send ([[</stream:stream]])
     conn:close()
   end
  end)
  -- conn:on("sent",function(conn) conn:close() end)
end)
