/*
 * Copyright (c) Contributors, http://opensimulator.org/
 * See CONTRIBUTORS.TXT for a full list of copyright holders.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the OpenSim Project nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Collections;
using System.Globalization;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Xml;
using OpenMetaverse;

namespace OpenSim.Framework.Communications.Capabilities
{
    /// <summary>
    /// Borrowed from (a older version of) libsl for now, as their new llsd code doesn't work we our decoding code.
    /// </summary>
    public static class LLSD
    {
        /// <summary>
        ///
        /// </summary>
        public class LLSDParseException : Exception
        {
            public LLSDParseException(string message) : base(message)
            {
            }
        }

        /// <summary>
        ///
        /// </summary>
        public class LLSDSerializeException : Exception
        {
            public LLSDSerializeException(string message) : base(message)
            {
            }
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="b"></param>
        /// <returns></returns>
        public static object LLSDDeserialize(byte[] b)
        {
            return LLSDDeserialize(new MemoryStream(b, false));
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="st"></param>
        /// <returns></returns>
        public static object LLSDDeserialize(Stream st)
        {
            XmlTextReader reader = new XmlTextReader(st);
            reader.Read();
            SkipWS(reader);

            if (reader.NodeType != XmlNodeType.Element || reader.LocalName != "llsd")
                throw new LLSDParseException("Expected <llsd>");

            reader.Read();
            object ret = LLSDParseOne(reader);
            SkipWS(reader);

            if (reader.NodeType != XmlNodeType.EndElement || reader.LocalName != "llsd")
                throw new LLSDParseException("Expected </llsd>");

            return ret;
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public static byte[] LLSDSerialize(object obj)
        {
            StringWriter sw = new StringWriter();
            XmlTextWriter writer = new XmlTextWriter(sw);
            writer.Formatting = Formatting.None;

            writer.WriteStartElement(String.Empty, "llsd", String.Empty);
            LLSDWriteOne(writer, obj);
            writer.WriteEndElement();

            writer.Close();

            return Encoding.UTF8.GetBytes(sw.ToString());
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="writer"></param>
        /// <param name="obj"></param>
        public static void LLSDWriteOne(XmlTextWriter writer, object obj)
        {
            if (obj == null)
            {
                writer.WriteStartElement(String.Empty, "undef", String.Empty);
                writer.WriteEndElement();
                return;
            }

            if (obj is string)
            {
                writer.WriteStartElement(String.Empty, "string", String.Empty);
                writer.WriteString((string) obj);
                writer.WriteEndElement();
            }
            else if (obj is int)
            {
                writer.WriteStartElement(String.Empty, "integer", String.Empty);
                writer.WriteString(obj.ToString());
                writer.WriteEndElement();
            }
            else if (obj is double)
            {
                writer.WriteStartElement(String.Empty, "real", String.Empty);
                writer.WriteString(obj.ToString());
                writer.WriteEndElement();
            }
            else if (obj is bool)
            {
                bool b = (bool) obj;
                writer.WriteStartElement(String.Empty, "boolean", String.Empty);
                writer.WriteString(b ? "1" : "0");
                writer.WriteEndElement();
            }
            else if (obj is ulong)
            {
                throw new Exception("ulong in LLSD is currently not implemented, fix me!");
            }
            else if (obj is UUID)
            {
                UUID u = (UUID) obj;
                writer.WriteStartElement(String.Empty, "uuid", String.Empty);
                writer.WriteString(u.ToString());
                writer.WriteEndElement();
            }
            else if (obj is Hashtable)
            {
                Hashtable h = obj as Hashtable;
                writer.WriteStartElement(String.Empty, "map", String.Empty);
                foreach (string key in h.Keys)
                {
                    writer.WriteStartElement(String.Empty, "key", String.Empty);
                    writer.WriteString(key);
                    writer.WriteEndElement();
                    LLSDWriteOne(writer, h[key]);
                }
                writer.WriteEndElement();
            }
            else if (obj is ArrayList)
            {
                ArrayList a = obj as ArrayList;
                writer.WriteStartElement(String.Empty, "array", String.Empty);
                foreach (object item in a)
                {
                    LLSDWriteOne(writer, item);
                }
                writer.WriteEndElement();
            }
            else if (obj is byte[])
            {
                byte[] b = obj as byte[];
                writer.WriteStartElement(String.Empty, "binary", String.Empty);

                writer.WriteStartAttribute(String.Empty, "encoding", String.Empty);
                writer.WriteString("base64");
                writer.WriteEndAttribute();

                //// Calculate the length of the base64 output
                //long length = (long)(4.0d * b.Length / 3.0d);
                //if (length % 4 != 0) length += 4 - (length % 4);

                //// Create the char[] for base64 output and fill it
                //char[] tmp = new char[length];
                //int i = Convert.ToBase64CharArray(b, 0, b.Length, tmp, 0);

                //writer.WriteString(new String(tmp));

                writer.WriteString(Convert.ToBase64String(b));
                writer.WriteEndElement();
            }
            else
            {
                throw new LLSDSerializeException("Unknown type " + obj.GetType().Name);
            }
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="reader"></param>
        /// <returns></returns>
        public static object LLSDParseOne(XmlTextReader reader)
        {
            SkipWS(reader);
            if (reader.NodeType != XmlNodeType.Element)
                throw new LLSDParseException("Expected an element");

            string dtype = reader.LocalName;
            object ret = null;

            switch (dtype)
            {
                case "undef":
                    {
                        if (reader.IsEmptyElement)
                        {
                            reader.Read();
                            return null;
                        }

                        reader.Read();
                        SkipWS(reader);
                        ret = null;
                        break;
                    }
                case "boolean":
                    {
                        if (reader.IsEmptyElement)
                        {
                            reader.Read();
                            return false;
                        }

                        reader.Read();
                        string s = reader.ReadString().Trim();

                        if (s == String.Empty || s == "false" || s == "0")
                            ret = false;
                        else if (s == "true" || s == "1")
                            ret = true;
                        else
                            throw new LLSDParseException("Bad boolean value " + s);

                        break;
                    }
                case "integer":
                    {
                        if (reader.IsEmptyElement)
                        {
                            reader.Read();
                            return 0;
                        }

                        reader.Read();
                        ret = Convert.ToInt32(reader.ReadString().Trim());
                        break;
                    }
                case "real":
                    {
                        if (reader.IsEmptyElement)
                        {
                            reader.Read();
                            return 0.0f;
                        }

                        reader.Read();
                        ret = Convert.ToDouble(reader.ReadString().Trim());
                        break;
                    }
                case "uuid":
                    {
                        if (reader.IsEmptyElement)
                        {
                            reader.Read();
                            return UUID.Zero;
                        }

                        reader.Read();
                        ret = new UUID(reader.ReadString().Trim());
                        break;
                    }
                case "string":
                    {
                        if (reader.IsEmptyElement)
                        {
                            reader.Read();
                            return String.Empty;
                        }

                        reader.Read();
                        ret = reader.ReadString();
                        break;
                    }
                case "binary":
                    {
                        if (reader.IsEmptyElement)
                        {
                            reader.Read();
                            return new byte[0];
                        }

                        if (reader.GetAttribute("encoding") != null &&
                            reader.GetAttribute("encoding") != "base64")
                        {
                            throw new LLSDParseException("Unknown encoding: " + reader.GetAttribute("encoding"));
                        }

                        reader.Read();
                        FromBase64Transform b64 = new FromBase64Transform(FromBase64TransformMode.IgnoreWhiteSpaces);
                        byte[] inp = Encoding.UTF8.GetBytes(reader.ReadString());
                        ret = b64.TransformFinalBlock(inp, 0, inp.Length);
                        break;
                    }
                case "date":
                    {
                        reader.Read();
                        throw new Exception("LLSD TODO: date");
                    }
                case "map":
                    {
                        return LLSDParseMap(reader);
                    }
                case "array":
                    {
                        return LLSDParseArray(reader);
                    }
                default:
                    throw new LLSDParseException("Unknown element <" + dtype + ">");
            }

            if (reader.NodeType != XmlNodeType.EndElement || reader.LocalName != dtype)
            {
                throw new LLSDParseException("Expected </" + dtype + ">");
            }

            reader.Read();
            return ret;
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="reader"></param>
        /// <returns></returns>
        public static Hashtable LLSDParseMap(XmlTextReader reader)
        {
            Hashtable ret = new Hashtable();

            if (reader.NodeType != XmlNodeType.Element || reader.LocalName != "map")
                throw new LLSDParseException("Expected <map>");

            if (reader.IsEmptyElement)
            {
                reader.Read();
                return ret;
            }

            reader.Read();

            while (true)
            {
                SkipWS(reader);
                if (reader.NodeType == XmlNodeType.EndElement && reader.LocalName == "map")
                {
                    reader.Read();
                    break;
                }

                if (reader.NodeType != XmlNodeType.Element || reader.LocalName != "key")
                    throw new LLSDParseException("Expected <key>");

                string key = reader.ReadString();

                if (reader.NodeType != XmlNodeType.EndElement || reader.LocalName != "key")
                    throw new LLSDParseException("Expected </key>");

                reader.Read();
                object val = LLSDParseOne(reader);
                ret[key] = val;
            }

            return ret; // TODO
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="reader"></param>
        /// <returns></returns>
        public static ArrayList LLSDParseArray(XmlTextReader reader)
        {
            ArrayList ret = new ArrayList();

            if (reader.NodeType != XmlNodeType.Element || reader.LocalName != "array")
                throw new LLSDParseException("Expected <array>");

            if (reader.IsEmptyElement)
            {
                reader.Read();
                return ret;
            }

            reader.Read();

            while (true)
            {
                SkipWS(reader);

                if (reader.NodeType == XmlNodeType.EndElement && reader.LocalName == "array")
                {
                    reader.Read();
                    break;
                }

                ret.Insert(ret.Count, LLSDParseOne(reader));
            }

            return ret; // TODO
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="count"></param>
        /// <returns></returns>
        private static string GetSpaces(int count)
        {
            StringBuilder b = new StringBuilder();
            for (int i = 0; i < count; i++) b.Append(" ");
            return b.ToString();
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="indent"></param>
        /// <returns></returns>
        public static String LLSDDump(object obj, int indent)
        {
            if (obj == null)
            {
                return GetSpaces(indent) + "- undef\n";
            }
            else if (obj is string)
            {
                return GetSpaces(indent) + "- string \"" + (string) obj + "\"\n";
            }
            else if (obj is int)
            {
                return GetSpaces(indent) + "- integer " + obj.ToString() + "\n";
            }
            else if (obj is double)
            {
                return GetSpaces(indent) + "- float " + obj.ToString() + "\n";
            }
            else if (obj is UUID)
            {
                return GetSpaces(indent) + "- uuid " + ((UUID) obj).ToString() + Environment.NewLine;
            }
            else if (obj is Hashtable)
            {
                StringBuilder ret = new StringBuilder();
                ret.Append(GetSpaces(indent) + "- map" + Environment.NewLine);
                Hashtable map = (Hashtable) obj;

                foreach (string key in map.Keys)
                {
                    ret.Append(GetSpaces(indent + 2) + "- key \"" + key + "\"" + Environment.NewLine);
                    ret.Append(LLSDDump(map[key], indent + 3));
                }

                return ret.ToString();
            }
            else if (obj is ArrayList)
            {
                StringBuilder ret = new StringBuilder();
                ret.Append(GetSpaces(indent) + "- array\n");
                ArrayList list = (ArrayList) obj;

                foreach (object item in list)
                {
                    ret.Append(LLSDDump(item, indent + 2));
                }

                return ret.ToString();
            }
            else if (obj is byte[])
            {
                return GetSpaces(indent) + "- binary\n" + Utils.BytesToHexString((byte[]) obj, GetSpaces(indent)) +
                       Environment.NewLine;
            }
            else
            {
                return GetSpaces(indent) + "- unknown type " + obj.GetType().Name + Environment.NewLine;
            }
        }

        public static object ParseTerseLLSD(string llsd)
        {
            int notused;
            return ParseTerseLLSD(llsd, out notused);
        }

        public static object ParseTerseLLSD(string llsd, out int endPos)
        {
            if (llsd.Length == 0)
            {
                endPos = 0;
                return null;
            }

            // Identify what type of object this is
            switch (llsd[0])
            {
                case '!':
                    throw new LLSDParseException("Undefined value type encountered");
                case '1':
                    endPos = 1;
                    return true;
                case '0':
                    endPos = 1;
                    return false;
                case 'i':
                    {
                        if (llsd.Length < 2) throw new LLSDParseException("Integer value type with no value");
                        int value;
                        endPos = FindEnd(llsd, 1);

                        if (Int32.TryParse(llsd.Substring(1, endPos - 1), out value))
                            return value;
                        else
                            throw new LLSDParseException("Failed to parse integer value type");
                    }
                case 'r':
                    {
                        if (llsd.Length < 2) throw new LLSDParseException("Real value type with no value");
                        double value;
                        endPos = FindEnd(llsd, 1);

                        if (Double.TryParse(llsd.Substring(1, endPos - 1), NumberStyles.Float,
                                            Helpers.EnUsCulture.NumberFormat, out value))
                            return value;
                        else
                            throw new LLSDParseException("Failed to parse double value type");
                    }
                case 'u':
                    {
                        if (llsd.Length < 17) throw new LLSDParseException("UUID value type with no value");
                        UUID value;
                        endPos = FindEnd(llsd, 1);

                        if (UUID.TryParse(llsd.Substring(1, endPos - 1), out value))
                            return value;
                        else
                            throw new LLSDParseException("Failed to parse UUID value type");
                    }
                case 'b':
                    //byte[] value = new byte[llsd.Length - 1];
                    // This isn't the actual binary LLSD format, just the terse format sent
                    // at login so I don't even know if there is a binary type
                    throw new LLSDParseException("Binary value type is unimplemented");
                case 's':
                case 'l':
                    if (llsd.Length < 2) throw new LLSDParseException("String value type with no value");
                    endPos = FindEnd(llsd, 1);
                    return llsd.Substring(1, endPos - 1);
                case 'd':
                    // Never seen one before, don't know what the format is
                    throw new LLSDParseException("Date value type is unimplemented");
                case '[':
                    {
                        if (llsd.IndexOf(']') == -1) throw new LLSDParseException("Invalid array");

                        int pos = 0;
                        ArrayList array = new ArrayList();

                        while (llsd[pos] != ']')
                        {
                            ++pos;

                            // Advance past comma if need be
                            if (llsd[pos] == ',') ++pos;

                            // Allow a single whitespace character
                            if (pos < llsd.Length && llsd[pos] == ' ') ++pos;

                            int end;
                            array.Add(ParseTerseLLSD(llsd.Substring(pos), out end));
                            pos += end;
                        }

                        endPos = pos + 1;
                        return array;
                    }
                case '{':
                    {
                        if (llsd.IndexOf('}') == -1) throw new LLSDParseException("Invalid map");

                        int pos = 0;
                        Hashtable hashtable = new Hashtable();

                        while (llsd[pos] != '}')
                        {
                            ++pos;

                            // Advance past comma if need be
                            if (llsd[pos] == ',') ++pos;

                            // Allow a single whitespace character
                            if (pos < llsd.Length && llsd[pos] == ' ') ++pos;

                            if (llsd[pos] != '\'') throw new LLSDParseException("Expected a map key");
                            int endquote = llsd.IndexOf('\'', pos + 1);
                            if (endquote == -1 || (endquote + 1) >= llsd.Length || llsd[endquote + 1] != ':')
                                throw new LLSDParseException("Invalid map format");
                            string key = llsd.Substring(pos, endquote - pos);
                            key = key.Replace("'", String.Empty);
                            pos += (endquote - pos) + 2;

                            int end;
                            hashtable.Add(key, ParseTerseLLSD(llsd.Substring(pos), out end));
                            pos += end;
                        }

                        endPos = pos + 1;
                        return hashtable;
                    }
                default:
                    throw new Exception("Unknown value type");
            }
        }

        private static int FindEnd(string llsd, int start)
        {
            int end = llsd.IndexOfAny(new char[] {',', ']', '}'});
            if (end == -1) end = llsd.Length - 1;
            return end;
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="reader"></param>
        private static void SkipWS(XmlTextReader reader)
        {
            while (
                reader.NodeType == XmlNodeType.Comment ||
                reader.NodeType == XmlNodeType.Whitespace ||
                reader.NodeType == XmlNodeType.SignificantWhitespace ||
                reader.NodeType == XmlNodeType.XmlDeclaration)
            {
                reader.Read();
            }
        }
    }
}