text: import itertools
  from typing import List, Optional
  from .lexigen import *
  from .markov import StochasticJolasticWordGenerator
  from .utils import too_similar
  
  
  class Poem:
  
      def __init__(self, input_words, words_for_sampling):
          self.input_words = input_words
          self.words_for_sampling = words_for_sampling
          self.title = "'".join(input_words)
          self.lines: List[str] = []
  
      def __str__(self):
          return self.raw_text
  
      def update(self):
          self.raw_text = '\n'.join(self.lines)
  
      @property
      def previous_line(self):
          if len(self.lines):
              return self.lines[-1]
          return ''
  
  
  class PoemGenerator:
  
      def __init__(self):
          self.default_connectors = [' ', '   ', '...   ', random.choice([' & ', ' and ']), '  or  ', ' or ']
          self.line_enders = ['.', ', ', '!', '?', '', ' or', '...']
          self.markov_line_enders = ['', '', ',', ',', '!', '.', '?']
          self.line_indents = ['', '    ', '         ']
          self.poem = None
  
  
      def poem_line_from_markov(self, starting_word: str, num_words: int = 4, rhyme_with: Optional[str] = None,
                                words_for_sampling: List[str] = [], max_line_length: Optional[int] = 35) -> str:
          """Generate a line of poetry using a markov chain that optionally tries to make a line rhyme with the last one
  
          Different algorithms handle the last word and all the other words: both algorithms use a mix of random
          probability and process stopwords differently to keep the generated text interesting and non-repetitive.
  
          :param starting_word: the input word for the Markov algorithm, which hence is also the poem line's first word
          :param num_words: the number of words to write in the poem line
          :param rhyme_with: an optional word to try to make the poem line rhyme with. The algorithm will try something
                             else if this word is a common stopword or if it can't find a rhyme though.
          :param words_for_sampling: a list of other words to throw in to the poem. If you don't know what to pass here,
                                     phonetically related words to the starting word probably adds some sonority.
          :param max_line_length: an upper limit in characters for the line -- important for PDF generation to keep
                                  everything on the page.
          """
          output_words, previous_word = [starting_word], starting_word
          markovgen = StochasticJolasticWordGenerator(previous_lines=self.poem.lines)
          for i in range(num_words - 1):
              if (i == num_words - 2) or (max_line_length and (max_line_length > 14 and
                                                               len(' '.join(output_words)) >= max_line_length - 14)):
                  # Checks if if it's the last word--the limit can be determined by either word count or character count
                  max_word_length = 12 if max_line_length else None
                  word = markovgen.last_word_of_markov_line(output_words, rhyme_with=rhyme_with,
                                                            max_length=max_word_length)
                  output_words.append(word)
                  break
              else:
                  word = markovgen.nonlast_word_of_markov_line(output_words, words_for_sampling=words_for_sampling)
                  output_words.append(word)
          correct_a_vs_an(output_words)
          return " ".join(output_words)
  
      def poem_from_markov(self, input_words, num_lines=10, min_line_words: int = 5, max_line_words: int = 9,
                           max_line_length: Optional[int] = 35) -> str:
          """Generate a line of poetry using a markov chain that optionally tries to make a line rhyme with the last one
              Different algorithms handle the last word and all the other words: both algorithms use a mix of random
              probability and process stopwords differently to keep the generated text interesting and non-repetitive.
  
          :param input words: the user provided words to try making a poem from
          :param num_lines: the number of lines the poem will have
          :param max_line_words: the maximum number of words a line may have
          :param words_for_sampling: a list of other words to throw in to the poem. If you don't know what to pass here,
                                     phonetically related words to the starting word probably adds some sonority.
          :param max_line_length: an upper limit in characters for the line -- important for PDF generation to keep
                                  everything on the page.
              """
          self.poem = None
          words_for_sampling = input_words + phonetically_related_words(input_words, limit_results_per_input_word=20)
          # Check for undesirable similarity overlap in the words for sampling list
          similarity_checks = list(itertools.combinations(words_for_sampling, 2))
          words_removed = []
          for word_pair in similarity_checks:
              if not(word_pair[0] in words_removed or word_pair[1] in words_removed) and \
                      too_similar(word_pair[0], word_pair[1]):
                  words_removed.append(random.choice([word_pair[0], word_pair[1]]))
                  words_for_sampling.remove(words_removed[-1])
  
          self.poem = Poem(input_words, words_for_sampling)
          last_line_last_word = ''
          random.shuffle(words_for_sampling)
          line_enders = []
          print("\n")
          for i in range(num_lines):
              rhyme_with = last_line_last_word if i % 2 == 1 else None
              # 67.5 % chance the line starts with an input word or something relate, 32.5% with a common word
              line_starter = words_for_sampling.pop() if random.random() > .4 else \
                      random.choice(StochasticJolasticWordGenerator.common_words)
              while i >= 1 and too_similar(line_starter, self.poem.lines[i - 1].split(' ')[0]):
                  # while statement prevents repetition of line starters
                  line_starter = words_for_sampling.pop() if random.random() > .4 else \
                      random.choice(StochasticJolasticWordGenerator.common_words)
              line = self.poem_line_from_markov(line_starter, words_for_sampling=words_for_sampling,
                                                num_words=random.randint(min_line_words, max_line_words),
                                                rhyme_with=rhyme_with, max_line_length=max_line_length)
              self.poem.lines.append(line)
              last_line_last_word = line.split(' ')[-1]
              # Directly adding line ender to line now will screw up rhyme pairs so save it & add it in another iteration
              line_enders.append(random.choice(self.markov_line_enders))
              print(line + line_enders[-1])
          for i, line in enumerate(self.poem.lines):
              self.poem.lines[i] += line_enders[i]
          poem = self.poem
          return poem
  
      def poem_line_from_word_list(self, word_list: List[str], max_line_length=35, connectors: List[str] = []) -> str:
          """Generate a line of a visual poem from a list of words by gluing them together with random connectors
             (whitespace, conjunctions, punctuation, and symbols).
  
          :param word_list: the words that will be used (in order, not randomly) that will form a visual poem
          :param max_line_length: upper limit on the length of the return value in characters
          :param connectors (list): list of glue strings
          """
          connectors = connectors if len(connectors) else self.default_connectors
          output, last_word = word_list[0], word_list[0]
          last_connector = ''
          for word in word_list[1:]:
              if random.random() < (
                      .2 + len(output) / 100):  # Increasing probability of line termination as line gets longer
                  break
              if too_similar(last_word, word):
                  continue
              connector = random.choice(connectors)
              while connector == last_connector:
                  connector = random.choice(connectors)
              if len(output + connector + word) <= max_line_length:
                  output += connector + word
              last_word = word
              last_connector = connector
          return output
  
      def poem_from_word_list(self, input_word_list: List[str], num_lines: int = 6, max_line_length: int = 35,
                              connectors: List[str] = [], limit_line_to_one_input_word: bool = False):
          """Generate a visual poem from a list of words by taking a given input word list, adding the phonetically
             related words to that word list, and then using those words to create a visual/concrete poem.
  
          :param input_word_list: the list of user-provided words that will be used, along with phonetically related
                                  words, to generate a poem
          :param max_line_length: upper limit on the length of the return value in characters
          :param max_line_length: upper limit on length of poem lines (excluding line ending punctuation) in characters
          :param connectors: list of glue strings
          :param limit_line_to_one_input_word: If true, when generating a line of poetry, only use words that are
                                               phonetically related to one input word.
          """
          connectors = self.default_connectors if not len(connectors) else connectors
          output, line_indent = '', ''
          if limit_line_to_one_input_word:
              for i in range(num_lines - 1):
                  linked_word = random.choice(input_word_list)
                  output += self.poem_line_from_word_list(phonetically_related_words(linked_word), connectors=connectors,
                                                          max_line_length=max_line_length)
                  line_indent = random.choice(self.line_indents) if line_indent == '' else \
                      random.choice([li for li in self.line_indents if li is not line_indent])  # Don't repeat the same indent 2x
                  output += random.choice(self.line_enders) + '\n' + line_indent
          else:
              word_list = input_word_list.copy()
              for word in input_word_list:
                  word_list.extend(phonetically_related_words(word))
              for i in range(num_lines - 1):
                  random.shuffle(word_list)
                  output += self.poem_line_from_word_list(word_list, connectors=connectors,
                                                          max_line_length=max_line_length)
                  # Don't repeat the same indent 2x
                  line_indent = random.choice(self.line_indents) if line_indent == '' else \
                      random.choice([li for li in self.line_indents if li is not line_indent])
                  output += random.choice(self.line_enders) + '\n' + line_indent
  
          output += random.choice(input_word_list[:-1]) + ' ' + input_word_list[-1]
          return output
  
  
  def print_poem(poem: str):
      """Print the poem with a newline before and after so it's easy to take a screenshot of its 'recipe' and the poem
      in your terminal and share it. :)
  
      :param poem: the poem, as a string, to be printed
      """
      print('\n')
      print(poem)
      print('\n')