docs: def evaluate_template(self, template, context, url_encode=False, strategy=EvaluationStrategy.COMPLETE):
          """
          Evaluates a template string, e.g. "Hello @contact.name you have @(contact.reports * 2) reports"
          :param template: the template string
          :param context: the evaluation context
          :param url_encode: whether or not values should be URL encoded
          :return: a tuple of the evaluated template and a list of evaluation errors
          """
          input_chars = list(template)
          output_chars = []
          errors = []
          state = State.BODY
          current_expression_chars = []
          current_expression_terminated = False
          parentheses_level = 0
  
          # determines whether the given character is a word character, i.e. \w in a regex
          is_word_char = lambda c: c and (c.isalnum() or c == '_')
  
          for pos, ch in enumerate(input_chars):
              # in order to determine if the b in a.b terminates an identifier, we have to peek two characters ahead as it
              # could be a.b. (b terminates) or a.b.c (b doesn't terminate)
              next_ch = input_chars[pos + 1] if (pos < (len(input_chars) - 1)) else None
              next_next_ch = input_chars[pos + 2] if (pos < (len(input_chars) - 2)) else None
  
              if state == State.BODY:
                  if ch == self._expression_prefix and (is_word_char(next_ch) or next_ch == '('):
                      state = State.PREFIX
                      current_expression_chars = [ch]
                  elif ch == self._expression_prefix and next_ch == self._expression_prefix:
                      state = State.ESCAPED_PREFIX
                  else:
                      output_chars.append(ch)
  
              elif state == State.PREFIX:
                  if is_word_char(ch):
                      # we're parsing an expression like @XXX
                      state = State.IDENTIFIER
                  elif ch == '(':
                      # we're parsing an expression like @(1 + 2)
                      state = State.BALANCED
                      parentheses_level += 1
  
                  current_expression_chars.append(ch)
  
              elif state == State.IDENTIFIER:
                  current_expression_chars.append(ch)
  
              elif state == State.BALANCED:
                  if ch == '(':
                      parentheses_level += 1
                  elif ch == ')':
                      parentheses_level -= 1
                  elif ch == '"':
                      state = State.STRING_LITERAL
  
                  current_expression_chars.append(ch)
  
                  # expression terminates if parentheses balance
                  if parentheses_level == 0:
                      current_expression_terminated = True
  
              elif state == State.STRING_LITERAL:
                  if ch == '"':
                      state = State.BALANCED
                  current_expression_chars.append(ch)
  
              elif state == State.ESCAPED_PREFIX:
                  state = State.BODY
                  output_chars.append(ch)
  
              # identifier can terminate expression in 3 ways:
              #  1. next char is null (i.e. end of the input)
              #  2. next char is not a word character or period
              #  3. next char is a period, but it's not followed by a word character
              if state == State.IDENTIFIER:
                  if not next_ch or (not is_word_char(next_ch) and next_ch != '.') or (next_ch == '.' and not is_word_char(next_next_ch)):
                      current_expression_terminated = True
  
              if current_expression_terminated:
                  expression = ''.join(current_expression_chars)
                  output_chars.append(self._resolve_expression_block(expression, context, url_encode, strategy, errors))
                  current_expression_chars = []
                  current_expression_terminated = False
                  state = State.BODY
  
          # if last expression didn't terminate - add to output as is
          if not current_expression_terminated and current_expression_chars:
              output_chars.append(''.join(current_expression_chars))
  
          output = ''.join(output_chars)  # joining is fastest way to build strings in Python
          return output, errors