import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, Pango, GLib
import threading
import time
import re
import queue
from io import BytesIO
import speech_recognition as sr
import g4f
import pygame
from gtts import gTTS
import sounddevice


NAME = 'Lucy'
RESP = 'Yes?'
BYE = 'Talk to you later.'
MAX_CONTEXT = 32
MAX_INACTIVE = 60


pygame.init()
q = queue.Queue()
r = sr.Recognizer()

get_sentence = False
sentence = ''
output_done = False
speech_done = True
chat_data = []
active_ts = 0;


class ChatView(Gtk.TextView):
  def __init__(self):
    Gtk.TextView.__init__(self)
    self.set_wrap_mode(Gtk.WrapMode.WORD)
    self.set_editable(False)
    self.set_cursor_visible(False)
    text_buffer = self.get_buffer()
    text_iter_end = text_buffer.get_end_iter()
    self.text_mark_end = text_buffer.create_mark("", text_iter_end, False)

  def append_text(self, text):
    text_buffer = self.get_buffer()
    text_iter_end = text_buffer.get_end_iter()
    text_buffer.insert_markup(text_buffer.get_end_iter(), text, -1)
    self.scroll_to_mark(self.text_mark_end, 0, False, 0, 0)

  def clear_text(self):
    text_buffer = self.get_buffer()
    text_iter_start = text_buffer.get_start_iter()
    text_iter_end = text_buffer.get_end_iter()
    text_buffer.delete(text_iter_start, text_iter_end);


class LucyWindow(Gtk.Window):
  active = False;
  listening = False;
  chat_view = ChatView()

  def __init__(self):
    Gtk.Window.__init__(self)
    self.set_title('Lucy')
    self.fullscreen()

    self.set_default_size(640, 360)
    self.grid = Gtk.Grid()
    self.scrolled_win = Gtk.ScrolledWindow()

    self.scrolled_win.set_hexpand(True)
    self.scrolled_win.set_vexpand(True)
    self.scrolled_win.add(self.chat_view)
    self.scrolled_win.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)

    text_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=20)
    text_box.set_margin_top(20)
    text_box.set_margin_bottom(20)
    text_box.set_margin_start(20)
    text_box.set_margin_end(20)
    text_box.add(self.scrolled_win)

    self.grid.add(text_box)
    self.add(self.grid)
    self.connect('destroy', Gtk.main_quit)
    self.show_all()

  def set_state(self, active, listening):
    self.active = active
    self.listening = listening

    window_context = self.get_style_context()
    window_context.remove_class('inactive')
    window_context.remove_class('active')
    window_context.remove_class('listening')

    view_context = self.chat_view.get_style_context()
    view_context.remove_class('inactive')
    view_context.remove_class('active')
    view_context.remove_class('listening')

    if active:
      if listening:
        window_context.add_class('listening')
        view_context.add_class('listening')
      else:
        window_context.add_class('active')
        view_context.add_class('active')
    else:
      window_context.add_class('inactive')
      view_context.add_class('inactive')


class QueueProcessingThread(threading.Thread):
  window = None

  def __init__(self, win):
    threading.Thread.__init__(self)
    self.window = win
    self.daemon = True

  def speak(self, txt):
    global active_ts
    if(txt and txt.strip()):
      active_ts = time.time()
      mp3_file_object = BytesIO()
      speech = gTTS(text=txt, slow=False, lang='en', tld='us')
      speech.write_to_fp(mp3_file_object)
      mp3_file_object.seek(0)
      pygame.mixer.music.load(mp3_file_object, 'mp3')
      pygame.mixer.music.play()
      while(pygame.mixer.music.get_busy()): pass    # wait until playing done
      active_ts = time.time()

  def run(self):
    global get_sentence
    global sentence
    global output_done
    global speech_done
    while True:
      if (get_sentence):
        item = q.get()
        sentence += item
        q.task_done()
        if item.endswith(".") or item.endswith("!") or item.endswith("?") or (output_done and q.empty()):
          self.speak(sentence)
          sentence = ''
          get_sentence = False
      else:
        if q.empty():
          if output_done:
            if not speech_done:
              speech_done = True
              if self.window.active:
                self.window.set_state(True, True)
        else:
          if output_done:
            get_sentence = True


class VoiceRecognizingThread(threading.Thread):
  window = None

  def __init__(self, win):
    threading.Thread.__init__(self)
    self.window = win
    self.daemon = True

  def run(self):
    global get_sentence
    global output_done
    global speech_done
    global chat_data
    global active_ts

    while True:
      Gtk.main_iteration_do(False)
      try:
        with sr.Microphone(sample_rate=44100) as mic:

          if not self.window.active and not self.window.listening:
              self.window.set_state(False, False);

          if not speech_done:
              continue

          ts = time.time()
          if self.window.active and active_ts and (ts - active_ts) > MAX_INACTIVE :
              active_ts = 0
              self.window.set_state(False, False)
              speech_done = False
              q.put(BYE)
              get_sentence = True
              output_done = True
              GLib.idle_add(self.window.chat_view.clear_text)

          voice = r.listen(mic)
          txt = r.recognize_google(voice)
          active_ts = ts

          if not self.window.active:
            if NAME in txt:
                self.window.set_state(True, False)
                speech_done = False
                q.put(RESP)
                get_sentence = True
                output_done = True
          else:
            active_ts = ts;
            output_done = False
            speech_done = False
            GLib.idle_add(self.window.chat_view.append_text, '\n\nQ: ' + txt + '\nA: ')
            self.window.set_state(True, False)

            chat_data.append({'role': 'user', 'content': txt})

            resp = g4f.ChatCompletion.create(
                model=g4f.models.gpt_4,
                provider=g4f.Provider.You,
                messages=chat_data,
                stream=True,
            )

            answer = ''
            for message in resp:
                msg = re.sub('[^A-Za-z0-9 ,.:_\'\"\+\-\*\/=]+', '', message.replace('**', ''))
                GLib.idle_add(self.window.chat_view.append_text, msg)
                answer += msg
                q.put(msg)
                if msg.endswith("."):
                    get_sentence = True

            output_done = True
            chat_data.append({'role': 'assistant', 'content': answer})

            if len(chat_data) > MAX_CONTEXT:
                del chat_data[0]
                del chat_data[0]

            active_ts = time.time()

      except sr.UnknownValueError as ue:
        output_done = True


if __name__ == '__main__':

  # load CSS
  screen = Gdk.Screen.get_default()
  provider = Gtk.CssProvider()
  style_context = Gtk.StyleContext()
  style_context.add_provider_for_screen(
      screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
  )
  css = b"""
  textview {
      font: 25px Arial;
      background: transparent;
  }
  textview text {
      color: white;
      background: transparent;
  }
  textview.inactive text {
      color: black;
  }
  window.inactive {
      background: black;
  }
  window.active {
      background: #7700df;
  }
  window.listening {
      background: #008c8c;
  }
  """
  provider.load_from_data(css)

  # Lucy window
  win = LucyWindow()

  # voice recognizing thread
  thread1 = VoiceRecognizingThread(win)
  thread1.start()

  # queue processing thread
  thread2 = QueueProcessingThread(win)
  thread2.start()

  Gtk.main()
