using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.IO;
using System.Windows.Media.Media3D;

namespace Dialect
{
    /// <summary>
    /// Controls the generation and parsing of manifest documents.
    /// </summary>
    public class Manifest
    {
        public Manifest()
        {
            Plugins = new List<PluginControl>();
            Color = Properties.Settings.Default.BackgroundColor;
        }

        /// <summary>
        /// The location of the manifest on disk.
        /// </summary>
        public String Path { get; set; }

        /// <summary>
        /// The visualisation name.
        /// </summary>
        public String Name { get; set; }

        /// <summary>
        /// The visualisation color.
        /// </summary>
        public Color Color { get; set; }

        /// <summary>
        /// The generated output string.
        /// </summary>
        public String Output { get; set; }

        /// <summary>
        /// The list of content plugins.
        /// </summary>
        public List<PluginControl> Plugins { get; set; }

        /// <summary>
        /// Writes to pre-set path location.
        /// </summary>
        public void Write()
        {
            Write(String.Empty);
        }

        /// <summary>
        /// Reads from pre-set path location.
        /// </summary>
        /// <param name="pluginManager">The PluginManager assistant.</param>
        public void Read(PluginManager pluginManager)
        {
            Read(pluginManager, null);
        }

        /// <summary>
        /// Reads a manifest file.
        /// </summary>
        /// <param name="pluginManager">The PluginManager assistant.</param>
        /// <param name="readPath">The location to read from.</param>
        public void Read(PluginManager pluginManager, String readPath)
        {
            String rawData = String.Empty;
            Char sectionMarker = '#';
            List<Plugin> uniquePlugins = new List<Plugin>();

            if (readPath == null)
            {
                // If the manifest has not been generated.
                if (!String.IsNullOrEmpty(Output))
                    rawData = Output;
                else
                    return;

                readPath = Path;
            }
            else
            {
                // Create stream reader.
                StreamReader INIReader = new StreamReader(readPath);
                Plugins = new List<PluginControl>();

                // Get all of the manifest document data.
                rawData = INIReader.ReadToEnd();
                INIReader.Close();
            }

            String rawBuffer = rawData;
            rawData = String.Empty;

            // Remove any comments from the document.
            for (int i = 0; i < rawBuffer.Length; i++)
            {
                char a = rawBuffer[i];
                if (rawBuffer[i] != ';')
                    rawData += rawBuffer[i];
                else
                {
                    int j = i;

                    for (; j < rawBuffer.Length; j++)
                        if (Environment.NewLine.Contains(rawBuffer[j]))
                        {
                            i = j;
                            break;
                        }

                    if (j >= rawBuffer.Length)
                        break;
                }
            }

            // Remove any line markers.
            rawData = rawData.Replace(Environment.NewLine, "\n");
            rawData = rawData.Replace("\n", Environment.NewLine);

            // Mark sections with a special character so they can be traced.
            rawData = rawData.Replace("[", "[" + sectionMarker);
            
            // Strip the section braces and get a list of lines.
            String[] rawDataLines = rawData.Split(new Char[] { '[', ']' }, StringSplitOptions.RemoveEmptyEntries);

            // Create the temp data holders.
            String attributeData = String.Empty;
            String contentData = String.Empty;
            List<String> pluginData = new List<String>();

            for (int i = 0; i < rawDataLines.Length; i++)
            {
                // Bail if line is empty.
                if (rawDataLines[i].Length <= 0)
                    continue;

                if (rawDataLines[i][0] == sectionMarker)
                {
                    // Remove the special marker.
                    rawDataLines[i] = rawDataLines[i].Remove(0, 1);

                    switch (rawDataLines[i])
                    {
                            // Attribute section.
                        case "attributes":
                            attributeData = rawDataLines[i + 1];
                            break;
                            // Content section.
                        case "content":
                            contentData = rawDataLines[i + 1];
                            break;
                            // Plugin section.
                        default:
                            pluginData.Add(rawDataLines[i + 1]);
                            break;
                    }
                }
            }

            // Create dictionary of attribute and content keys.
            Dictionary<String, String> attributeDictionary = ParseRawFormat(attributeData);
            Dictionary<String, String> contentDictionary = ParseRawFormat(contentData);

            // Set the visualisation name.
            if (attributeDictionary.ContainsKey("name"))
                Name = attributeDictionary["name"];

            // Set the visualisation color.
            if (attributeDictionary.ContainsKey("color"))
                Color = ParseColor(attributeDictionary["color"]);

            // Set the plugin content.
            foreach (String PluginName in contentDictionary.Keys)
                uniquePlugins.Add(pluginManager.FindPlugin(PluginName));

            // Parse the plugin data from the plugin sections.
            foreach (String rawPluginData in pluginData)
            {
                // Create dictionary of plugin keys.
                Dictionary<String, String> pluginDictionary = ParseRawFormat(rawPluginData);

                PluginControl pluginControl = null;

                if (pluginDictionary.ContainsKey("content"))
                    pluginControl = new PluginControl(Plugins.Count, uniquePlugins[int.Parse(pluginDictionary["content"])], null);

                if (pluginDictionary.ContainsKey("color"))
                    pluginControl.Color = ParseColor(pluginDictionary["color"]);

                if (pluginDictionary.ContainsKey("position"))
                    pluginControl.Position = ParseVector(pluginDictionary["position"]);

                if (pluginDictionary.ContainsKey("rotation"))
                    pluginControl.Rotation = Utils.RadiansToDegree(ParseVector(pluginDictionary["rotation"]));

                if (pluginDictionary.ContainsKey("spin"))
                    pluginControl.Spin = ParseVector(pluginDictionary["spin"]);

                if (pluginDictionary.ContainsKey("cycle"))
                    pluginControl.CycleColor = ParseBool(pluginDictionary["cycle"]);

                if (pluginDictionary.ContainsKey("resolution"))
                    pluginControl.Resolution = ParseResolution(pluginDictionary["resolution"]);

                // Add the newly read plugin.
                Plugins.Add(pluginControl);
            }

            Path = readPath;
        }

        private Dictionary<String, String> ParseRawFormat(String rawData)
        {
            Dictionary<String, String> dataDictionary = new Dictionary<String, String>();

            String[] dataBlocks = rawData.Split(new String[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);

            for (int i = 0; i < dataBlocks.Length; i++)
            {
                String[] currentBlock = dataBlocks[i].Split('=');
                dataDictionary.Add(currentBlock[0], currentBlock[1]);
            }

            return dataDictionary;
        }

        /// <summary>
        /// Writes a manifest to a specified location.
        /// </summary>
        /// <param name="writePath">The location to write.</param>
        public void Write(String writePath)
        {
            // Create the manifest.
            Generate();

            if (String.IsNullOrEmpty(writePath))
            {
                // If the manifest is SaveAs.
                writePath = Path;

                if (String.IsNullOrEmpty(Path))
                    throw new FileNotFoundException("The visualisation manifest was not found", writePath);
            }
            else
            {
                // If the manifest has not been saved before.
                Path = writePath;
            }

            // Write the manifest to disk.
            TextWriter INIWriter = new StreamWriter(writePath);

            INIWriter.WriteLine(String.Format("; {0} Visualisation Manifest - Automatically Generated by Dialect [{1}]", Name, DateTime.Now));
            INIWriter.WriteLine();
            
            INIWriter.Write(Output);

            INIWriter.Close();

            Path = writePath;
        }

        /// <summary>
        /// Generates the manifest document from internal data.
        /// </summary>
        public void Generate()
        {
            if (String.IsNullOrEmpty(Name))
                Name = "Untitled";

            List<Plugin> uniquePlugins = new List<Plugin>();
            StringBuilder INIBuilder = new StringBuilder();

            // Create attribute section.
            INIBuilder.AppendLine("[attributes]");
            INIBuilder.AppendLine(String.Format("name={0}", Name));
            INIBuilder.AppendLine(String.Format("color={0}", Format(Color)));
            INIBuilder.AppendLine();

            // Create list of unique plugins.
            foreach (PluginControl Plugin in Plugins)
                if (!uniquePlugins.Contains(Plugin.Parent))
                    uniquePlugins.Add(Plugin.Parent);

            // Build content section.
            INIBuilder.AppendLine("[content]");

            for (int i = 0; i < uniquePlugins.Count; i++)
                INIBuilder.AppendLine(String.Format("{0}={1}", uniquePlugins[i].Name, i.ToString()));

            INIBuilder.AppendLine();

            // Build plugin section.
            for (int i = 0; i < Plugins.Count; i++)
            {
                // Ensure plugin section has unique title.
                INIBuilder.AppendLine(String.Format("[{0}{1}]", Plugins[i].Name, i.ToString()));

                // Check if plugin reference is valid.
                int contentID = -1;

                for (int j = 0; j < uniquePlugins.Count; j++)
                    if (Plugins[i].Parent == uniquePlugins[j])
                    {
                        contentID = j;
                        break;
                    }

                if (contentID == -1)
                    throw new Exception();

                // Set the various plugin properties.
                INIBuilder.AppendLine(String.Format("content={0}", contentID.ToString()));

                if (!Plugins[i].CycleColor)
                    INIBuilder.AppendLine(String.Format("color={0}", Format(Plugins[i].Color)));
                else
                    INIBuilder.AppendLine(String.Format("color={0}", Format(Color.Black)));

                INIBuilder.AppendLine(String.Format("cycle={0}", Format(Plugins[i].CycleColor)));
                INIBuilder.AppendLine(String.Format("position={0}", Format(Plugins[i].Position)));


                INIBuilder.AppendLine(String.Format("rotation={0}", Format(Utils.DegreeToRadians(Plugins[i].Rotation))));


                INIBuilder.AppendLine(String.Format("spin={0}", Format(Plugins[i].Spin)));
                INIBuilder.AppendLine(String.Format("resolution={0}", Format(Plugins[i].Resolution)));

                INIBuilder.AppendLine();
            }

            Output = INIBuilder.ToString();
        }

        /// <summary>
        /// Converts a String to a Color.
        /// </summary>
        /// <param name="vector">The String.</param>
        /// <returns>The Color.</returns>
        private Color ParseColor(String color)
        {
            String[] colorComponents = color.Split(new Char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);

            return Color.FromArgb((int)Utils.ToColorByte(colorComponents[3]),
                (int)Utils.ToColorByte(colorComponents[0]),
                (int)Utils.ToColorByte(colorComponents[1]),
                (int)Utils.ToColorByte(colorComponents[2]));
        }

        /// <summary>
        /// Converts a String to a Bool.
        /// </summary>
        /// <param name="vector">The String.</param>
        /// <returns>The Bool.</returns>
        private bool ParseBool(String boolean)
        {
            if (boolean == "0")
                return false;

            return true;
        }

        /// <summary>
        /// Converts a String to a Resolution.
        /// </summary>
        /// <param name="vector">The String.</param>
        /// <returns>The Resolution.</returns>
        private Resolution ParseResolution(String resolution)
        {
            int iResolution = int.Parse(resolution);

            String[] resolutionNames = Enum.GetNames(typeof(Resolution));

            if (iResolution < resolutionNames.Length && iResolution >= 0)
                return (Resolution)Enum.Parse(typeof(Resolution), resolutionNames[iResolution]);

            return Resolution.Simple;
        }

        /// <summary>
        /// Converts a String to a Vector3D.
        /// </summary>
        /// <param name="vector">The String.</param>
        /// <returns>The Vector3D.</returns>
        private Vector3D ParseVector(String vector)
        {
            String[] vectorComponents = vector.Split(new Char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);

            return new Vector3D(double.Parse(vectorComponents[0]),
                double.Parse(vectorComponents[1]),
                double.Parse(vectorComponents[2]));
        }

        /// <summary>
        /// Converts a Color to a String.
        /// </summary>
        /// <param name="vector">The Color.</param>
        /// <returns>The String.</returns>
        private String Format(Color color)
        {
            return String.Format("{0}|{1}|{2}|{3}|",
                Utils.ToColorFloat(color.R),
                Utils.ToColorFloat(color.G),
                Utils.ToColorFloat(color.B),
                Utils.ToColorFloat(color.A));
        }

        /// <summary>
        /// Converts a Resolution to a String.
        /// </summary>
        /// <param name="vector">The Resolution.</param>
        /// <returns>The String.</returns>
        private String Format(Resolution resolution)
        {
            String[] resolutionNames = Enum.GetNames(typeof(Resolution));

            for (int i = 0; i < resolutionNames.Length; i++)
                if (resolutionNames[i] == resolution.ToString())
                    return i.ToString();

            return "0";
        }

        /// <summary>
        /// Converts a Bool to a String.
        /// </summary>
        /// <param name="vector">The Bool.</param>
        /// <returns>The String.</returns>
        private String Format(bool boolean)
        {
            return Utils.BoolToString(boolean);
        }

        /// <summary>
        /// Converts a Vector3D to a String.
        /// </summary>
        /// <param name="vector">The Vector3D</param>
        /// <returns>The String.</returns>
        private String Format(Vector3D vector)
        {
            return String.Format("{0}|{1}|{2}|0.0|",
                vector.X.ToString(),
                vector.Y.ToString(),
                vector.Z.ToString());
        }

        /// <summary>
        /// Writes a config document.
        /// </summary>
        /// <param name="writePath">The location to write.</param>
        /// <param name="properties">The properties to set.</param>
        public static void CreateConfig(String writePath, ConfigProperties properties)
        {
            TextWriter INIWriter = new StreamWriter(writePath);

            INIWriter.WriteLine("[general]");
            INIWriter.WriteLine(String.Format("width={0}", properties.Size.Width.ToString()));
            INIWriter.WriteLine(String.Format("height={0}", properties.Size.Height.ToString()));
            INIWriter.WriteLine(String.Format("splash={0}", Utils.BoolToString(properties.ShowSplash)));
            INIWriter.WriteLine(String.Format("fullscreen={0}", Utils.BoolToString(properties.IsFullscreen)));

            INIWriter.WriteLine();
            INIWriter.WriteLine("[dir]");
            INIWriter.WriteLine(String.Format("audio={0}", properties.AudioDirectory));
            INIWriter.WriteLine(String.Format("visualisations={0}", properties.VisualisationDirectory));
            INIWriter.WriteLine(String.Format("plugins={0}", properties.PluginsDirectory));

            INIWriter.Close();
        }
    }

    /// <summary>
    /// The options to set in a config document.
    /// </summary>
    public class ConfigProperties
    {
        public Size Size;
        public bool ShowSplash;
        public bool IsFullscreen;
        public String AudioDirectory;
        public String VisualisationDirectory;
        public String PluginsDirectory;
    }
}
