text: # -*- coding: utf-8 -*-
  """
      sanskrit_util.sandhi
      ~~~~~~~~~~~~~~~
  
      Classes that apply and undo sandhi rules.
  
      :license: MIT
  """
  
  from builtins import next
  from builtins import zip
  from builtins import range
  from builtins import object
  import six
  from . import sounds
  from .util import HashTrie
  
  
  class Exempt(six.text_type):
  
      """A helper class for marking strings as exempt from sandhi changes. To
      mark a string as exempt, just do the following::
  
          original = 'amI'
          exempt = Exempt('amI')
  
      :class:`Exempt` is a subclass of :class:`unicode`, so you can use normal
      string methods on :class:`Exempt` objects.
      """
  
  
  class SandhiObject(object):
  
      def add_rules(self, rules):
          raise NotImplementedError
  
  
  class Joiner(SandhiObject):
  
      """Joins multiple Sanskrit terms by applying sandhi rules."""
  
      def __init__(self, rules=None):
          self.data = {}
          if rules:
              self.add_rules(rules)
  
      def add_rules(self, rules):
          """Add rules for joining words.
  
          Example usage::
  
              joiner.add_rules[('a', 'i', 'e'), ('a', 'a', 'A'])
  
          :param rules: a list of 3-tuples, each of which contains:
  
          - the first part of the combination
          - the second part of the combination
          - the result
  
          """
          self.data = {}
          for first, second, result in rules:
              self.data[(first, second)] = result
  
      @staticmethod
      def internal_retroflex(term):
          """Apply the "n -> ṇ" and "s -> ṣ" rules of internal sandhi.
  
          :param term: the string to process
          """
          # causes "s" retroflexion
          s_trigger = set('iIuUfFeEoOkr')
          # causes "n" retroflexion
          n_trigger = set('fFrz')
          # Allowed after n_trigger
          n_between = sounds.VOWELS.union('kKgGNpPbBmhvyM')
          # Must appear after the retroflexed "n"
          n_after = sounds.VOWELS.union('myvn')
          # Defines t retroflexion
          retroflexion_dict = dict(zip('tT', 'wW'))
  
          letters = list(term)
  
          apply_s = False
          apply_n = False
          had_n = False  # Used for double retroflexion ('nisanna' -> 'nizaRRa')
          had_s = False  # Used for 't' retroflexion
          for i, L in enumerate(letters[:-1]):
              # "t" retroflexion after "s" retroflexion
              if had_s:
                  had_s = False
                  letters[i] = retroflexion_dict.get(L, L)
  
              # "s" retroflexion
              if apply_s and L == 's':
                  letters[i] = L = 'z'
                  had_s = True
              apply_s = L in s_trigger
  
              # "n" retroflexion
              if had_n and L == 'n':
                  letters[i] = 'R'
                  had_n = False
              elif apply_n and L == 'n' and letters[i + 1] in n_after:
                  letters[i] = 'R'
                  had_n = True
              if L in n_trigger:
                  apply_n = True
              else:
                  apply_n = apply_n and L in n_between
  
          return ''.join(letters)
  
      def join(self, chunks, internal=False):
          """Join the given chunks according to the object's rules::
  
              assert 'tasyecCA' == s.join('tasya', 'icCA')
  
          :meth:`join` does **not** take pragṛhya rules into account. As a
          reminder, the main exception are:
  
              | 1.1.11 "ī", "ū", and "e" when they end words in the dual.
              | 1.1.12 the same vowels after the "m" of adas;
              | 1.1.13 particles with just one vowel, apart from "ā"
              | 1.1.14 particles that end in "o".
  
          One simple way to account for these rules is to wrap exempt strings
          with :class:`Exempt`::
  
              assert joiner.join('te', 'iti') == 'ta iti'
              assert joiner.join(Exempt('te'), 'iti') == 'te iti'
  
          :param chunks: a list of the strings that should be joined
          :param internal: if true, join words using the empty string instead of
                           `' '`.
          """
          separator = '' if internal else ' '
  
          it = iter(chunks)
          returned = next(it)
          for chunk in it:
              if not chunk:
                  continue
              if isinstance(returned, Exempt):
                  returned += separator + chunk
              else:
                  # `i` controls the number of letters to grab from the end of
                  # the first word. For most rules, one letter is sufficient.
                  # But visarga sandhi needs slightly more context.
                  for i in (2, 1, 0):
                      if not i:
                          returned += separator + chunk
                          break
                      key = (returned[-i:], chunk[0])
                      result = self.data.get(key, None)
                      if result:
                          returned = returned[:-i] + result + chunk[1:]
                          break
              if isinstance(chunk, Exempt):
                  returned = Exempt(returned)
  
          if internal:
              return Joiner.internal_retroflex(returned)
          else:
              return returned
  
  
  class Splitter(object):
  
      """Splits Sanskrit terms by undoing sandhi rules."""
  
      def __init__(self, rules=None):
          """"""
          self.data = HashTrie()
          if rules:
              self.add_rules(rules)
  
      def add_rules(self, rules):
          for first, second, result in rules:
              result = result.replace(' ', '')
              items = (first, second, result, len(first), len(second),
                       len(result))
              self.data[result] = items
  
      def iter_splits(self, chunk):
          """Return a generator for all splits in `chunk`. Results are yielded
          as 2-tuples containing the term before the split and the term after::
  
              for item in s.splits('nareti'):
                  before, after = item
  
          :meth:`splits` will generate many false positives, usually when the
          first part of the split ends in an invalid consonant::
  
              assert ('narAv', 'iti') in s.splits('narAviti')
  
          These should be filtered out in the calling function.
  
          Splits are generated from left to right, but the function makes no
          guarantees on when certain rules are applied. That is, output is
          loosely ordered but nondeterministic.
          """
  
          chunk_len = len(chunk)
  
          for i in range(chunk_len):
              # Default split: chop the chunk in half with no other changes.
              # This can yield a lot of false positives.
              chunk1, chunk2 = chunk[:i], chunk[i:]
              if i:
                  yield (chunk1, chunk2)
  
              # Rule-based splits: undo a sandhi change
              rules = self.data[chunk2]
              for first, second, result, _, _, len_result in rules:
                  before = chunk1 + first
                  after = second + chunk2[len_result:]
                  yield (before, after)
  
          # Non-split: yield the chunk as-is.
          yield (chunk, '')
  