using HavenSoft.HexManiac.Core.ViewModels;
using HavenSoft.HexManiac.Core;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;

namespace HavenSoft.HexManiac.Core.Models.Code {
   public interface IScriptLine {
      IReadOnlyList<IScriptArg> Args { get; }
      IReadOnlyList<byte> LineCode { get; }
      string LineCommand { get; }
      IReadOnlyList<string> Documentation { get; }
      string Usage { get; }

      bool IsEndingCommand { get; }

      bool MatchesGame(int gameCodeHash);
      int CompiledByteLength(IDataModel model, int start, IDictionary<int, int> destinationLengths); // compile from the bytes in the model, at that start location
      int CompiledByteLength(IDataModel model, string line); // compile from the line of code passed in
      bool Matches(int gameCodeHash, IReadOnlyList<byte> data, int index);
      string Decompile(IDataModel data, int start, DecompileLabelLibrary labels, IList<ExpectedPointerType> streamTypes);

      /// <summary>
      /// Returns true if the command looks correct, even if the arguments are incomplete.
      /// </summary>
      bool CanCompile(string line);

      /// <summary>
      /// Returns an error if the line cannot be compiled, or a set of tokens if it can be compiled.
      /// </summary>
      string ErrorCheck(string scriptLine, out string[] tokens);

      string Compile(IDataModel model, int start, string scriptLine, LabelLibrary labels, out byte[] result);

      void AddDocumentation(string content);

      public int CountShowArgs() {
         return Args.Sum(arg => {
            if (arg is ScriptArg) return 1;
            return 0;
            // something with array args?
         });
      }
   }

   public class MacroScriptLine : IScriptLine {
      private static readonly IReadOnlyList<byte> emptyByteList = new byte[0];
      private readonly List<string> documentation = new List<string>();

      private bool hasShortForm;
      private readonly Dictionary<int, int> shortIndexFromLongIndex = new();
      private readonly IReadOnlyList<int> matchingGames;

      public IReadOnlyList<IScriptArg> Args { get; }
      public IReadOnlyList<IScriptArg> ShortFormArgs { get; private set; }
      public IReadOnlyList<byte> LineCode => emptyByteList;
      public IReadOnlyList<string> Documentation => documentation;
      public string LineCommand { get; }
      public bool IsEndingCommand => false;
      public bool IsValid { get; } = true;
      public string Usage { get; private set; }

      public static bool IsMacroLine(string engineLine) {
         engineLine = engineLine.Trim();
         var tokens = engineLine.Split(' ', StringSplitOptions.RemoveEmptyEntries);
         if (tokens.Length == 0) return false;
         var token = tokens[0];
         if (token.StartsWith("[") && tokens.Length > 1) token = tokens[1];
         if (token.StartsWith("#")) return false;
         if (token.Length == 2 && token.TryParseHex(out _)) return false;
         return true;
      }

      public MacroScriptLine(string engineLine) {
         var docSplit = engineLine.Split(new[] { '#' }, 2);
         if (docSplit.Length > 1) documentation.Add('#' + docSplit[1]);
         engineLine = docSplit[0].Trim();
         matchingGames = ScriptLine.ExtractMatchingGames(ref engineLine);
         ExtractShortformInfo(ref engineLine);
         if (!hasShortForm) {
            Usage = " ".Join(engineLine.Split(' ').Where(token => token.Length != 2 || !token.TryParseHex(out _)));
         }
         var usageTokens = Usage.Split(" ", StringSplitOptions.RemoveEmptyEntries);
         Usage = usageTokens[0] + " " + " ".Join(usageTokens.Skip(1).Select(t => t.Split(".:|<".ToCharArray())[0]));

         var tokens = engineLine.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
         var args = new List<IScriptArg>();
         LineCommand = tokens[0];

         for (int i = 1; i < tokens.Length; i++) {
            var token = tokens[i];
            if (token.Length == 2 && token.TryParseHex(out int number)) {
               args.Add(new SilentMatchArg((byte)number));
            } else if (ScriptArg.IsValidToken(token)) {
               args.Add(new ScriptArg(token));
            } else {
               IsValid = false;
            }
         }

         Args = args;
         ShortFormArgs ??= Args.Where(arg => arg is not SilentMatchArg).ToList();
      }

      /// <summary>
      /// Once the short form is exatracted, the long form is left behind.
      /// So either way, engineLine is now just the long form.
      /// </summary>
      private void ExtractShortformInfo(ref string engineLine) {
         if (!engineLine.Contains("->")) return;
         var parts = engineLine.Split("->");
         if (parts.Length != 2) return;
         engineLine = parts[1];
         var shortTokens = parts[0].Split(' ', StringSplitOptions.RemoveEmptyEntries);
         var longTokens = parts[1].Split(' ', StringSplitOptions.RemoveEmptyEntries);
         if (shortTokens[0] != longTokens[0]) return;
         shortTokens = shortTokens.Skip(1).ToArray();
         longTokens = longTokens.Skip(1).ToArray();

         // for each entry in long, it shows up somewhere in short
         // entries in long can appear multiple times
         // entries in long might not have any exact matches if the short-argument is multiple bytes and is split into multiple single-byte arguments in the long arguments.
         for (int i = 0; i < longTokens.Length; i++) {
            var index = shortTokens.IndexOf(longTokens[i]);
            if (index == -1) continue;
            shortIndexFromLongIndex.Add(i, index);
         }

         // NOTE: short form args do not permit array args, such as with the animation scripts
         ShortFormArgs = shortTokens.Select(token => new ScriptArg(token)).ToList();
         hasShortForm = true;
         Usage = parts[0];
      }

      public bool MatchesGame(int game) => matchingGames?.Contains(game) ?? true;

      public int CompiledByteLength(IDataModel model, int start, IDictionary<int, int> destinationLengths) {
         var length = LineCode.Count;
         foreach (var arg in Args) {
            if (destinationLengths != null) {
               var argLength = ScriptParser.GetArgLength(model, arg, start + length, destinationLengths);
               if (argLength > 0) destinationLengths[model.ReadPointer(start + length)] = argLength;
            }
            length += arg.Length(default, -1);
         }
         return length;
      }

      public int CompiledByteLength(IDataModel model, string line) {
         if (!CanCompile(line)) return 0;
         var length = LineCode.Count;
         foreach (var arg in Args) {
            length += arg.Length(default, -1);
         }
         return length;
      }

      public bool Matches(int gameCodeHash, IReadOnlyList<byte> data, int index) {
         if (Args.Count == 0) return false;
         if (!MatchesGame(gameCodeHash)) return false;
         var expectedVariableValues = new Dictionary<string, int>();
         for (int i = 0; i < Args.Count; i++) {
            var arg = Args[i];
            if (arg is SilentMatchArg smarg) {
               if (data[index] != smarg.ExpectedValue) return false;
            } else if (arg is ScriptArg sarg) {
               // if the argument is duplicated through multiple spots,
               // make sure all the spots match the same value.
               var matchingShortArg = ShortFormArgs.FirstOrDefault(shortArg => shortArg.Name == arg.Name);
               var argLength = arg.Length(default, -1);
               if ((matchingShortArg?.Length(default, -1) ?? 0) == argLength) {
                  var value = data.ReadMultiByteValue(index, argLength);
                  if (!expectedVariableValues.TryGetValue(sarg.Name, out var expectedValue)) expectedVariableValues[sarg.Name] = value;
                  else if (expectedValue != value) return false; // only match the macro if all the variables match
               }
            } else {
               throw new NotImplementedException();
            }
            index += arg.Length(default, -1);
         }
         return true;
      }

      public string Decompile(IDataModel data, int start, DecompileLabelLibrary labels, IList<ExpectedPointerType> streamTypes) {
         var builder = new StringBuilder(LineCommand);
         var streamContent = new List<string>();
         var args = new List<string>();
         var shiftNames = new Dictionary<string, int>();
         var carryNames = new Dictionary<string, int>();
         int i = 0;
         foreach (var arg in Args) {
            if (arg is ScriptArg sarg) {
               var tempBuilder = new StringBuilder();
               int shift = 0, carry = 0;
               var argLength = sarg.Length(data, start);
               if (!shortIndexFromLongIndex.TryGetValue(i, out var shortIndex)) {
                  var shortArg = ShortFormArgs.FirstOrDefault(arg => arg.Name == sarg.Name);
                  if (shortArg != null && shortArg.Length(data, start) != argLength) {
                     if (!carryNames.TryGetValue(sarg.Name, out carry)) carry = 0;
                     if (!shiftNames.TryGetValue(sarg.Name, out shift)) shift = 0;
                     shiftNames[sarg.Name] = shift + 8 * argLength;
                     carryNames[sarg.Name] = carry + (data.ReadMultiByteValue(start, argLength) << shift);
                  }
               }
               sarg.Build(false, data, start, tempBuilder, streamContent, shift, carry, labels, streamTypes);
               args.Add(tempBuilder.ToString());
               i += 1;
            }
            start += arg.Length(data, start);
         }
         if (args.Count > 0) {
            builder.Append(" ");
            builder.Append(" ".Join(ConvertLongFormToShortForm(args.ToArray())));
         }
         foreach (var content in streamContent) {
            builder.AppendLine();
            builder.AppendLine("{");
            builder.AppendLine(content);
            builder.Append("}");
         }
         return builder.ToString();
      }

      public bool CanCompile(string line) {
         var tokens = ScriptLine.Tokenize(line);
         if (tokens.Length == 0) return false;
         if (tokens[0] != LineCommand) return false;
         return true;
      }

      public string ErrorCheck(string scriptLine, out string[] tokens) {
         tokens = ScriptLine.Tokenize(scriptLine);
         for (int i = 1; i < scriptLine.Length - 1; i++) {
            if (scriptLine[i] != '"') continue;
            if (scriptLine[i - 1] != ' ' && scriptLine[i + 1] != ' ') return "Cannot have \"quotes\" in the middle of a name.";
         }
         if (tokens[0] != LineCommand) throw new ArgumentException($"Command {LineCommand} was expected, but received {tokens[0]} instead.");
         var args = tokens.Skip(1).ToArray();
         var shortArgs = args;
         var commandText = LineCommand;
         if (hasShortForm && shortArgs.Length != ShortFormArgs.Count) {
            return $"Command {commandText} expects {ShortFormArgs.Count} arguments, but received {shortArgs.Length} instead.";
         }
         args = ConvertShortFormToLongForm(args);
         var specifiedArgs = Args.Where(arg => arg is ScriptArg).Count();
         if (!hasShortForm && specifiedArgs != args.Length) {
            return $"Command {commandText} expects {specifiedArgs} arguments, but received {shortArgs.Length} instead.";
         }
         return null;
      }

      public string Compile(IDataModel model, int start, string scriptLine, LabelLibrary labels, out byte[] result) {
         result = null;
         var error = ErrorCheck(scriptLine, out var tokens);
         if (error != null) return error;
         var args = tokens.Skip(1).ToArray();
         args = ConvertShortFormToLongForm(args);
         var results = new List<byte>();
         var specifiedArgIndex = 0;
         var shiftNames = new Dictionary<string, int>();
         for (int i = 0; i < Args.Count; i++) {
            if (Args[i] is ScriptArg scriptArg) {
               int shift = 0;
               if (!shortIndexFromLongIndex.ContainsKey(i)) {
                  // the arguments length doesn't match the short version.
                  // pull the current shift
                  if (!shiftNames.TryGetValue(scriptArg.Name, out shift)) shift = 0;
                  shiftNames[scriptArg.Name] = shift + scriptArg.Length(model, start + results.Count) * 8;
               }
               var token = args[specifiedArgIndex];
               var message = scriptArg.Build(model, start + results.Count, token, shift, results, labels);
               if (message != null) return message;
               specifiedArgIndex += 1;
            } else if (Args[i] is SilentMatchArg silentArg) {
               results.Add(silentArg.ExpectedValue);
            }
         }
         result = results.ToArray();
         return null;
      }

      public void AddDocumentation(string doc) => documentation.Add(doc);

      private string[] ConvertShortFormToLongForm(string[] args) {
         if (!hasShortForm || args.Length == 0) return args;
         // build long-form args from this short form
         var longForm = new List<string>();
         for (int i = 0; i < Args.Count; i++) {
            if (Args[i] is SilentMatchArg) continue;
            if (shortIndexFromLongIndex.TryGetValue(i, out var shortIndex)) {
               if (shortIndex < args.Length) longForm.Add(args[shortIndex]);
            } else {
               var shortformIndex = ShortFormArgs.Count.Range().FirstOrDefault(i => ShortFormArgs[i].Name == Args[i].Name);
               longForm.Add(args[shortformIndex]);
            }
         }
         return longForm.ToArray();
      }

      private string[] ConvertLongFormToShortForm(string[] args) {
         if (!hasShortForm) return args;
         var shortForm = new Dictionary<int, string>();
         int j = 0;

         for (int i = 0; i < Args.Count; i++) {
            if (Args[i] is SilentMatchArg) continue;
            if (shortIndexFromLongIndex.TryGetValue(i, out var shortIndex)) {
               shortForm[shortIndex] = args[j]; // this prefers to use the last instance of the argument, if the argument appears multiple times. Needs to work this way for split-byte args.
            } else {
               var shortformIndex = ShortFormArgs.Count.Range().FirstOrDefault(i => ShortFormArgs[i].Name == Args[i].Name);
               shortForm[shortformIndex] = args[j];
            }
            j += 1;
         }
         return shortForm.Count.Range(i => shortForm[i]).ToArray();
      }
   }

   public abstract class ScriptLine : IScriptLine {
      private readonly List<string> documentation = new List<string>();
      private readonly IReadOnlyList<int> matchingGames;

      public const string Hex = "0123456789ABCDEF";
      public IReadOnlyList<IScriptArg> Args { get; }
      public IReadOnlyList<byte> LineCode { get; }
      public string LineCommand { get; }
      public IReadOnlyList<string> Documentation => documentation;
      public string Usage { get; }

      public virtual bool IsEndingCommand { get; }

      /// <param name="destinationLengths">If this line contains pointers, calculate the pointer data's lengths and include here.</param>
      public int CompiledByteLength(IDataModel model, int start, IDictionary<int, int> destinationLengths) {
         var length = LineCode.Count;
         foreach (var arg in Args) {
            if (arg.Type == ArgType.Pointer) {
               var destination = model.ReadPointer(start + length);
               if (destinationLengths != null && !destinationLengths.ContainsKey(destination)) {
                  var argLength = ScriptParser.GetArgLength(model, arg, start + length, destinationLengths);
                  if (argLength > 0) destinationLengths[destination] = argLength;
               }
            }
            length += arg.Length(model, start + length);
         }
         return length;
      }
      public int CompiledByteLength(IDataModel model, string line) {
         var length = LineCode.Count;
         var tokens = line.Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
         for (var i = 0; i < Args.Count; i++) {
            if (Args[i] is ScriptArg sarg) length += sarg.Length(default, -1);
            if (Args[i] is ArrayArg aarg) length += aarg.ConvertMany(model, tokens.Skip(i + 1)).Count() * aarg.TokenLength + 1;
         }
         return length;
      }

      public ScriptLine(string engineLine) {
         var docSplit = engineLine.Split(new[] { '#' }, 2);
         if (docSplit.Length > 1) documentation.Add('#' + docSplit[1]);
         engineLine = docSplit[0].Trim();
         matchingGames = ExtractMatchingGames(ref engineLine);
         Usage = engineLine.Split(new[] { ' ' }, 2).Last();
         var usageTokens = Usage.Split(" ", StringSplitOptions.RemoveEmptyEntries);
         Usage = usageTokens[0] + " " + " ".Join(usageTokens.Skip(1).Select(t => {
            var hasBraces = t.StartsWith('[') && t.EndsWith(']');
            if (hasBraces) t = t.Substring(1, t.Length - 2);
            t = t.Split(".:|<".ToCharArray())[0];
            if (hasBraces) t = $"[{t}]";
            return t;
         }));

         var tokens = engineLine.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
         var lineCode = new List<byte>();
         var args = new List<IScriptArg>();

         foreach (var token in tokens) {
            if (token.Length == 2 && token.All(ViewPort.AllHexCharacters.Contains)) {
               lineCode.Add(byte.Parse(token, NumberStyles.HexNumber));
            } else if (token.StartsWith("[") && token.EndsWith("]")) {
               var content = token.Substring(1, token.Length - 2);
               args.Add(new ArrayArg(content));
            } else if (ScriptArg.IsValidToken(token)) {
               args.Add(new ScriptArg(token));
            } else {
               LineCommand = token;
            }
         }

         LineCode = lineCode;
         Args = args;
      }

      public static IReadOnlyList<int> ExtractMatchingGames(ref string line) {
         if (!line.StartsWith("[")) return null;
         var gamesEnd = line.IndexOf("]");
         if (gamesEnd == -1) return null;
         var games = line.Substring(1, gamesEnd - 1);
         line = line.Substring(gamesEnd + 1).TrimStart();
         return games.Split("_").Select(ConvertAscii).ToList();
      }

      public static IReadOnlyList<string> GetMatchingGames(IScriptLine line) {
         var names = new[] { "AXVE", "AXPE", "BPRE", "BPGE", "BPEE" };
         return names.Where(name => line.MatchesGame(ConvertAscii(name))).ToList();
      }

      public static int ConvertAscii(string letters) {
         return letters.Reverse().Aggregate(0, (current, letter) => (current << 8) | (byte)letter);
      }

      public bool MatchesGame(int game) => matchingGames?.Contains(game) ?? true;

      public void AddDocumentation(string doc) => documentation.Add(doc);

      public bool PartialMatchLine(string line) => LineCommand.MatchesPartial(line.Split(' ')[0]);

      public bool Matches(int gameCodeHash, IReadOnlyList<byte> data, int index) {
         if (index + LineCode.Count >= data.Count) return false;
         if (MatchesGame(gameCodeHash)) {
            var result = true;
            for (int i = 0; result && i < LineCode.Count; i++) result = data[index + i] == LineCode[i]; // avoid making lambda for performance
            return result;
         }
         return false;
      }

      public bool CanCompile(string line) {
         if (!(line + " ").StartsWith(LineCommand + " ", StringComparison.CurrentCultureIgnoreCase)) return false;
         if (LineCode.Count == 1) return true;
         var tokens = Tokenize(line).ToList();
         if (tokens.Count < LineCode.Count) return false;
         tokens.RemoveAt(0);
         for (int i = 1; i < LineCode.Count; i++) {
            if (!byte.TryParse(tokens[0], NumberStyles.HexNumber, CultureInfo.CurrentCulture, out var value)) return false;
            if (value != LineCode[i]) return false;
            tokens.RemoveAt(0);
         }
         return true;
      }

      public string ErrorCheck(string scriptLine, out string[] tokens) {
         tokens = Tokenize(scriptLine);
         if (!tokens[0].Equals(LineCommand, StringComparison.CurrentCultureIgnoreCase)) throw new ArgumentException($"Command {LineCommand} was expected, but received {tokens[0]} instead.");
         var commandText = LineCommand;
         for (int i = 1; i < LineCode.Count; i++) commandText += " " + LineCode[i].ToString("X2");
         var fillerCount = Args.Count(arg => arg.Name == "filler");
         for (int i = 0; i < fillerCount; i++) {
            if (tokens.Length < Args.Count + LineCode.Count) tokens = tokens.Append("0").ToArray();
         }
         if (Args.Count > 0 && Args.Last() is ArrayArg) {
            if (Args.Count > tokens.Length) {
               return $"Command {commandText} expects {Args.Count} arguments, but received {tokens.Length - LineCode.Count} instead.";
            }
         } else if (Args.Count != tokens.Length - LineCode.Count) {
            return $"Command {commandText} expects {Args.Count} arguments, but received {tokens.Length - LineCode.Count} instead.";
         }
         return null;
      }

      public string Compile(IDataModel model, int start, string scriptLine, LabelLibrary labels, out byte[] result) {
         result = null;
         var error = ErrorCheck(scriptLine, out var tokens);
         if (error != null) return error;
         var results = new List<byte>(LineCode);
         start += LineCode.Count;
         for (int i = 0; i < Args.Count; i++) {
            if (Args[i] is ScriptArg scriptArg) {
               var token = tokens[i + LineCode.Count];
               var message = scriptArg.Build(model, start, token, results, labels);
               if (message != null) return message;
               start += scriptArg.Length(model, start);
            } else if (Args[i] is ArrayArg arrayArg) {
               var values = arrayArg.ConvertMany(model, tokens.Skip(i + 1)).ToList();
               results.Add((byte)values.Count);
               start += 1;
               foreach (var value in values) {
                  if (Args[i].Type == ArgType.Byte) {
                     results.Add((byte)value);
                     start += 1;
                  } else if (Args[i].Type == ArgType.Short) {
                     results.Add((byte)value);
                     results.Add((byte)(value >> 8));
                     start += 2;
                  } else if (Args[i].Type == ArgType.Word) {
                     results.Add((byte)value);
                     results.Add((byte)(value >> 0x8));
                     results.Add((byte)(value >> 0x10));
                     results.Add((byte)(value >> 0x18));
                     start += 4;
                  } else {
                     throw new NotImplementedException();
                  }
               }
            }
         }
         result = results.ToArray();
         return null;
      }

      public string Decompile(IDataModel data, int start, DecompileLabelLibrary labels, IList<ExpectedPointerType> streamTypes) {
         for (int i = 0; i < LineCode.Count; i++) {
            if (LineCode[i] != data[start + i]) throw new ArgumentException($"Data at {start:X6} does not match the {LineCommand} command.");
         }
         var allFillerIsZero = IsAllFillerZero(data, start);
         start += LineCode.Count;
         var builder = new StringBuilder(LineCommand);
         for (int i = 1; i < LineCode.Count; i++) {
            builder.Append(" " + LineCode[i].ToHexString());
         }

         var streamContent = new List<string>();
         foreach (var arg in Args) {
            builder.Append(" ");
            if (arg is ScriptArg scriptArg) {
               if (scriptArg.Build(allFillerIsZero, data, start, builder, streamContent, labels, streamTypes)) continue;
            } else if (arg is ArrayArg arrayArg) {
               builder.Append(arrayArg.ConvertMany(data, start));
            } else {
               throw new NotImplementedException();
            }
            start += arg.Length(data, start);
         }
         foreach (var content in streamContent) {
            builder.AppendLine();
            builder.AppendLine("{");
            builder.AppendLine(content);
            builder.Append("}");
         }
         return builder.ToString();
      }

      private bool IsAllFillerZero(IDataModel data, int start) {
         start += LineCode.Count;
         foreach (var arg in Args) {
            if (arg.Name == "filler") {
               var value = data.ReadMultiByteValue(start, arg.Length(data, start));
               if (value != 0) return false;
            }
            start += arg.Length(data, start);
         }
         return true;
      }

      public static string ReadString(IDataModel data, int start) {
         var length = PCSString.ReadString(data, start, true);
         return data.TextConverter.Convert(data, start, length);
      }

      public static string[] Tokenize(string scriptLine) {
         var result = new List<string>();
         var quoteCut = scriptLine.Split('"');

         for (int i = 0; i < quoteCut.Length; i++) {
            if (i % 2 == 0 && quoteCut[i].Length == 0) continue;

            if (i % 2 == 1) result.Add($"\"{quoteCut[i]}\"");
            else result.AddRange(quoteCut[i].Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries));
         }

         return result.ToArray();
      }

      public override string ToString() {
         return string.Join(" ", LineCode.Select(code => code.ToHexString()).Concat(Args.Select(arg => arg.Name)).ToArray());
      }
   }

   public class XSEScriptLine : ScriptLine {
      public XSEScriptLine(string engineLine) : base(engineLine) { }

      public override bool IsEndingCommand => LineCode.Count == 1 && LineCode[0].IsAny<byte>(0x02, 0x03, 0x05, 0x08, 0x0A, 0x0C, 0x0D);
   }

   public class BSEScriptLine : ScriptLine {
      public BSEScriptLine(string engineLine) : base(engineLine) { }

      public override bool IsEndingCommand => LineCode.Count == 1 && LineCode[0].IsAny<byte>(0x28, 0x3c, 0x3d, 0x3e, 0x3f, 0xef, 0xf6, 0xf7);
   }

   public class ASEScriptLine : ScriptLine {
      public ASEScriptLine(string engineLine) : base(engineLine) { }

      public override bool IsEndingCommand => LineCode.Count == 1 && LineCode[0].IsAny<byte>(0x08, 0x0F, 0x11, 0x13);
   }

   public class TSEScriptLine : ScriptLine {
      public TSEScriptLine(string engineLine) : base(engineLine) { }
      public override bool IsEndingCommand => LineCode.Count == 1 && LineCode[0].IsAny<byte>(0x45, 0x47, 0x59, 0x5A);
   }

}
