context: using System;
  using System.Collections.Generic;
  using System.IO;
  using System.Linq;
  using System.Text;
  using Kuriimu.IO;
  using Cetera.Hash;
  
  namespace text_msbt
  {
      public sealed class MSBT
      {
          public const string LabelFilter = @"^[a-zA-Z0-9_]+$";
          public const uint LabelHashMagic = 0x492;
          public const int LabelMaxLength = 64;
  
          private const long _byteOrderOffset = 0x8;
          private const long _headerSize = 0x20;
          private byte _paddingChar = 0xAB;
  
          public Header Header = new Header();
          public LBL1 LBL1 = new LBL1();
          public NLI1 NLI1 = new NLI1();
          public ATO1 ATO1 = new ATO1();
          public ATR1 ATR1 = new ATR1();
          public TSY1 TSY1 = new TSY1();
          public TXT2 TXT2 = new TXT2();
          public Encoding Encoding = Encoding.Unicode;
          public List<string> SectionOrder = new List<string>();
          public bool HasLabels;
  
          public MSBT(Stream input)
          {
              using (var br = new BinaryReaderX(input))
              {
                  // Get ByteOrder
                  br.BaseStream.Position = _byteOrderOffset;
                  br.ByteOrder = (ByteOrder)br.ReadUInt16();
                  br.BaseStream.Position = 0;
  
                  // Header
                  Header = br.ReadStruct<Header>();
                  Encoding = Header.EncodingByte == EncodingByte.UTF8 ? Encoding.UTF8 : Header.ByteOrder == ByteOrder.BigEndian ? Encoding.BigEndianUnicode : Encoding.Unicode;
  
                  if (Header.Magic != "MsgStdBn")
                      throw new InvalidMSBTException("The file provided is not a valid MSBT file.");
                  if (Header.FileSize != br.BaseStream.Length)
                      throw new InvalidMSBTException("The file provided is not a valid MSBT file. Filesize mismtach.");
  
                  for (var i = 0; i < Header.NumberOfSections; i++)
                  {
                      switch (br.PeekString())
                      {
                          case "LBL1":
                              ReadLBL1(br);
                              SectionOrder.Add("LBL1");
                              break;
                          case "NLI1":
                              ReadNLI1(br);
                              SectionOrder.Add("NLI1");
                              break;
                          case "ATO1":
                              ReadATO1(br);
                              SectionOrder.Add("ATO1");
                              break;
                          case "ATR1":
                              ReadATR1(br);
                              SectionOrder.Add("ATR1");
                              break;
                          case "TSY1":
                              ReadTSY1(br);
                              SectionOrder.Add("TSY1");
                              break;
                          case "TXT2":
                              ReadTXT2(br);
                              SectionOrder.Add("TXT2");
                              break;
                      }
                  }
              }
          }
  
          // Tools
          public string GetString(byte[] bytes)
          {
              var sb = new StringBuilder();
              using (var br = new BinaryReaderX(new MemoryStream(bytes), Encoding, Header.ByteOrder))
              {
                  while (br.BaseStream.Length != br.BaseStream.Position)
                  {
                      var c = br.ReadChar();
                      sb.Append(c);
                      if (c == 0xE)
                      {
                          sb.Append((char)br.ReadInt16());
                          sb.Append((char)br.ReadInt16());
                          int count = br.ReadInt16();
                          sb.Append((char)count);
                          for (var i = 0; i < count; i++)
                          {
                              sb.Append((char)br.ReadByte());
                          }
                      }
                  }
              }
              return sb.Remove(sb.Length - 1, 1).ToString();
          }
  
          public byte[] GetBytes(string str)
          {
              var ms = new MemoryStream();
              using (var bw = new BinaryWriterX(ms, Encoding, Header.ByteOrder))
              {
                  for (var i = 0; i < str.Length; i++)
                  {
                      var c = str[i];
                      bw.Write(c);
                      if (c == 0xE)
                      {
                          bw.Write((short)str[++i]);
                          bw.Write((short)str[++i]);
                          int count = str[++i];
                          bw.Write((short)count);
                          for (var j = 0; j < count; j++)
                          {
                              bw.Write((byte)str[++i]);
                          }
                      }
                  }
                  bw.Write('\0');
              }
              return ms.ToArray();
          }
  
          // Reading
          private void ReadLBL1(BinaryReaderX br)
          {
              LBL1.Section = br.ReadStruct<Section>();
              LBL1.NumberOfGroups = br.ReadUInt32();
  
              LBL1.Groups = br.ReadMultiple<Group>((int)LBL1.NumberOfGroups).ToList();
  
              foreach (Group grp in LBL1.Groups)
                  for (var i = 0; i < grp.NumberOfLabels; i++)
                      LBL1.Labels.Add(new Label
                      {
                          Name = br.ReadString(Convert.ToInt32(br.ReadByte())),
                          Index = br.ReadUInt32(),
                          Checksum = (uint)LBL1.Groups.IndexOf(grp)
                      });
  
              //// Old rename correction
              //foreach (var lbl in LBL1.Labels)
              //{
              //    var previousChecksum = lbl.Checksum;
              //    lbl.Checksum = SimpleHash.Create(lbl.Name, LabelHashMagic, LBL1.NumberOfGroups);
  
              //    if (previousChecksum == lbl.Checksum) continue;
              //    LBL1.Groups[(int)previousChecksum].NumberOfLabels -= 1;
              //    LBL1.Groups[(int)lbl.Checksum].NumberOfLabels += 1;
              //}
  
              HasLabels = LBL1.Labels.Count > 0;
  
              _paddingChar = br.SeekAlignment(_paddingChar);
          }
  
          private void ReadNLI1(BinaryReaderX br)
          {
              NLI1.Section = br.ReadStruct<Section>();
              NLI1.Unknown = br.ReadBytes((int)NLI1.Section.Size); // Read in the entire section at once since we don't know what it's for
  
              _paddingChar = br.SeekAlignment(_paddingChar);
          }
  
          private void ReadATO1(BinaryReaderX br)
          {
              ATO1.Section = br.ReadStruct<Section>();
              ATO1.Unknown = br.ReadBytes((int)ATO1.Section.Size); // Read in the entire section at once since we don't know what it's for
  
              _paddingChar = br.SeekAlignment(_paddingChar); // TODO: Determine why this line was missing before
          }
  
          private void ReadATR1(BinaryReaderX br)
          {
              ATR1.Section = br.ReadStruct<Section>();
              ATR1.Unknown = br.ReadBytes((int)ATR1.Section.Size); // Read in the entire section at once since we don't know what it's for
  
              _paddingChar = br.SeekAlignment(_paddingChar);
          }
  
          private void ReadTSY1(BinaryReaderX br)
          {
              TSY1.Section = br.ReadStruct<Section>();
              TSY1.Unknown = br.ReadBytes((int)TSY1.Section.Size); // Read in the entire section at once since we don't know what it's for
  
              _paddingChar = br.SeekAlignment(_paddingChar);
          }
  
          private void ReadTXT2(BinaryReaderX br)
          {
              TXT2.Section = br.ReadStruct<Section>();
              var startOfStrings = (int)br.BaseStream.Position;
              TXT2.NumberOfStrings = br.ReadUInt32();
  
              var offsets = br.ReadMultiple<uint>((int)TXT2.NumberOfStrings);
  
              for (var i = 0; i < TXT2.NumberOfStrings; i++)
                  TXT2.Strings.Add(new String
                  {
                      Text = GetString(br.ReadBytes((i + 1 < offsets.Count ? startOfStrings + (int)offsets[i + 1] : startOfStrings + (int)TXT2.Section.Size) - (startOfStrings + (int)offsets[i]))),
                      Index = (uint)i
                  });
  
              // Tie in LBL1 labels
              foreach (var lbl in LBL1.Labels)
                  lbl.String = TXT2.Strings[(int)lbl.Index];
  
              _paddingChar = br.SeekAlignment(_paddingChar);
          }
  
          // Manipulation
          public Label AddLabel(string labelName)
          {
              var newString = new String();
              TXT2.Strings.Add(newString);
              TXT2.NumberOfStrings += 1;
  
              var newLabel = new Label
              {
                  Name = labelName.Trim(),
                  Index = (uint)TXT2.Strings.IndexOf(newString),
                  Checksum = SimpleHash.Create(labelName.Trim(), LabelHashMagic, LBL1.NumberOfGroups),
                  String = newString
              };
              LBL1.Labels.Add(newLabel);
              LBL1.Groups[(int)newLabel.Checksum].NumberOfLabels += 1;
              //ATR1.NumberOfAttributes += 1;
  
              return newLabel;
          }
  
          public void RenameLabel(Label label, string labelName)
          {
              label.Name = labelName.Trim();
              LBL1.Groups[(int)label.Checksum].NumberOfLabels -= 1;
              label.Checksum = SimpleHash.Create(labelName.Trim(), LabelHashMagic, LBL1.NumberOfGroups);
              LBL1.Groups[(int)label.Checksum].NumberOfLabels += 1;
          }
  
          public void RemoveLabel(Label label)
          {
              var textIndex = TXT2.Strings.IndexOf(label.String);
  
gt:             for (var i = 0; i < TXT2.NumberOfStrings; i++)