/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Avro
{
    public class Protocol
    {
        /// <summary>
        /// Name of the protocol
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// Namespace of the protocol
        /// </summary>
        public string Namespace { get; set; }

        /// <summary>
        /// Documentation for the protocol
        /// </summary>
        public string Doc { get; set; }

        /// <summary>
        /// List of schemas objects representing the different schemas defined under the 'types' attribute
        /// </summary>
        public IList<Schema> Types { get; set; }

        /// <summary>
        /// List of message objects representing the different schemas defined under the 'messages' attribute
        /// </summary>
        public IDictionary<string,Message> Messages { get; set; }

        private byte[] md5;
        public byte[] MD5
        {
            get 
            {
                try
                {
                    if (md5 == null)
                        md5 = System.Security.Cryptography.MD5.Create().ComputeHash(Encoding.UTF8.GetBytes(ToString()));
                }
                catch (Exception ex)
                {
                    throw new AvroRuntimeException("MD5 get exception", ex);
                }
                return md5; 
            }
        }

        /// <summary>
        /// Constructor for Protocol class
        /// </summary>
        /// <param name="name">required name of protocol</param>
        /// <param name="space">optional namespace</param>
        /// <param name="doc">optional documentation</param>
        /// <param name="types">required list of types</param>
        /// <param name="messages">required list of messages</param>
        public Protocol(string name, string space,
                        string doc, IEnumerable<Schema> types,
                        IDictionary<string,Message> messages)
        {
            if (string.IsNullOrEmpty(name)) throw new ArgumentNullException("name", "name cannot be null.");
            if (null == types) throw new ArgumentNullException("types", "types cannot be null.");
            if (null == messages) throw new ArgumentNullException("messages", "messages cannot be null.");

            this.Name = name;
            this.Namespace = space;
            this.Doc = doc;
            this.Types = new List<Schema>(types);
            this.Messages = new Dictionary<string, Message>(messages);
        }

        /// <summary>
        /// Parses the given JSON string to create a Protocol object
        /// </summary>
        /// <param name="jstring">JSON string</param>
        /// <returns>Protocol object</returns>
        public static Protocol Parse(string jstring)
        {
            if (string.IsNullOrEmpty(jstring)) throw new ArgumentNullException("json", "json cannot be null.");

            JToken jtok = null;
            try
            {
                jtok = JObject.Parse(jstring);
            }
            catch (Exception ex)
            {
                throw new ProtocolParseException("Invalid JSON format: " + jstring, ex);
            }
            return Parse(jtok);
        }

        /// <summary>
        /// Parses the given JSON object to create a Protocol object
        /// </summary>
        /// <param name="jtok">JSON object</param>
        /// <returns>Protocol object</returns>
        private static Protocol Parse(JToken jtok)
        {
            string name = JsonHelper.GetRequiredString(jtok, "protocol");
            string space = JsonHelper.GetOptionalString(jtok, "namespace");
            string doc = JsonHelper.GetOptionalString(jtok, "doc");

            var names = new SchemaNames();

            JToken jtypes = jtok["types"];
            var types = new List<Schema>();
            if (jtypes is JArray)
            {
                foreach (JToken jtype in jtypes)
                {
                    var schema = Schema.ParseJson(jtype, names, space);
                    types.Add(schema);
                }
            }

            var messages = new Dictionary<string,Message>();
            JToken jmessages = jtok["messages"];
            if (null != jmessages)
            {
                foreach (JProperty jmessage in jmessages)
                {
                    var message = Message.Parse(jmessage, names, space);
                    messages.Add(message.Name, message);
                }
            }

            return new Protocol(name, space, doc, types, messages);
        }

        /// <summary>
        /// Writes Protocol in JSON format
        /// </summary>
        /// <returns>JSON string</returns>
        public override string ToString()
        {
            using (System.IO.StringWriter sw = new System.IO.StringWriter())
            {
                using (Newtonsoft.Json.JsonTextWriter writer = new Newtonsoft.Json.JsonTextWriter(sw))
                {
                    #if(DEBUG)
                    writer.Formatting = Newtonsoft.Json.Formatting.Indented;
                    #endif

                    WriteJson(writer, new SchemaNames());
                    writer.Flush();
                    return sw.ToString();
                }
            }
        }

        /// <summary>
        /// Writes Protocol in JSON format
        /// </summary>
        /// <param name="writer">JSON writer</param>
        /// <param name="names">list of named schemas already written</param>
        internal void WriteJson(Newtonsoft.Json.JsonTextWriter writer, SchemaNames names)
        {
            writer.WriteStartObject();

            JsonHelper.writeIfNotNullOrEmpty(writer, "protocol", this.Name);
            JsonHelper.writeIfNotNullOrEmpty(writer, "namespace", this.Namespace);
            JsonHelper.writeIfNotNullOrEmpty(writer, "doc", this.Doc);

            writer.WritePropertyName("types");
            writer.WriteStartArray();

            foreach (Schema type in this.Types)
                type.WriteJson(writer, names, this.Namespace);

            writer.WriteEndArray();

            writer.WritePropertyName("messages");
            writer.WriteStartObject();

            foreach (KeyValuePair<string,Message> message in this.Messages)
            {
                writer.WritePropertyName(message.Key);
                message.Value.writeJson(writer, names, this.Namespace);
            }

            writer.WriteEndObject();
            writer.WriteEndObject();
        }

        /// <summary>
        /// Tests equality of this protocol object with the passed object
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public override bool Equals(object obj)
        {
            if (obj == this) return true;
            if (!(obj is Protocol)) return false;

            Protocol that = obj as Protocol;

            return this.Name.Equals(that.Name) && this.Namespace.Equals(that.Namespace) && 
                    TypesEquals(that.Types) && MessagesEquals(that.Messages);
        }

        /// <summary>
        /// Test equality of this protocols Types list with the passed Types list.
        /// Order of schemas does not matter, as long as all types in this protocol
        /// are also defined in the passed protocol
        /// </summary>
        /// <param name="that"></param>
        /// <returns></returns>
        private bool TypesEquals(IList<Schema> that)
        {
            if (Types.Count != that.Count) return false;
            foreach (Schema schema in Types)
                if (!that.Contains(schema)) return false;
            return true;
        }

        /// <summary>
        /// Test equality of this protocols Message map with the passed Message map
        /// Order of messages does not matter, as long as all messages in this protocol
        /// are also defined in the passed protocol
        /// </summary>
        /// <param name="that"></param>
        /// <returns></returns>
        private bool MessagesEquals(IDictionary<string, Message> that)
        {
            if (Messages.Count != that.Count) return false;
            foreach (KeyValuePair<string, Message> pair in Messages) 
            { 
                if (!that.ContainsKey(pair.Key))
                    return false;
                if (!pair.Value.Equals(that[pair.Key]))
                    return false; 
            } 
            return true;
        }

        /// <summary>
        /// Returns the hash code of this protocol object
        /// </summary>
        /// <returns></returns>
        public override int GetHashCode()
        {
            return Name.GetHashCode() + Namespace.GetHashCode() +
                   GetTypesHashCode() + GetMessagesHashCode();
        }

        /// <summary>
        /// Returns the hash code of the Types list
        /// </summary>
        /// <returns></returns>
        private int GetTypesHashCode()
        {
            int hash = Types.Count;
            foreach (Schema schema in Types)
                hash += schema.GetHashCode();
            return hash;
        }

        /// <summary>
        /// Returns the hash code of the Messages map
        /// </summary>
        /// <returns></returns>
        private int GetMessagesHashCode()
        {
            int hash = Messages.Count;
            foreach (KeyValuePair<string, Message> pair in Messages)
                hash += (pair.Key.GetHashCode() + pair.Value.GetHashCode());
            return hash;
        }
    }
}
