context: using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Reflection;
  using System.Windows;
  using System.Windows.Media;
  using WpfMath.Atoms;
  using WpfMath.Colors;
  using WpfMath.Exceptions;
  using WpfMath.Parsers;
  using WpfMath.Utils;
  
  namespace WpfMath
  {
      // TODO: Put all error strings into resources.
      // TODO: Use TextReader for lexing.
      public class TexFormulaParser
      {
          // Special characters for parsing
          private const char escapeChar = '\\';
  
          internal const char leftGroupChar = '{';
          internal const char rightGroupChar = '}';
          private const char leftBracketChar = '[';
          private const char rightBracketChar = ']';
  
          private const char subScriptChar = '_';
          private const char superScriptChar = '^';
          private const char primeChar = '\'';
  
          /// <summary>
          /// A set of names of the commands that are embedded in the parser itself, <see cref="ProcessCommand"/>.
          /// These're not the additional commands that may be supplied via <see cref="_commandRegistry"/>.
          /// </summary>
          private static readonly HashSet<string> embeddedCommands = new HashSet<string>
          {
              "color",
              "colorbox",
              "frac",
              "left",
              "overline",
              "right",
              "sqrt"
          };
  
          private static readonly IList<string> symbols;
          private static readonly IList<string> delimeters;
          private static readonly HashSet<string> textStyles;
          private static readonly IDictionary<string, Func<SourceSpan, TexFormula?>> predefinedFormulas =
              new Dictionary<string, Func<SourceSpan, TexFormula?>>();
  
          private static readonly string[][] delimiterNames =
          {
              new[] { "lbrace", "rbrace" },
              new[] { "(", ")" },
              new[] { "lbrack", "rbrack" },
              new[] { "downarrow", "downarrow" },
              new[] { "uparrow", "uparrow" },
              new[] { "updownarrow", "updownarrow" },
              new[] { "Downarrow", "Downarrow" },
              new[] { "Uparrow", "Uparrow" },
              new[] { "Updownarrow", "Updownarrow" },
              new[] { "vert", "vert" },
              new[] { "Vert", "Vert" }
          };
  
          static TexFormulaParser()
          {
              //
              // If start application isn't WPF, pack isn't registered by defaultTexFontParser
              //
              if (Application.ResourceAssembly == null)
              {
                  Application.ResourceAssembly = Assembly.GetExecutingAssembly();
                  if (!UriParser.IsKnownScheme("pack"))
                      UriParser.Register(new GenericUriParser(GenericUriParserOptions.GenericAuthority), "pack", -1);
              }
  
              var formulaSettingsParser = new TexPredefinedFormulaSettingsParser();
              symbols = formulaSettingsParser.GetSymbolMappings();
              delimeters = formulaSettingsParser.GetDelimiterMappings();
              textStyles = formulaSettingsParser.GetTextStyles();
  
              var predefinedFormulasParser = new TexPredefinedFormulaParser();
              predefinedFormulasParser.Parse(predefinedFormulas);
          }
  
          internal static string[][] DelimiterNames
          {
              get { return delimiterNames; }
          }
  
          internal static string GetDelimeterMapping(char character)
          {
              try
              {
                  return delimeters[character];
              }
              catch (KeyNotFoundException)
              {
                  throw new DelimiterMappingNotFoundException(character);
              }
          }
  
          internal static SymbolAtom? GetDelimiterSymbol(string? name, SourceSpan? source)
          {
              if (name == null)
                  return null;
  
              var result = SymbolAtom.GetAtom(name, source);
              if (!result.IsDelimeter)
                  return null;
              return result;
          }
  
          private static bool IsSymbol(char c)
          {
              return !char.IsLetterOrDigit(c);
          }
  
          private static bool IsWhiteSpace(char ch)
          {
              return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r';
          }
  
          private static bool ShouldSkipWhiteSpace(string? style) => style != TexUtilities.TextStyleName;
  
          /// <summary>A registry for additional commands.</summary>
          private readonly IReadOnlyDictionary<string, ICommandParser> _commandRegistry;
  
          private readonly IReadOnlyDictionary<string, IColorParser> _colorModelParsers;
  
          /// <summary>A color parser for cases when the color model isn't specified.</summary>
          private readonly IColorParser _defaultColorParser;
  
          internal TexFormulaParser(
              IReadOnlyDictionary<string, ICommandParser> commandRegistry,
              IReadOnlyDictionary<string, IColorParser> colorModelParsers,
              IColorParser defaultColorParser)
          {
              _commandRegistry = commandRegistry;
              _colorModelParsers = colorModelParsers;
              _defaultColorParser = defaultColorParser;
          }
  
          public TexFormulaParser(
              IReadOnlyDictionary<string, IColorParser> colorModelParsers,
              IColorParser defaultColorParser) : this(StandardCommands.Dictionary, colorModelParsers, defaultColorParser)
          {}
  
          public TexFormulaParser() : this(
              StandardColorParsers.Dictionary,
              PredefinedColorParser.Instance)
          {}
  
          public TexFormula Parse(string value, string? textStyle = null) =>
              Parse(new SourceSpan("User input", value, 0, value.Length), textStyle);
  
          public TexFormula Parse(SourceSpan value, string? textStyle = null)
          {
              var position = 0;
              return Parse(value, ref position, false, textStyle, DefaultCommandEnvironment.Instance);
          }
  
          internal TexFormula Parse(SourceSpan value, string? textStyle, ICommandEnvironment environment)
          {
              int localPostion = 0;
              return Parse(value, ref localPostion, false, textStyle, environment);
          }
  
          private DelimiterInfo ParseUntilDelimiter(
              SourceSpan value,
              ref int position,
              string? textStyle,
              ICommandEnvironment environment)
          {
              var embeddedFormula = Parse(value, ref position, true, textStyle, environment);
              if (embeddedFormula.RootAtom == null)
                  throw new TexParseException("Cannot find closing delimiter");
  
              var source = embeddedFormula.RootAtom.Source;
              var bodyRow = embeddedFormula.RootAtom as RowAtom;
              var lastAtom = bodyRow?.Elements.LastOrDefault() ?? embeddedFormula.RootAtom;
              var lastDelimiter = lastAtom as SymbolAtom;
              if (lastDelimiter == null || !lastDelimiter.IsDelimeter)
                  throw new TexParseException($"Cannot find closing delimiter; got {lastDelimiter} instead");
  
              Atom bodyAtom;
              if (bodyRow == null)
              {
                  bodyAtom = new RowAtom(source);
              }
              else if (bodyRow.Elements.Count > 2)
              {
                  var row = bodyRow.Elements.Take(bodyRow.Elements.Count - 1)
                      .Aggregate(new RowAtom(source), (r, atom) => r.Add(atom));
                  bodyAtom = row;
              }
              else if (bodyRow.Elements.Count == 2)
              {
                  bodyAtom = bodyRow.Elements[0];
              }
              else
              {
                  throw new NotSupportedException($"Cannot convert {bodyRow} to fenced atom body");
              }
  
              return new DelimiterInfo(bodyAtom, lastDelimiter);
          }
  
          private TexFormula Parse(
              SourceSpan value,
              ref int position,
              bool allowClosingDelimiter,
              string? textStyle,
              ICommandEnvironment environment)
          {
              var formula = new TexFormula { Source = value, TextStyle = textStyle };
              var closedDelimiter = false;
              var skipWhiteSpace = ShouldSkipWhiteSpace(textStyle);
              var initialPosition = position;
              while (position < value.Length && !(allowClosingDelimiter && closedDelimiter))
              {
                  char ch = value[position];
                  var source = value.Segment(position, 1);
                  if (IsWhiteSpace(ch))
                  {
                      if (!skipWhiteSpace)
                      {
                          formula.Add(new SpaceAtom(source), source);
                      }
  
                      position++;
                  }
                  else if (ch == escapeChar)
                  {
                      ProcessEscapeSequence(
                          formula,
                          value,
                          ref position,
                          allowClosingDelimiter,
                          ref closedDelimiter,
                          environment);
                  }
                  else if (ch == leftGroupChar)
                  {
                      var groupValue = ReadElement(value, ref position);
                      var parsedGroup = Parse(groupValue, textStyle, environment.CreateChildEnvironment());
                      var innerGroupAtom = parsedGroup.RootAtom ?? new RowAtom(groupValue);
                      var groupAtom = new TypedAtom(
                          innerGroupAtom.Source,
                          innerGroupAtom,
                          TexAtomType.Ordinary,
                          TexAtomType.Ordinary);
                      var scriptsAtom = this.AttachScripts(formula, value, ref position, groupAtom, true, environment);
                      formula.Add(scriptsAtom, value.Segment(initialPosition, position - initialPosition));
                  }
                  else if (ch == rightGroupChar)
                  {
                      throw new TexParseException("Found a closing '" + rightGroupChar
                          + "' without an opening '" + leftGroupChar + "'!");
                  }
                  else if (ch == superScriptChar || ch == subScriptChar || ch == primeChar)
                  {
                      if (position == 0)
                          throw new TexParseException("Every script needs a base: \""
                              + superScriptChar + "\", \"" + subScriptChar + "\" and \""
                              + primeChar + "\" can't be the first character!");
                      else
                      {
                          var scriptsAtom = this.AttachScripts(formula, value, ref position, new RowAtom(value), true, environment);
                          formula.Add(scriptsAtom, value.Segment(initialPosition, position));
                      }
                  }
                  else
                  {
                      var character = ConvertCharacter(formula, ref position, source, environment);
                      if (character != null)
                      {
                          var scriptsAtom = AttachScripts(
                              formula,
                              value,
                              ref position,
                              character,
                              skipWhiteSpace,
                              environment);
                          formula.Add(scriptsAtom, value.Segment(initialPosition, position));
                      }
                  }
              }
  
              return formula;
          }
  
          private static TexFormula ConvertRawText(SourceSpan value, string textStyle)
          {
              var formula = new TexFormula { Source = value, TextStyle = textStyle };
  
              var position = 0;
              var initialPosition = position;
              while (position < value.Length)
              {
                  var ch = value[position];
                  var source = value.Segment(position, 1);
                  var atom = IsWhiteSpace(ch)
                      ? (Atom) new SpaceAtom(source)
                      : new CharAtom(source, ch, textStyle);
                  position++;
                  formula.Add(atom, value.Segment(initialPosition, position - initialPosition));
              }
  
              return formula;
          }
  
          internal static SourceSpan ReadElementGroup(SourceSpan value, ref int position, char openChar, char closeChar)
          {
              if (position == value.Length || value[position] != openChar)
                  throw new TexParseException("missing '" + openChar + "'!");
  
              var group = 0;
              position++;
              var start = position;
              while (position < value.Length && !(value[position] == closeChar && group == 0))
              {
                  if (value[position] == openChar)
                      group++;
                  else if (value[position] == closeChar)
                      group--;
                  position++;
              }
  
              if (position == value.Length)
              {
                  // Reached end of formula but group has not been closed.
                  throw new TexParseException("Illegal end,  missing '" + closeChar + "'!");
              }
  
              position++;
              return value.Segment(start, position - start - 1);
          }
  
          /// <summary>Reads a char-delimited element group if it exists; returns <c>null</c> if it isn't.</summary>
          private static SourceSpan? ReadElementGroupOptional(
              SourceSpan value,
              ref int position,
              char openChar,
              char closeChar)
          {
              SkipWhiteSpace(value, ref position);
              if (position == value.Length || value[position] != openChar)
                  return null;
  
              return ReadElementGroup(value, ref position, openChar, closeChar);
          }
  
          private static SourceSpan ReadEscapeSequence(SourceSpan value, ref int position)
          {
              var initialPosition = position;
              if (value[initialPosition] != escapeChar)
                  throw new Exception($"Invalid state: {nameof(ReadEscapeSequence)} called for a value without escape character ({value})");
  
              position++;
              var start = position;
              while (position < value.Length)
              {
                  var ch = value[position];
                  var isEnd = position == value.Length - 1;
                  if (!char.IsLetter(ch) || isEnd)
                  {
                      // Escape sequence has ended
                      // Or it's a symbol. Assuming in this case it will only be a single char.
                      if ((isEnd && char.IsLetter(ch)) || position - start == 0)
                      {
                          position++;
                      }
                      break;
                  }
  
                  position++;
              }
  
              var length = position - initialPosition;
              if (length <= 1)
                  throw new TexParseException($"Unfinished escape sequence (value: \"{value}\", index {position})");
  
              return value.Segment(initialPosition, length);
          }
  
          private SymbolAtom ParseDelimiter(SourceSpan value, int start, ref int position)
          {
              var delimiter = ReadElement(value, ref position);
  
              string delimiterName;
              if (delimiter.Length == 1)
                  delimiterName = GetDelimeterMapping(delimiter[0]);
              else
              {
                  if (delimiter[0] != escapeChar)
                      throw new Exception($"Incorrect parser state: delimiter should start from {escapeChar}: {delimiter}");
  
                  // Here goes the fancy business: for non-alphanumeric commands (e.g. \{, \\ etc.) we need to pass them
                  // through GetDelimeterMapping, but for alphanumeric ones, we don't.
                  delimiterName = delimiter.Segment(1).ToString(); // skip an escape character
                  if (delimiterName.Length == 1 && !char.IsLetterOrDigit(delimiterName[0]))
                      delimiterName = GetDelimeterMapping(delimiterName[0]);
              }
  
              var delimiterSource = value.Segment(start, position - start); // will map the whole "\left(" to a delimiter atom created
              if (delimiterName == null || !SymbolAtom.TryGetAtom(delimiterName, delimiterSource, out var atom) || !atom.IsDelimeter)
                  throw new TexParseException($"Cannot find delimiter {delimiter}");
  
              return atom;
          }
  
          /// <summary>
          /// Reads an element: typically, a curly brace-enclosed value group, a singular value or a character sequence
          /// prefixed by a backslash.
          /// </summary>
          /// <exception cref="TexParseException">Will be thrown for ill-formed groups.</exception>
          internal static SourceSpan ReadElement(SourceSpan value, ref int position)
          {
              SkipWhiteSpace(value, ref position);
  
              if (position == value.Length)
                  throw new TexParseException("An element is missing");
  
              return value[position] switch
              {
                  leftGroupChar => ReadElementGroup(value, ref position, leftGroupChar, rightGroupChar),
                  escapeChar => ReadEscapeSequence(value, ref position),
                  _ => value.Segment(position++, 1)
              };
          }
  
          private TexFormula ReadScript(
              TexFormula formula,
              SourceSpan value,
              ref int position,
              ICommandEnvironment environment) =>
              Parse(ReadElement(value, ref position), formula.TextStyle, environment.CreateChildEnvironment());
  
          /// <remarks>May return <c>null</c> for commands that produce no atoms.</remarks>
          private Tuple<AtomAppendMode, Atom?> ProcessCommand(
              TexFormula formula,
              SourceSpan value,
              ref int position,
              string command,
              bool allowClosingDelimiter,
              ref bool closedDelimiter,
              ICommandEnvironment environment)
          {
  
gt:             int start = position - command.Length;