//public domain assumed from cyotek.com

using System;
using System.Reflection;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Xml;

namespace Cyotek.Drawing.BitmapFont
{
  // Parsing class for bitmap fonts generated by AngelCode BMFont
  // http://www.angelcode.com/products/bmfont/

  public static class BitmapFontLoader
  {
    #region  Public Class Methods

		///// <summary>
		///// Loads a bitmap font from a file, attempting to auto detect the file type
		///// </summary>
		///// <param name="fileName">Name of the file to load.</param>
		///// <returns></returns>
		//public static BitmapFont LoadFontFromFile(string fileName)
		//{
		//  BitmapFont result;

		//  if (string.IsNullOrEmpty(fileName))
		//    throw new ArgumentNullException("fileName", "File name not specified");
		//  else if (!File.Exists(fileName))
		//    throw new FileNotFoundException(string.Format("Cannot find file '{0}'", fileName), fileName);

		//  using (FileStream file = File.OpenRead(fileName))
		//  {
		//    using (TextReader reader = new StreamReader(file))
		//    {
		//      string line;

		//      line = reader.ReadLine();

		//      if (line.StartsWith("info "))
		//        result = BitmapFontLoader.LoadFontFromTextFile(fileName);
		//      else if (line.StartsWith("<?xml"))
		//        result = BitmapFontLoader.LoadFontFromXmlFile(fileName);
		//      else
		//        throw new InvalidDataException("Unknown file format.");
		//    }
		//  }

		//  return result;
		//}

    /// <summary>
    /// Loads a bitmap font from a text file.
    /// </summary>
    /// <param name="fileName">Name of the file to load.</param>
    /// <returns></returns>
    public static BitmapFont LoadFontFromTextFile(string fileName)
    {
      BitmapFont font;
      IDictionary<int, Page> pageData;
      IDictionary<Kerning, int> kerningDictionary;
      IDictionary<char, Character> charDictionary;
      string resourcePath;
      string[] lines;

      if (string.IsNullOrEmpty(fileName))
        throw new ArgumentNullException("fileName", "File name not specified");
      else if (!File.Exists(fileName))
        throw new FileNotFoundException(string.Format("Cannot find file '{0}'", fileName), fileName);

      pageData = new SortedDictionary<int, Page>();
      kerningDictionary = new Dictionary<Kerning, int>();
      charDictionary = new Dictionary<char, Character>();
      font = new BitmapFont();

      resourcePath = Path.GetDirectoryName(fileName);
      lines = File.ReadAllLines(fileName);

      foreach (string line in lines)
      {
        string[] parts;

        parts = BitmapFontLoader.Split(line, ' ');

        if (parts.Length != 0)
        {
          switch (parts[0])
          {
            case "info":
              font.FamilyName = BitmapFontLoader.GetNamedString(parts, "face");
              font.FontSize = BitmapFontLoader.GetNamedInt(parts, "size");
              font.Bold = BitmapFontLoader.GetNamedBool(parts, "bold");
              font.Italic = BitmapFontLoader.GetNamedBool(parts, "italic");
              font.Charset = BitmapFontLoader.GetNamedString(parts, "charset");
              font.Unicode = BitmapFontLoader.GetNamedBool(parts, "unicode");
              font.StretchedHeight = BitmapFontLoader.GetNamedInt(parts, "stretchH");
              font.Smoothed = BitmapFontLoader.GetNamedBool(parts, "smooth");
              font.SuperSampling = BitmapFontLoader.GetNamedInt(parts, "aa");
              font.Padding = BitmapFontLoader.ParsePadding(BitmapFontLoader.GetNamedString(parts, "padding"));
              font.Spacing = BitmapFontLoader.ParsePoint(BitmapFontLoader.GetNamedString(parts, "spacing"));
              font.OutlineSize = BitmapFontLoader.GetNamedInt(parts, "outline");
              break;
            case "common":
              font.LineHeight = BitmapFontLoader.GetNamedInt(parts, "lineHeight");
              font.BaseHeight = BitmapFontLoader.GetNamedInt(parts, "base");
              font.TextureSize = new Size
              (
                BitmapFontLoader.GetNamedInt(parts, "scaleW"),
                BitmapFontLoader.GetNamedInt(parts, "scaleH")
              );
              font.Packed = BitmapFontLoader.GetNamedBool(parts, "packed");
              font.AlphaChannel = BitmapFontLoader.GetNamedInt(parts, "alphaChnl");
              font.RedChannel = BitmapFontLoader.GetNamedInt(parts, "redChnl");
              font.GreenChannel = BitmapFontLoader.GetNamedInt(parts, "greenChnl");
              font.BlueChannel = BitmapFontLoader.GetNamedInt(parts, "blueChnl");
              break;
            case "page":
              int id;
              string name;
              string textureId;

              id = BitmapFontLoader.GetNamedInt(parts, "id");
              name = BitmapFontLoader.GetNamedString(parts, "file");
              textureId = Path.GetFileNameWithoutExtension(name);

              pageData.Add(id, new Page(id, Path.Combine(resourcePath, name)));
              break;
            case "char":
              Character charData;

              charData = new Character
              {
                Char = (char)BitmapFontLoader.GetNamedInt(parts, "id"),
                Bounds = new Rectangle
                (
                BitmapFontLoader.GetNamedInt(parts, "x"),
                BitmapFontLoader.GetNamedInt(parts, "y"),
                BitmapFontLoader.GetNamedInt(parts, "width"),
                BitmapFontLoader.GetNamedInt(parts, "height")
                ),
                Offset = new Point
                (
                  BitmapFontLoader.GetNamedInt(parts, "xoffset"),
                  BitmapFontLoader.GetNamedInt(parts, "yoffset")
                ),
                XAdvance = BitmapFontLoader.GetNamedInt(parts, "xadvance"),
                TexturePage = BitmapFontLoader.GetNamedInt(parts, "page"),
                Channel = BitmapFontLoader.GetNamedInt(parts, "chnl")
              };
              charDictionary.Add(charData.Char, charData);
              break;
            case "kerning":
              Kerning key;

              key = new Kerning((char)BitmapFontLoader.GetNamedInt(parts, "first"), (char)BitmapFontLoader.GetNamedInt(parts, "second"), GetNamedInt(parts, "amount"));

              if (!kerningDictionary.ContainsKey(key))
                kerningDictionary.Add(key, key.Amount);
              break;
          }
        }
      }

      font.Pages = BitmapFontLoader.ToArray(pageData.Values);
      font.Characters = charDictionary;
      font.Kernings = kerningDictionary;

      return font;
    }

    /// <summary>
    /// Loads a bitmap font from an XML file.
    /// </summary>
    /// <param name="fileName">Name of the file to load.</param>
    /// <returns></returns>
    public static BitmapFont LoadFontFromXmlFile(Stream stream)
    {
      XmlDocument document;
      BitmapFont font;
      IDictionary<int, Page> pageData;
      IDictionary<Kerning, int> kerningDictionary;
      IDictionary<char, Character> charDictionary;
      XmlNode root;
      XmlNode properties;

      document = new XmlDocument();
      pageData = new SortedDictionary<int, Page>();
      kerningDictionary = new Dictionary<Kerning, int>();
      charDictionary = new Dictionary<char, Character>();
      font = new BitmapFont();

      document.Load(stream);
      root = document.DocumentElement;

      // load the basic attributes
      properties = root.SelectSingleNode("info");
      font.FamilyName = properties.Attributes["face"].Value;
      font.FontSize = Convert.ToInt32(properties.Attributes["size"].Value);
      font.Bold = Convert.ToInt32(properties.Attributes["bold"].Value) != 0;
      font.Italic = Convert.ToInt32(properties.Attributes["italic"].Value) != 0;
      font.Unicode = Convert.ToInt32(properties.Attributes["unicode"].Value) != 0;
      font.StretchedHeight = Convert.ToInt32(properties.Attributes["stretchH"].Value);
      font.Charset = properties.Attributes["charset"].Value;
      font.Smoothed = Convert.ToInt32(properties.Attributes["smooth"].Value) != 0;
      font.SuperSampling = Convert.ToInt32(properties.Attributes["aa"].Value);
      font.Padding = BitmapFontLoader.ParsePadding(properties.Attributes["padding"].Value);
      font.Spacing = BitmapFontLoader.ParsePoint(properties.Attributes["spacing"].Value);
      font.OutlineSize = Convert.ToInt32(properties.Attributes["outline"].Value);

      // common attributes
      properties = root.SelectSingleNode("common");
      font.BaseHeight = Convert.ToInt32(properties.Attributes["lineHeight"].Value);
      font.LineHeight = Convert.ToInt32(properties.Attributes["base"].Value);
      font.TextureSize = new Size
      (
        Convert.ToInt32(properties.Attributes["scaleW"].Value),
        Convert.ToInt32(properties.Attributes["scaleH"].Value)
      );
      font.Packed = Convert.ToInt32(properties.Attributes["packed"].Value) != 0;
      font.AlphaChannel = Convert.ToInt32(properties.Attributes["alphaChnl"].Value);
      font.RedChannel = Convert.ToInt32(properties.Attributes["redChnl"].Value);
      font.GreenChannel = Convert.ToInt32(properties.Attributes["greenChnl"].Value);
      font.BlueChannel = Convert.ToInt32(properties.Attributes["blueChnl"].Value);

      // load texture information
      foreach (XmlNode node in root.SelectNodes("pages/page"))
      {
        Page page;

        page = new Page();
        page.Id = Convert.ToInt32(node.Attributes["id"].Value);
        page.FileName = node.Attributes["file"].Value;

        pageData.Add(page.Id, page);
      }
      font.Pages = BitmapFontLoader.ToArray(pageData.Values);

      // load character information
      foreach (XmlNode node in root.SelectNodes("chars/char"))
      {
        Character character;

        character = new Character();
        character.Char = (char)Convert.ToInt32(node.Attributes["id"].Value);
        character.Bounds = new Rectangle
        (
          Convert.ToInt32(node.Attributes["x"].Value),
          Convert.ToInt32(node.Attributes["y"].Value),
          Convert.ToInt32(node.Attributes["width"].Value),
          Convert.ToInt32(node.Attributes["height"].Value)
        );
        character.Offset = new Point
        (
          Convert.ToInt32(node.Attributes["xoffset"].Value),
          Convert.ToInt32(node.Attributes["yoffset"].Value)
        );
        character.XAdvance = Convert.ToInt32(node.Attributes["xadvance"].Value);
        character.TexturePage = Convert.ToInt32(node.Attributes["page"].Value);
        character.Channel = Convert.ToInt32(node.Attributes["chnl"].Value);

        charDictionary.Add(character.Char, character);
      }
      font.Characters = charDictionary;

      // loading kerning information
      foreach (XmlNode node in root.SelectNodes("kernings/kerning"))
      {
        Kerning key;

        key = new Kerning((char)Convert.ToInt32(node.Attributes["first"].Value), (char)Convert.ToInt32(node.Attributes["second"].Value), Convert.ToInt32(node.Attributes["amount"].Value));

        if (!kerningDictionary.ContainsKey(key))
          kerningDictionary.Add(key, key.Amount);
      }
      font.Kernings = kerningDictionary;

      return font;
    }

    #endregion  Public Class Methods

    #region  Private Class Methods

    /// <summary>
    /// Returns a boolean from an array of name/value pairs.
    /// </summary>
    /// <param name="parts">The array of parts.</param>
    /// <param name="name">The name of the value to return.</param>
    /// <returns></returns>
    private static bool GetNamedBool(string[] parts, string name)
    {
      return BitmapFontLoader.GetNamedInt(parts, name) != 0;
    }

    /// <summary>
    /// Returns an integer from an array of name/value pairs.
    /// </summary>
    /// <param name="parts">The array of parts.</param>
    /// <param name="name">The name of the value to return.</param>
    /// <returns></returns>
    private static int GetNamedInt(string[] parts, string name)
    {
      return Convert.ToInt32(BitmapFontLoader.GetNamedString(parts, name));
    }

    /// <summary>
    /// Returns a string from an array of name/value pairs.
    /// </summary>
    /// <param name="parts">The array of parts.</param>
    /// <param name="name">The name of the value to return.</param>
    /// <returns></returns>
    private static string GetNamedString(string[] parts, string name)
    {
      string result;

      result = string.Empty;
      name = name.ToLowerInvariant();

      foreach (string part in parts)
      {
        int nameEndIndex;

        nameEndIndex = part.IndexOf("=");
        if (nameEndIndex != -1)
        {
          string namePart;
          string valuePart;

          namePart = part.Substring(0, nameEndIndex).ToLowerInvariant();
          valuePart = part.Substring(nameEndIndex + 1);

          if (namePart == name)
          {
            if (valuePart.StartsWith("\"") && valuePart.EndsWith("\""))
              valuePart = valuePart.Substring(1, valuePart.Length - 2);

            result = valuePart;
            break;
          }
        }
      }

      return result;
    }

    /// <summary>
    /// Creates a Padding object from a string representation
    /// </summary>
    /// <param name="s">The string.</param>
    /// <returns></returns>
    private static Padding ParsePadding(string s)
    {
      string[] parts;

      parts = s.Split(',');

      return new Padding()
      {
        Left = Convert.ToInt32(parts[3].Trim()),
        Top = Convert.ToInt32(parts[0].Trim()),
        Right = Convert.ToInt32(parts[1].Trim()),
        Bottom = Convert.ToInt32(parts[2].Trim())
      };
    }

    /// <summary>
    /// Creates a Point object from a string representation
    /// </summary>
    /// <param name="s">The string.</param>
    /// <returns></returns>
    private static Point ParsePoint(string s)
    {
      string[] parts;

      parts = s.Split(',');

      return new Point()
      {
        X = Convert.ToInt32(parts[0].Trim()),
        Y = Convert.ToInt32(parts[1].Trim())
      };
    }

    /// <summary>
    /// Splits the specified string using a given delimiter, ignoring any instances of the delimiter as part of a quoted string.
    /// </summary>
    /// <param name="s">The string to split.</param>
    /// <param name="delimiter">The delimiter.</param>
    /// <returns></returns>
    private static string[] Split(string s, char delimiter)
    {
      string[] results;

      if (s.Contains("\""))
      {
        List<string> parts;
        int partStart;

        partStart = -1;
        parts = new List<string>();

        do
        {
          int partEnd;
          int quoteStart;
          int quoteEnd;
          bool hasQuotes;

          quoteStart = s.IndexOf("\"", partStart + 1);
          quoteEnd = s.IndexOf("\"", quoteStart + 1);
          partEnd = s.IndexOf(delimiter, partStart + 1);

          if (partEnd == -1)
            partEnd = s.Length;

          hasQuotes = quoteStart != -1 && partEnd > quoteStart && partEnd < quoteEnd;
          if (hasQuotes)
            partEnd = s.IndexOf(delimiter, quoteEnd + 1);

          parts.Add(s.Substring(partStart + 1, partEnd - partStart - 1));

          if (hasQuotes)
            partStart = partEnd - 1;

          partStart = s.IndexOf(delimiter, partStart + 1);
        } while (partStart != -1);

        results = parts.ToArray();
      }
      else
        results = s.Split(new char[] { delimiter }, StringSplitOptions.RemoveEmptyEntries);

      return results;
    }

    /// <summary>
    /// Converts the given collection into an array
    /// </summary>
    /// <typeparam name="T">Type of the items in the array</typeparam>
    /// <param name="values">The values.</param>
    /// <returns></returns>
    private static T[] ToArray<T>(ICollection<T> values)
    {
      T[] result;

      // avoid a forced .NET 3 dependency just for one call to Linq

      result = new T[values.Count];
      values.CopyTo(result, 0);

      return result;
    }

    #endregion  Private Class Methods
  }
}
