#!/usr/bin/python3
import enum, socket, traceback, subprocess

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 = 'ake.crabdance.com'
       self.port = 70
       self.path = ''

       self.history = []

   def go(self, mode=None, host=None, port=None, path=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:
           query = input('Search 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:
           traceback_exc()
           print('Failed to fetch data')
           return None

class Navigator(Fetcher):
   def __init__(self):
       super().__init__()
       self.links = []
   def goto(self, num):
       if num <= 0 or num > len(self.links):
           print('\tWrong link number')
       else:
           self.go(*self.links[num - 1])
   def display(self):
       if self.mode == MenuType.BIN:
           print('Enter filename or leave empty to ignore')
           filename = input('Save as: ')
           if filename == '':
               return
       elif 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 self.mode == MenuType.DOC:
           print(data.decode('utf-8'))
       elif self.mode == MenuType.BIN:
           with open(filename, 'wb') as f:
               f.write(data)
       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:
                   print('\t{}'.format(itext))
               elif itype == MenuType.DOC:
                   self.links.append((itype, item[2], item[3], item[1]))
                   print('{}[{}]\t{}{}{}'.format(
                       ESC_WHITE,
                       len(self.links),
                       ESC_BLUE,
                       itext,
                       ESC_RESET
                   ))
               elif itype == MenuType.MENU:
                   self.links.append((itype, item[2], item[3], item[1]))
                   print('{}[{}]\t{}{}{}'.format(
                       ESC_WHITE,
                       len(self.links),
                       ESC_WHITE,
                       itext,
                       ESC_RESET
                   ))
               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]))
                   print('{}[{}]\t{}{}{}'.format(
                       ESC_WHITE,
                       len(self.links),
                       ESC_GREEN,
                       itext,
                       ESC_RESET
                   ))
               elif itype == MenuType.QUERY:
                   self.links.append((itype, item[2], item[3], item[1]))
                   print('{}[{}]\t{}{}{}'.format(
                       ESC_WHITE,
                       len(self.links),
                       ESC_CYAN,
                       itext,
                       ESC_RESET
                   ))
               elif itype == MenuType.HTML:
                   self.links.append((itype, item[2], item[3], item[1]))
                   print('{}[{}]\t{}{}{}'.format(
                       ESC_WHITE,
                       len(self.links),
                       ESC_RED,
                       itext,
                       ESC_RESET
                   ))
               else:
                   print('\tUnknown type: {}'.format(itype))
       else:
           print('Not yet implemented')

def display_help():
   print('''Omsk subway -- commandline gopher navigator
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Commands:

exit       -- exit program
help       -- view this help
back       -- return to previous menu
index      -- return to server index
@<host>    -- request server's index
<selector> -- request menu by selector on current
             server
''')

if __name__ == '__main__':
   navi = Navigator()
   omit_display = False
   while True:
       try:
           if not omit_display:
               navi.display()
           else:
               omit_display = False
       except:
           traceback.print_exc()
           print('\tFailed to render')
       command = input('{}{} {}{}> {}'.format(ESC_WHITE, navi.host, ESC_CYAN, navi.path, ESC_RESET))
       if command == '':
           continue
       elif command == 'help' or command == '?':
           display_help()
           omit_display = True
       elif command == 'exit':
           break
       elif command == 'back':
           navi.back()
       elif command == 'index':
           navi.go(path='')
       elif command[0] == '@':
           try:
               host, port = command[1:].split(':')
               navi.go(host=host, port=port, path='', mode=MenuType.MENU)
           except ValueError:
               navi.go(host=command[1:], path='', mode=MenuType.MENU)
       elif command.isdigit():
           navi.goto(int(command))
       else:
           navi.go(path=command, mode=MenuType.MENU)