#!/usr/bin/python3
import enum, socket, traceback, subprocess, curses
from curses.textpad import Textbox, rectangle

class MenuType(str, enum.Enum):
   DOC = '0'
   MENU = '1'
   QUERY = '7'
   BIN = '9'
   HTML = 'h'
   INFO = 'i'
   SOUND = 's'
   IMAGE = 'I'
   GIF = 'g'

ESC_WHITE = '\033[1;37m'
ESC_CYAN  = '\033[1;36m'
ESC_RESET = '\033[0m'
ESC_RED   = '\033[1;31m'
ESC_GREEN = '\033[1;32m'
ESC_YELLO = '\033[1;33m'
ESC_BLUE  = '\033[1;34m'

class Fetcher(object):
   def __init__(self):
       self.mode = MenuType.MENU
       self.host = 'republic.circumlunar.space'
       self.port = 70
       self.path = '/~ake'

       self.history = []

   def go(self, mode=None, host=None, port=None, path=None, query=None):
       self.history.append((
           self.mode, self.host, self.port, self.path
       ))
       if mode is not None:
           self.mode = mode
       if host is not None:
           self.host = host
       if port is not None:
           self.port = int(port)
       if path is not None:
           self.path = path
       if mode == MenuType.QUERY:
           self.path += '\t{}'.format(query)

   def back(self):
       try:
           mode, host, port, path = self.history.pop()
           self.mode = mode
           self.host = host
           self.port = port
           self.path = path
       except:
           print('No entries in history')

   def fetch(self):
       try:
           s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
           s.connect((self.host, self.port))
           s.sendall('{}\r\n'.format(self.path).encode('utf-8'))
           resp = b''
           while True:
               chunk = s.recv(1024)
               if not chunk:
                   break
               resp += chunk
           return resp
       except:
           return None

class Navigator(Fetcher):
   def __init__(self):
       super().__init__()
       self.links = []
       self.buffer = []
   def goto(self, num):
       if num <= 0 or num > len(self.links):
           print('\tWrong link number')
       else:
           self.go(*self.links[num - 1])
   def download(self, filename):
       data = self.fetch()
       with open(filename, 'wb') as f:
           f.write(data)

   def render(self):
       self.buffer = []
       if self.mode == MenuType.HTML:
           if self.path.lower().startswith('url:'):
               subprocess.call(['/usr/bin/xdg-open', self.path[4:]])
           else:
               subprocess.call(['/usr/bin/lynx', 'gopher://{}:{}/0{}'.format(self.host, self.port, self.path)])
           self.back()
           return

       data = self.fetch()
       if data is None:
           self.buffer = 'Failed to display gopher://{}:{}/{}{}\n{}'.format(
               self.host, self.port, self.mode, self.path, traceback.format_exc()
           ).split('\n')
           self.mode = MenuType.DOC
           return
       if self.mode == MenuType.DOC:
           self.buffer = data.decode('utf-8').split('\n')
       elif self.mode == MenuType.MENU or self.mode == MenuType.QUERY:
           self.links = []
           items = [item.split('\t') for item in data.decode('utf-8').split('\n')]
           for item in items:
               if len(item) < 4:
                   continue
               itype = item[0][0]
               itext = item[0][1:]
               if itype == MenuType.INFO:
                   self.buffer.append((None, itext))
               elif itype == MenuType.DOC:
                   self.links.append((itype, item[2], item[3], item[1]))
                   self.buffer.append((
                       len(self.links),
                       itext, itype
                   ))
               elif itype == MenuType.MENU:
                   self.links.append((itype, item[2], item[3], item[1]))
                   self.buffer.append((
                       len(self.links),
                       itext, itype
                   ))
               elif itype == MenuType.BIN or itype == MenuType.SOUND or itype == MenuType.IMAGE or itype == MenuType.GIF:
                   self.links.append((itype, item[2], item[3], item[1]))
                   self.buffer.append((
                       len(self.links),
                       itext, itype
                   ))
               elif itype == MenuType.QUERY:
                   self.links.append((itype, item[2], item[3], item[1]))
                   self.buffer.append((
                       len(self.links),
                       itext, itype
                   ))
               elif itype == MenuType.HTML:
                   self.links.append((itype, item[2], item[3], item[1]))
                   self.buffer.append((
                       len(self.links),
                       itext, itype
                   ))
               else:
                   self.buffer.append((None, 'Unknown type: {}'.format(itype)))
       else:
           self.buffer.append('\tNot yet implemented')

def main(stdscr):
   navi.render()
   start = 0
   link = 0
   key = None
   history = []
   pagesize = curses.LINES - 2
   curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK)
   curses.init_pair(2, curses.COLOR_MAGENTA, curses.COLOR_BLACK)
   curses.init_pair(3, curses.COLOR_BLUE, curses.COLOR_BLACK)
   curses.init_pair(4, curses.COLOR_CYAN, curses.COLOR_BLACK)
   while True:
       stdscr.clear()
       stdscr.addstr(0, 0, '[ gopher://{}:{}/{}{}'.format(navi.host, navi.port, navi.mode, navi.path), curses.A_BOLD)
       stdscr.addstr(0, curses.COLS - 1, ']')
       stdscr.refresh()
       first = None
       last = None
       for i in range(min(pagesize, len(navi.buffer) - start)):
           color = curses.A_NORMAL
           if navi.mode == MenuType.MENU or navi.mode == MenuType.QUERY:
               text = navi.buffer[i + start][1]
               color = curses.A_BOLD
               if navi.buffer[i + start][0] is not None:
                   if first is None:
                       first = navi.buffer[i + start][0] - 1
                   last = navi.buffer[i + start][0] - 1
                   linktype = navi.buffer[i + start][2]
                   if linktype == MenuType.DOC:
                       color |= curses.color_pair(1)
                   elif linktype == MenuType.BIN:
                       color |= curses.color_pair(2)
                   elif linktype == MenuType.MENU:
                       color |= curses.color_pair(3)
                   elif linktype == MenuType.QUERY:
                       color |= curses.color_pair(4)
                   if navi.buffer[i + start][0] == link + 1:
                       color |= curses.A_REVERSE
                       text = navi.buffer[i + start][1]
                       # stdscr.addstr(i + 1, 0, '> ')

           else:
               text = navi.buffer[i + start]
           try:
               stdscr.addstr(i + 1, 2, text, color)
           except:
               pass
       stdscr.move(curses.LINES - 1, 0)
       stdscr.refresh()
       key = stdscr.getkey()
       if key == 'KEY_PPAGE':
           start = max(0, start - pagesize)
       elif key == 'KEY_NPAGE':
           start = start + pagesize
       elif key == 'KEY_RIGHT' or key == '\n':
           if navi.links[link][0] == MenuType.QUERY:
               curses.echo()
               stdscr.addstr(curses.LINES - 1, 0, 'Query: ')
               s = stdscr.getstr(curses.LINES - 1, 7).decode('utf-8')
               curses.noecho()
               navi.go(*navi.links[link], query=s)
           elif navi.links[link][0] == MenuType.BIN:
               curses.echo()
               stdscr.addstr(curses.LINES - 1, 0, 'Filename: ')
               filename = stdscr.getstr(curses.LINES - 1, 10).decode('utf-8')
               curses.noecho()
               if filename != '':
                   navi.go(*navi.links[link])
                   navi.download(filename)
                   navi.back()
           else:
               navi.goto(link + 1)
           history.append((start, link))
           link = 0
           start = 0
           navi.render()
       elif key == 'KEY_LEFT':
           navi.back()
           if len(history) > 0:
               start, link = history.pop()
           navi.render()
       elif key == 'KEY_UP':
           if navi.mode == MenuType.DOC or last is None or link == 0:
               start = max(0, start - pagesize)
           elif link > 0:
               link -= 1
               if link < first:
                   start = max(0, start - pagesize)
       elif key == 'KEY_DOWN':
           if navi.mode == MenuType.DOC or last is None:
               start += pagesize
           elif link < len(navi.links):
               link += 1
               if link > last:
                   start += pagesize
       elif key == 'g':
           curses.echo()
           stdscr.addstr(curses.LINES - 1, 0, 'Host: ')
           s = stdscr.getstr(curses.LINES - 1, 6).decode('utf-8')
           curses.noecho()
           parts = s.split(':')
           navi.go(host=parts[0], port=70 if len(parts) < 2 else int(parts[1]), path='', mode=MenuType.MENU)
           history.append((start, link))
           link = 0
           start = 0
           navi.render()
       elif key == 'q':
           break
#        elif str(key).isdigit():
#           stdscr.addstr(curses.LINES - 1, 0, key)
#           curses.echo()
#           s = str(key) + stdscr.getstr(curses.LINES - 1, 1)
#           navi.goto()
#           curses.noecho()


if __name__ == '__main__':
   navi = Navigator()
   curses.wrapper(main)