context: using System;
  using System.Collections.Generic;
  using Server.Network;
  
  namespace Server.Items
  {
      public struct BulletinEquip
      {
          public int itemID;
          public int hue;
          public BulletinEquip(int itemID, int hue)
          {
              this.itemID = itemID;
              this.hue = hue;
          }
      }
  
      [Flipable(0x1E5E, 0x1E5F)]
      public class BulletinBoard : BaseBulletinBoard
      {
          [Constructable]
          public BulletinBoard()
              : base(0x1E5E)
          {
          }
  
          public BulletinBoard(Serial serial)
              : base(serial)
          {
          }
  
          public override void Serialize(GenericWriter writer)
          {
              base.Serialize(writer);
  
              writer.Write((int)0); // version
          }
  
          public override void Deserialize(GenericReader reader)
          {
              base.Deserialize(reader);
  
              int version = reader.ReadInt();
          }
      }
  
      public abstract class BaseBulletinBoard : Item
      {
          // Threads will be removed six hours after the last post was made
          private static readonly TimeSpan ThreadDeletionTime = TimeSpan.FromHours(6.0);
          // A player may only create a thread once every two minutes
          private static readonly TimeSpan ThreadCreateTime = TimeSpan.FromMinutes(2.0);
          // A player may only reply once every thirty seconds
          private static readonly TimeSpan ThreadReplyTime = TimeSpan.FromSeconds(30.0);
          private string m_BoardName;
          public BaseBulletinBoard(int itemID)
              : base(itemID)
          {
              m_BoardName = "bulletin board";
              Movable = false;
          }
  
          public BaseBulletinBoard(Serial serial)
              : base(serial)
          {
          }
  
          [CommandProperty(AccessLevel.GameMaster)]
          public string BoardName
          {
              get
              {
                  return m_BoardName;
              }
              set
              {
                  m_BoardName = value;
              }
          }
          public static bool CheckTime(DateTime time, TimeSpan range)
          {
              return (time + range) < DateTime.UtcNow;
          }
  
          public static string FormatTS(TimeSpan ts)
          {
              int totalSeconds = (int)ts.TotalSeconds;
              int seconds = totalSeconds % 60;
              int minutes = totalSeconds / 60;
  
              if (minutes != 0 && seconds != 0)
              {
                  return String.Format("{0} minute{1} and {2} second{3}", minutes, minutes == 1 ? "" : "s", seconds, seconds == 1 ? "" : "s");
              }
              else if (minutes != 0)
              {
                  return String.Format("{0} minute{1}", minutes, minutes == 1 ? "" : "s");
              }
              else
              {
                  return String.Format("{0} second{1}", seconds, seconds == 1 ? "" : "s");
              }
          }
  
          public static void Initialize()
          {
              PacketHandlers.Register(0x71, 0, true, new OnPacketReceive(BBClientRequest));
          }
  
          public static void BBClientRequest(NetState state, PacketReader pvSrc)
          {
              Mobile from = state.Mobile;
  
              int packetID = pvSrc.ReadByte();
              BaseBulletinBoard board = World.FindItem(pvSrc.ReadInt32()) as BaseBulletinBoard;
  
              if (board == null || !board.CheckRange(from))
              {
                  return;
              }
  
              switch ( packetID )
              {
                  case 3:
                      BBRequestContent(from, board, pvSrc);
                      break;
                  case 4:
                      BBRequestHeader(from, board, pvSrc);
                      break;
                  case 5:
                      BBPostMessage(from, board, pvSrc);
                      break;
                  case 6:
                      BBRemoveMessage(from, board, pvSrc);
                      break;
              }
          }
  
          public static void BBRequestContent(Mobile from, BaseBulletinBoard board, PacketReader pvSrc)
          {
              BulletinMessage msg = World.FindItem(pvSrc.ReadInt32()) as BulletinMessage;
  
              if (msg == null || msg.Parent != board)
              {
                  return;
              }
  
              from.Send(new BBMessageContent(board, msg));
          }
  
          public static void BBRequestHeader(Mobile from, BaseBulletinBoard board, PacketReader pvSrc)
          {
              BulletinMessage msg = World.FindItem(pvSrc.ReadInt32()) as BulletinMessage;
  
              if (msg == null || msg.Parent != board)
              {
                  return;
              }
  
              from.Send(new BBMessageHeader(board, msg));
          }
  
          public static void BBPostMessage(Mobile from, BaseBulletinBoard board, PacketReader pvSrc)
          {
              BulletinMessage thread = World.FindItem(pvSrc.ReadInt32()) as BulletinMessage;
  
              if (thread != null && thread.Parent != board)
              {
                  thread = null;
              }
  
              int breakout = 0;
  
              while (thread != null && thread.Thread != null && breakout++ < 10)
                  thread = thread.Thread;
  
              DateTime lastPostTime = DateTime.MinValue;
  
              if (board.GetLastPostTime(from, (thread == null), ref lastPostTime))
              {
                  if (!CheckTime(lastPostTime, (thread == null ? ThreadCreateTime : ThreadReplyTime)))
                  {
                      if (thread == null)
                      {
                          from.SendMessage("You must wait {0} before creating a new thread.", FormatTS(ThreadCreateTime));
                      }
                      else
                      {
                          from.SendMessage("You must wait {0} before replying to another thread.", FormatTS(ThreadReplyTime));
                      }
  
                      return;
                  }
              }
  
              string subject = pvSrc.ReadUTF8StringSafe(pvSrc.ReadByte());
  
              if (subject.Length == 0)
              {
                  return;
              }
  
              string[] lines = new string[pvSrc.ReadByte()];
  
              if (lines.Length == 0)
              {
                  return;
              }
  
              for (int i = 0; i < lines.Length; ++i)
              {
                  lines[i] = pvSrc.ReadUTF8StringSafe(pvSrc.ReadByte());
              }
  
              board.PostMessage(from, thread, subject, lines);
          }
  
          public static void BBRemoveMessage(Mobile from, BaseBulletinBoard board, PacketReader pvSrc)
          {
              BulletinMessage msg = World.FindItem(pvSrc.ReadInt32()) as BulletinMessage;
  
              if (msg == null || msg.Parent != board)
              {
                  return;
              }
  
              if (from.AccessLevel < AccessLevel.GameMaster && msg.Poster != from)
              {
                  return;
              }
  
              msg.Delete();
          }
  
          public virtual void Cleanup()
          {
              List<Item> items = Items;
  
              for (int i = items.Count - 1; i >= 0; --i)
              {
                  if (i >= items.Count)
                  {
                      continue;
                  }
  
                  BulletinMessage msg = items[i] as BulletinMessage;
  
                  if (msg == null)
                  {
                      continue;
                  }
  
                  if (msg.Thread == null && CheckTime(msg.LastPostTime, ThreadDeletionTime))
                  {
                      msg.Delete();
                      RecurseDelete(msg); // A root-level thread has expired
                  }
              }
          }
  
          public virtual bool GetLastPostTime(Mobile poster, bool onlyCheckRoot, ref DateTime lastPostTime)
          {
              List<Item> items = Items;
              bool wasSet = false;
  
              for (int i = 0; i < items.Count; ++i)
              {
                  BulletinMessage msg = items[i] as BulletinMessage;
  
                  if (msg == null || msg.Poster != poster)
                  {
                      continue;
                  }
  
                  if (onlyCheckRoot && msg.Thread != null)
                  {
                      continue;
                  }
  
                  if (msg.Time > lastPostTime)
                  {
                      wasSet = true;
                      lastPostTime = msg.Time;
                  }
              }
  
              return wasSet;
          }
  
          public override void OnDoubleClick(Mobile from)
          {
              if (CheckRange(from))
              {
                  Cleanup();
  
                  NetState state = from.NetState;
  
                  state.Send(new BBDisplayBoard(this));
                  if (state.ContainerGridLines)
                      state.Send(new ContainerContent6017(from, this));
                  else
                      state.Send(new ContainerContent(from, this));
              }
              else
              {
                  from.LocalOverheadMessage(MessageType.Regular, 0x3B2, 1019045); // I can't reach that.
              }
          }
  
          public virtual bool CheckRange(Mobile from)
          {
              if (from.AccessLevel >= AccessLevel.GameMaster)
              {
                  return true;
              }
  
              return (from.Map == Map && from.InRange(GetWorldLocation(), 2));
          }
  
          public void PostMessage(Mobile from, BulletinMessage thread, string subject, string[] lines)
          {
              if (thread != null)
                  thread.LastPostTime = DateTime.UtcNow;
  
              AddItem(new BulletinMessage(from, thread, subject, lines));
          }
  
          public override void Serialize(GenericWriter writer)
          {
              base.Serialize(writer);
  
              writer.Write((int)0); // version
  
              writer.Write((string)m_BoardName);
          }
  
          public override void Deserialize(GenericReader reader)
          {
              base.Deserialize(reader);
  
              int version = reader.ReadInt();
  
              switch ( version )
              {
                  case 0:
                      {
                          m_BoardName = reader.ReadString();
                          break;
                      }
              }
          }
  
          private void RecurseDelete(BulletinMessage msg)
          {
  
gt:             List<Item> found = new List<Item>();