text: import os.path
  import sys
  sys.path.append(os.path.dirname(__file__))
  
  # import the main window object (mw) from aqt
  from aqt import mw
  # import all of the Qt GUI library
  from aqt.qt import *
  from PyQt5 import QtCore
  import logging
  import jieba
  from cached_property import cached_property
  import chinesevocablist
  import chineseflashcards
  import genanki
  
  RECOMMENDED_LEARN_WORDS_NUM = 3500
  RECOMMENDED_SKIP_WORDS_NUM = 0
  
  try:
      from typing import List, Optional, Set, Tuple
  except ImportError:
      # Temporary hack until typing module is supported.
      from collections import defaultdict
      List = defaultdict(lambda: None)
      Optional = defaultdict(lambda: None)
      Set = defaultdict(lambda: None)
      Tuple = defaultdict(lambda: None)
  
  
  def is_chinese_word(s):
      if not s:
          return False
      # TODO this is not a complete list, see
      # https://stackoverflow.com/questions/1366068/whats-the-complete-range-for-chinese-characters-in-unicode
      return all(0x4E00 <= ord(c) <= 0x9FFF for c in s)
  
  
  class LineEditWithFocusedSignal(QLineEdit):
      focused = pyqtSignal()
  
      def focusInEvent(self, e):
          self.focused.emit()
  
  
  class ChinesePrestudy:
      """
      Class that manages all the state associated with the Chinese Prestudy add-on.
      """
  
      @classmethod
      def instantiate_and_run(cls):
          cls().show_text_entry_window()
  
      def show_text_entry_window(self):
          """
          Show the first window of the utility. This window prompts the user to paste in some text.
          """
          self.text_entry_window = w = QWidget(mw, flags=QtCore.Qt.Window)
          w.setWindowTitle('Chinese Prestudy')
  
          vbox = QVBoxLayout()
  
          vbox.addWidget(QLabel('Paste in the Chinese text you want to read:'))
  
          self.input_text_box = QTextEdit()
          vbox.addWidget(self.input_text_box)
  
          continue_button = QPushButton('Continue')
          # TODO not sure why a lambda is needed here
          continue_button.clicked.connect(lambda: self.text_entry_continue_action())
          hbox = QHBoxLayout()
          hbox.addStretch(1)
          hbox.addWidget(continue_button)
          vbox.addLayout(hbox)
  
          w.setLayout(vbox)
  
          w.show()
  
      def text_entry_continue_action(self):
          self.input_text = self.input_text_box.toPlainText()
          self.text_entry_window.close()
  
          self.show_words_window()
  
      @cached_property
      def input_segmented(self) -> List[str]:
          """
          Return the input after segmenting into words.
          """
          jieba.setLogLevel(logging.ERROR)
          jieba.initialize()
  
          return list(jieba.cut(self.input_text))
  
      @cached_property
      def input_words(self) -> List[str]:
          """
          Return unique words in text, as a list, sorted by order of appearance in text.
          """
          rv = []
          seen_words = set()
  
          for word in self.input_segmented:
              if word in seen_words:
                  continue
              if not is_chinese_word(word):
                  continue
  
              seen_words.add(word)
              rv.append(word)
  
          return rv
  
      def show_words_window(self):
          """
          Show the second window of the utility. This window shows the new words that were extracted from the text.
          """
          self.words_window = QWidget(mw, flags=QtCore.Qt.Window)
  
          config = mw.addonManager.getConfig(__name__)
          config.setdefault('study_words_num', RECOMMENDED_LEARN_WORDS_NUM)
          config.setdefault('skip_words_num', RECOMMENDED_SKIP_WORDS_NUM)
  
          vbox = QVBoxLayout()
  
          study_words_num_hbox = QHBoxLayout()
          self.study_words_num_box = QLineEdit()
          self.study_words_num_box.setText(str(config['study_words_num']))
          study_words_num_hbox.addWidget(QLabel('Study the most common'))
          study_words_num_hbox.addWidget(self.study_words_num_box)
          study_words_num_hbox.addWidget(QLabel('words (recommended: {})'.format(RECOMMENDED_LEARN_WORDS_NUM)))
          study_words_num_hbox.addStretch(1)
          vbox.addLayout(study_words_num_hbox)
  
          skip_words_num_hbox = QHBoxLayout()
          self.skip_words_num_box = QLineEdit()
          self.skip_words_num_box.setText(str(config['skip_words_num']))
          skip_words_num_hbox.addWidget(QLabel('...skipping the most common'))
          skip_words_num_hbox.addWidget(self.skip_words_num_box)
          skip_words_num_hbox.addWidget(QLabel('words (recommended: {})'.format(RECOMMENDED_SKIP_WORDS_NUM)))
          skip_words_num_hbox.addStretch(1)
          vbox.addLayout(skip_words_num_hbox)
          vbox.addWidget(QLabel('These are the new words you should learn:'))
  
          self.words_and_defs_table = self.init_words_and_defs_table()
          vbox.addWidget(self.words_and_defs_table)
  
          continue_hbox = QHBoxLayout()
          continue_hbox.addStretch(1)
          continue_button = QPushButton('Continue')
          continue_hbox.addWidget(continue_button)
          vbox.addLayout(continue_hbox)
  
          self.words_window.setLayout(vbox)
  
          self.update_words_and_defs_table()
  
          self.study_words_num_box.textChanged.connect(lambda: self.update_words_and_defs_table())
          self.skip_words_num_box.textChanged.connect(lambda: self.update_words_and_defs_table())
          continue_button.clicked.connect(lambda: self.words_window_continue_action())
  
          self.words_window.show()
  
      def update_words_and_defs_table(self):
          words_to_study = self.words_to_study
          self.words_and_defs_table.setRowCount(len(words_to_study))
          for i, word in enumerate(words_to_study):
              self.words_and_defs_table.setItem(i, 0, QTableWidgetItem(word.simp))
              self.words_and_defs_table.setItem(i, 1, QTableWidgetItem(word.defs[0]))
  
      @property
      def words_to_study(self) -> List[chinesevocablist.VocabWord]:
          return self.get_words_to_study(self.study_words_num, self.skip_words_num)
  
      @property
      def study_words_num(self):
          try:
              return int(self.study_words_num_box.text())
          except ValueError:
              return 0
  
      @property
      def skip_words_num(self):
          try:
              return int(self.skip_words_num_box.text())
          except ValueError:
              return 0
  
      def get_words_to_study(self, study_words_num, skip_words_num) -> List[chinesevocablist.VocabWord]:
          words = [w for w in self.all_words_to_study[skip_words_num:study_words_num] if w is not None]
  
          # re-sort to match input order
          # TODO this is inefficient
          def index(vocab_word):
              for i, input_word in enumerate(self.unknown_words):
                  if input_word in [vocab_word.simp, vocab_word.trad]:
                      return i
              raise Exception
  
          return sorted(words, key=index)
  
      @cached_property
      def all_words_to_study(self) -> List[Optional[chinesevocablist.VocabWord]]:
          """
          Returns `vocab_list.words`, with `None` replacing elements that aren't in `input_text`.
  
          This allows us to quickly determine which words correspond to a given vocab target.
          """
          unknown_words_set = set(self.unknown_words)
          return [w if {w.simp, w.trad} & unknown_words_set else None for w in self.vocab_list.words]
  
      @cached_property
      def unknown_words(self) -> List[str]:
          """
          Get words in the text that aren't already studied.
          """
          return [word for word in self.input_words if word not in self.words_already_studied]
  
      @cached_property
      def words_already_studied(self) -> Set[str]:
          """
          Get words that are already studied, as a set.
  
          TODO this is a total hack right now.
          """
          # logic: a word is studied if there is a corresponding note with at least one card that is seen, or if there
          # is a corresponding note with all cards suspended
          def words_for_query(query):
              notes = [mw.col.getNote(id_) for id_ in mw.col.findNotes(query)]
              rv = set()
              for note in notes:
                  rv.update(f for f in note.fields if is_chinese_word(f))
              return rv
  
          suspended = words_for_query('is:suspended')
          not_suspended = words_for_query('-is:suspended')
          not_new = words_for_query('-is:new')
  
          return not_new | (suspended - not_suspended)
  
      @property
      def input_with_hard_words_annotated(self) -> List[Tuple[str, Optional[chinesevocablist.VocabWord]]]:
          """
          Returns the input text, with "hard" words (words that are unknown and are above the study limit) annotated with
          definitions.
  
          The return format is a sequence of tuples (word, defn), where defn is None if the word is already known.
          Punctuation and newlines will also have their own tuples (with a None defn). It should be true that:
  
              ''.join(elem[0] for elem in self.input_with_hard_words_annotated) == self.input_text
          """
          # TODO we currently only cover words that appear in the Chinese Vocab List. Instead, we should synthesize
          #     VocabWords when they aren't in the vocab list.
          # TODO We're assuming simplified characters here.
          definitions = {word.simp: word for word in self.all_words_to_study if word is not None}
          to_define_set = set(definitions)
          to_define_set -= set(word.simp for word in self.words_to_study)
  
          rv = []
          for seg in self.input_segmented:
              if seg in to_define_set:
                  rv.append((seg, definitions[seg]))
                  to_define_set.remove(seg)
              else:
                  rv.append((seg, None))
  
          return rv
  
      @cached_property
      def vocab_list(self):
          return chinesevocablist.VocabList.load()
  
      def init_words_and_defs_table(self, parent=None):
          """
          Generates a widget that displays a table of words and definitions.
  
          :param word_def_pairs: list of (word, def) tuples
          :return: a widget
          """
          return QTableWidget(0, 2, parent)
  
      def words_window_continue_action(self):
          config = mw.addonManager.getConfig(__name__)
          config['study_words_num'] = self.study_words_num
          config['skip_words_num'] = self.skip_words_num
          mw.addonManager.writeConfig(__name__, config)
  
          final_touches_window = FinalTouchesWindow(self.words_to_study)
  
          self.words_window.close()
          final_touches_window.show()
  
  
  class FinalTouchesWindow(QWidget):
      """
      Window 3/3, allows the user to set deck and tags.
      """
  
      def __init__(self, vocab_words: List[chinesevocablist.VocabWord]):
          super().__init__(mw, flags=QtCore.Qt.Window)
          self.vocab_words = vocab_words
          self.init_layout()
  
      def init_layout(self):
          self.setWindowTitle('Chinese Prestudy')
  
          vbox = QVBoxLayout()
  
          vbox.addWidget(QLabel('Select deck to add notes to:'))
          self.combo_box = QComboBox(self)
          self.combo_box.addItems(self.deck_names)
          vbox.addWidget(self.combo_box)
  
          vbox.addWidget(QLabel('(Optional) Enter tag(s) to add to notes, separated by spaces:'))
          self.tags_box = QLineEdit()
          vbox.addWidget(self.tags_box)
  
          hbox = QHBoxLayout()
          self.finish_button = QPushButton('Add Notes')
          hbox.addStretch(1)
          hbox.addWidget(self.finish_button)
          vbox.addLayout(hbox)
  
          self.finish_button.clicked.connect(lambda: self.add_notes_action())
  
          self.setLayout(vbox)
  
      @property
      def deck_names(self):
          return [d['name'] for d in self.decks]
  
      @property
      def decks(self):
          return sorted(list(mw.col.decks.decks.values()), key=lambda d: d['name'])
  
      def add_notes_action(self):
          # Checkpoint so user can undo later
          mw.checkpoint('Add Chinese Prestudy Notes')
  
          add_notes(self.vocab_words, self.combo_box.currentText(), self.tags_box.text().split())
  
          # Refresh main window view
          mw.reset()
  
          self.close()
  
  
  def add_notes(vocab_words: List[chinesevocablist.VocabWord], deck_name: str, tags: List[str]):
      # get dict that describes deck
      deck = [d for d in mw.col.decks.decks.values() if d['name'] == deck_name][0]
  
      # By using the same ID and name as the existing deck, the notes are added to the existing deck, rather than going
      # into a new deck or the default deck.
      out_deck = chineseflashcards.ChineseDeck(deck['id'], deck_name)
  
      for vocab_word in vocab_words:
          note = out_deck.add_vocab_list_word(vocab_word, tags=tags)
          # Temporary hack: suspend everything except the "Simplified" card.
          # This should ideally be based on a GUI selector.
          for i, c in enumerate(note.cards):
              if i == 1:
                  continue
              c.suspend = True
  
      # Write the data to the collection
      out_deck.write_to_collection_from_addon()
  
  
  # create a new menu item, "Chinese Prestudy"
  action = QAction('Chinese Prestudy', mw)
  # set it to call ChinesePrestudy.instantiate_and_run when it's clicked
  action.triggered.connect(ChinesePrestudy.instantiate_and_run)
  # and add it to the tools menu
  mw.form.menuTools.addAction(action)
  