using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using VisimBox.Resources;
using VisiMath;
using System.Drawing;
using System.Xml.Serialization;
using System.Xml;

namespace VisimBox.Renderer
{
    /// <summary>
    /// Abstract material manager
    /// </summary>
    public abstract class MaterialManager : AbstractResMan<IRenderMaterial>
    {
        protected IRenderer owner;

        protected string MatVersionLibrary = "1.0";

        /// <summary>
        /// Manager constructor
        /// </summary>
        /// <param name="render">Required renderer</param>
        public MaterialManager(IRenderer render)
        {
            owner = render;            
        }

        /// <summary>
        /// Creates, adds to manager and returns new simple colored material with specified color
        /// </summary>
        /// <param name="key">Requested name (will be made unique)</param>
        /// <param name="color">Color</param>
        /// <returns>New material</returns>
        public IRenderMaterial Add(string key, ColorByte color)
        {
            key = GetUniqueName(key);
            IRenderMaterial rval = CreateSimpleColor(key, color);
            this.Add(key, rval);
            return rval;
        }

        /// <summary>
        /// Creates, adds to manager and returns new simple colored material with specified color
        /// </summary>
        /// <param name="key">Requested name (will be made unique)</param>
        /// <param name="color">Color</param>
        /// <returns>New material</returns>
        public IRenderMaterial Add(string key, Color color)
        {
            return Add(key, new ColorByte(color.ToArgb()));
        }

        /// <summary>
        /// Creates, adds to manager and returns new colored material with specified color values
        /// </summary>
        /// <param name="key">Requested name (will be made unique)</param>
        /// <param name="values">Color values</param>
        /// <returns>New material</returns>
        public IRenderMaterial Add(string key, ref StandardColorValues values)
        {
            key = GetUniqueName(key);
            var rval = CreateColorMaterial(key, ref values);
            Add(key, rval);
            return rval;
        }

        /// <summary>
        /// Creates, adds to manager and returns new colored material with specified color values
        /// </summary>
        /// <param name="key">Requested name (will be made unique)</param>
        /// <param name="values">Color values</param>
        /// <returns>New material</returns>
        public IRenderMaterial Add(string key, StandardColorValues values)
        {
            return Add(key, ref values);
        }

        /// <summary>
        /// Creates, adds to manager and returns new simple textured material
        /// </summary>
        /// <param name="key">Requested name (will be made unique)</param>
        /// <param name="tex">Texture</param>
        /// <returns>New material</returns>
        public IRenderMaterial Add(string key, Texture tex)
        {
            key = GetUniqueName(key);
            var rval = CreateTexturedSimple(key, tex);
            if (rval == null)
                return null;
            this.Add(key, rval);
            return rval;

        }

        /// <summary>
        /// Creates, adds to manager and returns new simple textured material
        /// </summary>
        /// <param name="key">Requested name (will be made unique)</param>
        /// <param name="tex">Texture name</param>
        /// <returns>New material</returns>
        public IRenderMaterial Add(string key, string tex)
        {
            key = GetUniqueName(key);
            var rval = CreateTexturedSimple(key, tex);
            if (rval == null)
                return null;
            this.Add(key, rval);
            return rval;
        }

        /// <summary>
        /// Creates, adds to manager and returns new simple textured material
        /// </summary>
        /// <param name="key">Requested name (will be made unique)</param>
        /// <param name="texture">Texture name</param>
        /// <param name="values">Color values</param>
        /// <returns>New material</returns>
        public IRenderMaterial Add(string key, string texture, ref StandardColorValues values)
        {
            key = GetUniqueName(key);
            var rval = CreateTextured(key, texture, ref values);
            if (rval == null)
                return null;
            this.Add(key, rval);
            return rval;
        }

        /// <summary>
        /// Creates, adds to manager and returns new simple textured material
        /// </summary>
        /// <param name="key">Requested name (will be made unique)</param>
        /// <param name="texture">Texture name</param>
        /// <param name="values">Color values</param>
        /// <returns>New material</returns>
        public IRenderMaterial Add(string key, string texture, StandardColorValues values)
        {
            return Add(key, texture, ref values);
        }

        /// <summary>
        /// Creates, adds to manager and returns new simple textured material
        /// </summary>
        /// <param name="key">Requested name (will be made unique)</param>
        /// <param name="texture">Texture</param>
        /// <param name="values">Color values</param>
        /// <returns>New material</returns>
        public IRenderMaterial Add(string key, Texture texture, ref StandardColorValues values)
        {
            key = GetUniqueName(key);
            var rval = CreateTextured(key, texture, ref values);
            if (rval == null)
                return null;
            this.Add(key, rval);
            return rval;
        }

        /// <summary>
        /// Creates, adds to manager and returns new simple textured material
        /// </summary>
        /// <param name="key">Requested name (will be made unique)</param>
        /// <param name="texture">Texture</param>
        /// <param name="values">Color values</param>
        /// <returns>New material</returns>
        public IRenderMaterial Add(string key, Texture texture, StandardColorValues values)
        {
            return Add(key, texture, ref values);
        }

        /// <summary>
        /// creates copy of material with identifier original key, adds to manager and returns pointer
        /// </summary>
        /// <param name="originalkey">original material</param>
        /// <param name="newkey">name for copy, if null, will be "originalkey + copy"</param>
        /// <returns></returns>
        public IRenderMaterial CreateCopy(string originalkey, string newkey = null)
        {
            if (!Contains(originalkey))
                return null;

            IRenderMaterial mat = this[originalkey];

            if (newkey == null)
                newkey = originalkey + "Copy";

            newkey = GetUniqueName(newkey);

            IRenderMaterial rval = mat.CreateCopy(newkey);
            Add(newkey, rval);
            return rval;                   
        }


        public override string Rename(string key, string newkey, bool uniquename = true)
        {
            string rval = base.Rename(key, newkey, uniquename);
            if (rval == null)
                return null;
            this[rval].Name = rval;
            return rval;
        }

        /// <summary>
        /// Saves material library to XML document
        /// </summary>
        /// <param name="writer">XML writer</param>
        /// <param name="allmaterials">If true, even nonreferenced materials will be saved</param>
        /// <returns>True if save sucessful</returns>
        public bool SaveLibrary(XmlTextWriter writer, bool allmaterials = true)
        {
            writer.WriteStartElement("MaterialLibrary");
            writer.WriteAttributeString("version", MatVersionLibrary);

            bool rval = true;

            foreach (var mat in resources)
            {
                if ((!allmaterials) && (mat.Value.References < 2))
                    continue;   //don't save materials which has only 1 reference (to me)
                writer.WriteStartElement("Material");
                mat.Value.WriteXml(writer);
                writer.WriteEndElement();
            }
            //note, check for correct saving somehow?
            writer.WriteEndElement();
            if (!rval)
                Console.DebugLine("MaterialManager:SaveLibrary failed");
            return rval;
        }

        /// <summary>
        /// Saves material library to disk
        /// </summary>
        /// <param name="filename">Filename to save</param>
        /// <param name="allmaterials">If true, even nonreferenced materials will be saved</param>
        /// <returns>True if save sucessful</returns>
        public bool SaveLibrary(string filename, bool allmaterials = true)
        {
            System.IO.TextWriter tw;
            try
            {
                tw = new System.IO.StreamWriter(filename);
            }
            catch
            {
                Console.DebugLine("MaterialManager:SaveLibrary failed - can't open text writer (wrong filename?)");
                return false;
            }

            System.Xml.XmlTextWriter writer = new System.Xml.XmlTextWriter(tw);
            writer.WriteStartDocument();
            writer.Formatting = System.Xml.Formatting.Indented;

            bool rval = SaveLibrary(writer, allmaterials);

            writer.WriteEndDocument();
            tw.Close();
            return rval;
        }


        public bool ReadMaterial(XmlTextReader reader, bool merge = true)
        {
            string materialtype = reader.GetAttribute("type");
            materialtype = materialtype.ToLower();
            string name = reader.GetAttribute("name");
            if (name == null)
                name = "unnamedmaterial";

            IRenderMaterial imat;

            switch (materialtype)
            {
                case "simplecolored":
                    imat = CreateSimpleColor(name, ColorByte.White);
                    break;
                case "simpletextured":
                    imat = CreateTexturedSimple(name, (Texture)null);
                    break;
                case "textured":
                    imat = CreateTextured(name, (Texture)null, new StandardColorValues());
                    break;
                case "colored":
                    imat = CreateColorMaterial(name, new StandardColorValues());
                    break;
                default:
                    return false;
            }

            imat.ReadXml(reader);

            if (merge)
            {
                //check if we already have this kind of material. 
                //If true, check for validity
                if (resources.ContainsKey(name))
                {
                    if (CheckMaterialSameness(imat, resources[name]))
                    {
                        //same materials, throw away
                        imat.Dispose();
                        return true;
                    }
                }
            }

            name = GetUniqueName(name);
            imat.Name = name;
            Add(name, imat);
            return true;
        }

        public bool CheckMaterialSameness(IRenderMaterial mat1, IRenderMaterial mat2)
        {
            if (mat1.MaterialType != mat2.MaterialType)
                return false;

            if (mat1.HasTextureElement(MaterialElement.Diffuse))
            {
                if (!mat2.HasTextureElement(MaterialElement.Diffuse))
                    return false;
                Texture tx1 = mat1.GetTextureElement(MaterialElement.Diffuse);
                Texture tx2 = mat2.GetTextureElement(MaterialElement.Diffuse);
                if (tx1 != tx2)
                    return false;
            }
            else if (mat2.HasTextureElement(MaterialElement.Diffuse))
                return false;

            if (mat1.HasColorElement(MaterialElement.Ambient))
            {
                if (!mat2.HasColorElement(MaterialElement.Ambient))
                    return false;
                ColorByte c1 = mat1.GetColorElement(MaterialElement.Ambient);
                ColorByte c2 = mat2.GetColorElement(MaterialElement.Ambient);
                if (c1 != c2)
                    return false;
            }
            else if (mat2.HasColorElement(MaterialElement.Ambient))
                return false;

            if (mat1.HasColorElement(MaterialElement.Specular))
            {
                if (!mat2.HasColorElement(MaterialElement.Specular))
                    return false;
                ColorByte c1 = mat1.GetColorElement(MaterialElement.Specular);
                ColorByte c2 = mat2.GetColorElement(MaterialElement.Specular);
                if (c1 != c2)
                    return false;
            }
            else if (mat2.HasColorElement(MaterialElement.Specular))
                return false;

            if (mat1.HasColorElement(MaterialElement.Diffuse))
            {
                if (!mat2.HasColorElement(MaterialElement.Diffuse))
                    return false;
                ColorByte c1 = mat1.GetColorElement(MaterialElement.Diffuse);
                ColorByte c2 = mat2.GetColorElement(MaterialElement.Diffuse);
                if (c1 != c2)
                    return false;
            }
            else if (mat2.HasColorElement(MaterialElement.Diffuse))
                return false;

            if (mat1.HasColorElement(MaterialElement.Emmissive))
            {
                if (!mat2.HasColorElement(MaterialElement.Emmissive))
                    return false;
                ColorByte c1 = mat1.GetColorElement(MaterialElement.Emmissive);
                ColorByte c2 = mat2.GetColorElement(MaterialElement.Emmissive);
                if (c1 != c2)
                    return false;
            }
            else if (mat2.HasColorElement(MaterialElement.Emmissive))
                return false;
            return true;
        }

        /// <summary>
        /// Reads library from XML file
        /// </summary>
        /// <param name="reader">XML reader, should be set to node "materiallibrary"</param>
        /// <param name="merge">if true, materials with same name and same values in manager and file will be merged, otherwise copies will be made</param>
        /// <returns>True if load sucesfull</returns>
        public bool LoadLibrary(XmlTextReader reader, bool merge = true)
        {                        
            string versionstring = reader.GetAttribute("version");

            if (versionstring != MatVersionLibrary)
            {
                Console.DebugLine("MaterialManager: load library - uncompatible version");
                return false;
            }

            while (reader.Read())
            {
                if (reader.IsStartElement("Material"))
                {
                    if (!ReadMaterial(reader, merge))
                        return false;
                }
                else return false;
            }
            return true;
        }

        /// <summary>
        /// Loads material library from disk
        /// </summary>
        /// <param name="filename">Filename to load library from</param>
        /// <param name="merge">if true, materials with same name and same values in manager and file will be merged, otherwise copies will be made</param>
        /// <returns>True if load sucesfull</returns>
        public bool LoadLibrary(string filename, bool merge = true)
        {
            System.IO.TextReader tr;
            try
            {
                tr = new System.IO.StreamReader(filename);
            }
            catch
            {
                Console.DebugLine("MaterialManager:LoadLibrary failer, can't open file!");
                return false;
            }

            XmlTextReader reader = new XmlTextReader(tr);

            bool rval = false;

            while (reader.Read())
            {
                if (reader.Name == "MaterialLibrary")
                {
                    if (LoadLibrary(reader, merge))
                    {
                        rval = true;
                        break;
                    }
                }
            }
            tr.Close();
            return rval;
        }


        /// <summary>
        /// Replaces material mat1 with mat2 (exchanging names for named references, calling resourcereplaced callbacks of consumers)
        /// Note, mat2 must be referenceless, otherwise throws exception!
        /// </summary>
        /// <param name="mat1"></param>
        /// <param name="mat2"></param>
        public void ReplaceMaterial(IRenderMaterial mat1, IRenderMaterial mat2, bool deleteold = false)
        {
            if (mat2.References > 1)
                throw new ArgumentException("ReplaceMaterial - new material has references!");

            if (!resources.ContainsValue(mat1) || !resources.ContainsValue(mat2))
            {
                throw new ArgumentException("ReplaceMaterial - one of the materials is not registered!!!");
            }

            mat1.RemoveReference(this);
            mat2.RemoveReference(this);

            if (!resources.Remove(mat1.Name) || !resources.Remove(mat2.Name))
            {
                throw new ArgumentException("ReplaceMaterial - material replacing failed!!!");
            }

            string tmp = mat1.Name;
            mat1.Name = mat2.Name;
            mat2.Name = tmp;

            bool rval = true;

            if (mat1.References > 0)
            {
                if (!mat1.ReplaceReferences(mat2))
                    rval = false;
            }

            if (!rval)
            {
                Console.DebugLine("Material replace - one of consumers refused to accept change!");
            }

            Add(mat2.Name, mat2);

            if (deleteold)
            {
                mat1.Dispose();
            }
            else
            {
                Add(mat1.Name, mat1);
            }
        }


        #region Material creation

        #region simple colored


        /// <summary>
        /// Creates new simple colored material (just one color, no lightning)
        /// </summary>
        /// <param name="color">Required color</param>
        /// <param name="name">Requested name for the material</param>
        /// <returns>New material</returns>
        protected abstract IRenderMaterial CreateSimpleColor(string name, ColorByte color);

        /// <summary>
        /// Creates new simple colored material (just one color, no lightning)
        /// </summary>
        /// <param name="color">Required color</param>
        /// <param name="name">Requested name for the material</param>
        /// <returns>New material</returns>
        protected IRenderMaterial CreateSimpleColor(string name, Color color)
        {
            return CreateSimpleColor(name, new ColorByte(color.ToArgb()));
        }


        #endregion

        #region colored

        /// <summary>
        /// Creates standard colored material from color values
        /// </summary>
        /// <param name="values">Color values for standard lightning equation</param>
        /// <returns>New material</returns>
        protected abstract IRenderMaterial CreateColorMaterial(string name, ref StandardColorValues values);

        /// <summary>
        /// Creates standard colored material from color values
        /// </summary>
        /// <param name="values">Color values for standard lightning equation</param>
        /// <param name="name">Requested name for the material</param>
        /// <returns>New material</returns>
        protected IRenderMaterial CreateColorMaterial(string name, StandardColorValues values)
        {
            return CreateColorMaterial(name, ref values);
        }

        #endregion 
        #region Textured
        /// <summary>
        /// Creates simple textured material (just one color texture)
        /// </summary>
        /// <param name="tex">Required texture</param>
        /// <param name="name">Requested name for the material</param>
        /// <returns>New material</returns>
        protected abstract IRenderMaterial CreateTexturedSimple(string name, Texture tex);


        /// <summary>
        /// Creates simple textured material (just one color texture)
        /// </summary>
        /// <param name="name">Requested name for the material</param>
        /// <param name="texture">Required texture name (either name in texture manager, or filename to load)</param>
        /// <returns>New material</returns>
        protected IRenderMaterial CreateTexturedSimple(string name, string texture)
        {
            Texture t = owner.TextureMan.Load(texture);
            if (t == null)
                return null;
            return CreateTexturedSimple(name, t);
        }

        /// <summary>
        /// Creates colored textured material (standard colors + one diffuse texture)
        /// </summary>
        /// <param name="tex">Texture</param>
        /// <param name="values">Color values</param>
        /// <param name="name">Requested name for the material</param>
        /// <returns>New material</returns>
        protected abstract IRenderMaterial CreateTextured(string name, Texture tex, ref StandardColorValues values);

        /// <summary>
        /// Creates colored textured material (standard colors + one diffuse texture)
        /// </summary>
        /// <param name="tex">Texture</param>
        /// <param name="values">Color values</param>
        /// <param name="name">Requested name for the material</param>
        /// <returns>New material</returns>
        protected IRenderMaterial CreateTextured(string name, Texture tex, StandardColorValues values)
        {
            return CreateTextured(name, tex, ref values);
        }

        /// <summary>
        /// Creates colored textured material (standard colors + one diffuse texture)
        /// </summary>
        /// <param name="texture">Required texture name (either name in texture manager, or filename to load)</param>
        /// <param name="name">Requested name for the material</param>
        /// <param name="values">Color values</param>
        /// <returns>New material</returns>
        protected IRenderMaterial CreateTextured(string name, string texture, ref StandardColorValues values)
        {
            Texture t = owner.TextureMan.Load(texture);
            if (t == null)
                return null;
            return CreateTextured(name, t, ref values);
        }

        /// <summary>
        /// Creates colored textured material (standard colors + one diffuse texture)
        /// </summary>
        /// <param name="texture">Required texture name (either name in texture manager, or filename to load)</param>
        /// <param name="values">Color values</param>
        /// <param name="name">Requested name for the material</param>
        /// <returns>New material</returns>
        protected IRenderMaterial CreateTextured(string name, string texture, StandardColorValues values)
        {
            return CreateTextured(name, texture, ref values);
        }

        #endregion


        #endregion
        //todo: add enhanced materials
    }
}
