/*
 *
 * 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;
using System.Text;
using log4net;
using Qpid.Buffer;
using Qpid.Collections;
using Qpid.Messaging;

namespace Qpid.Framing
{
    public class FieldTable : IFieldTable, IEnumerable
    {
        private static readonly ILog _log = LogManager.GetLogger(typeof(FieldTable));

        IDictionary _properties;
        private ByteBuffer _encodedForm;
        private object _syncLock;
        private uint _encodedSize;

        public FieldTable()
        {
           _syncLock = new object();
        }
        
        /// <summary>
        /// Construct a new field table.
        /// </summary>
        /// <param name="buffer">the buffer from which to read data. The length byte must be read already</param>
        /// <param name="length">the length of the field table. Must be > 0.</param>
        public FieldTable(ByteBuffer buffer, uint length) : this()
        {
           _encodedForm = buffer.Slice();
           _encodedForm.Limit = (int)length;
           _encodedSize = length;
           buffer.Skip((int)length);
        }

        /// <summary>
        /// The set of all property names
        /// </summary>
        public ICollection Keys
        {
           get 
           { 
              InitMapIfNecessary();
              return _properties.Keys;  
           }
        }

        /// <summary>
        /// Calculated size of this field table once encoded
        /// </summary>
        public uint EncodedSize
        {
           get { return _encodedSize; }
        }

        /// <summary>
        /// Number of properties in the field table
        /// </summary>
        public int Count
        {
           get
           {
              InitMapIfNecessary();
              return _properties.Count;
           }
        }

        /// <summary>
        /// Gets or sets the specified property.
        /// </summary>
        /// <param name="key">Property name</param>
        /// <returns>The specified property value</returns>
        public object this[string key]
        { 
           get { return GetObject(key); }
           set { SetObject(key, value); }
        }

        #region Typed Setters and Getters
        //
        // Typed Setters and Getters
        //
        public bool GetBoolean(string key)
        {
           return (bool)this[key];
        }
        public void SetBoolean(string key, bool value)
        {
           CheckPropertyName(key);
           SetProperty(key, AMQType.BOOLEAN.AsTypedValue(value));
        }
        public byte GetByte(string key)
        {
           return (byte)this[key];
        }
        public void SetByte(string key, byte value)
        {
           CheckPropertyName(key);
           SetProperty(key, AMQType.BYTE.AsTypedValue(value));
        }
        public sbyte GetSByte(string key)
        {
           return (sbyte)this[key];
        }
        public void SetSByte(string key, sbyte value)
        {
           CheckPropertyName(key);
           SetProperty(key, AMQType.SBYTE.AsTypedValue(value));
        }
        public short GetInt16(string key)
        {
           return (short)this[key];
        }
        public void SetInt16(string key, short value)
        {
           CheckPropertyName(key);
           SetProperty(key, AMQType.INT16.AsTypedValue(value));
        }
        public int GetInt32(string key)
        {
           return (int)this[key];
        }
        public void SetInt32(string key, int value)
        {
           CheckPropertyName(key);
           SetProperty(key, AMQType.INT32.AsTypedValue(value));
        }
        public long GetInt64(string key)
        {
           return (long)this[key];
        }
        public void SetInt64(string key, long value)
        {
           CheckPropertyName(key);
           SetProperty(key, AMQType.INT64.AsTypedValue(value));
        }
        public char GetChar(string key)
        {
           return (char)this[key];
        }
        public void SetChar(string key, char value)
        {
           CheckPropertyName(key);
           SetProperty(key, AMQType.ASCII_CHARACTER.AsTypedValue(value));
        }
        public float GetFloat(string key)
        {
           return (float)this[key];
        }
        public void SetFloat(string key, float value)
        {
           CheckPropertyName(key);
           SetProperty(key, AMQType.FLOAT.AsTypedValue(value));
        }
        public double GetDouble(string key)
        {
           return (double)this[key];
        }
        public void SetDouble(string key, double value)
        {
           CheckPropertyName(key);
           SetProperty(key, AMQType.DOUBLE.AsTypedValue(value));
        }
        public decimal GetDecimal(string key)
        {
           return (decimal)this[key];
        }
        public void SetDecimal(string key, decimal value)
        {
           CheckPropertyName(key);
           SetProperty(key, AMQType.DECIMAL.AsTypedValue(value));
        }
        public string GetString(string key)
        {
           return (string)this[key];
        }
        public void SetString(string key, string value)
        {
           CheckPropertyName(key);
           if ( value == null )
              SetProperty(key, AMQType.VOID.AsTypedValue(null));
           else
              SetProperty(key, AMQType.LONG_STRING.AsTypedValue(value));
        }
        public byte[] GetBytes(string key)
        {
           return (byte[])this[key];
        }
        public void SetBytes(string key, byte[] value)
        {
           CheckPropertyName(key);
           SetProperty(key, AMQType.BINARY.AsTypedValue(value));
        }
        public ushort GetUInt16(string key)
        {
           return (ushort)this[key];
        }
        public void SetUInt16(string key, ushort value)
        {
           CheckPropertyName(key);
           SetProperty(key, AMQType.UINT16.AsTypedValue(value));
        }
        public uint GetUInt32(string key)
        {
           return (uint)this[key];
        }
        public void SetUInt32(string key, uint value)
        { 
           CheckPropertyName(key);
           SetProperty(key, AMQType.UINT32.AsTypedValue(value));
        }
        public ulong GetUInt64(string key)
        {
           return (ulong)this[key];
        }
        public void SetUInt64(string key, ulong value)
        {
           CheckPropertyName(key);
           SetProperty(key, AMQType.UINT64.AsTypedValue(value));
        } 

        #endregion // Typed Setters and Getters

        #region Public Methods
        //
        // Public Methods
        //

        /// <summary>
        /// Removes the property with the specified name
        /// </summary>
        /// <param name="key">The name of the property to remove</param>
        /// <returns>The previous value of the property or null</returns>
        public AMQTypedValue RemoveKey(string key)
        {
           InitMapIfNecessary();
           _encodedForm = null;
           AMQTypedValue value = (AMQTypedValue)_properties[key];
           if ( value != null )
           {
              _properties.Remove(key);
              _encodedSize -= EncodingUtils.EncodedShortStringLength(key);
              _encodedSize--;
              _encodedSize -= value.EncodingLength;

           }
           return value;
        }
        

        /// <summary>
        /// Remove the property with the specified name
        /// </summary>
        /// <param name="key">The name of the property to remove</param>
        public void Remove(string key)
        {
           RemoveKey(key);
        }

        /// <summary>
        /// Remove all properties from the table
        /// </summary>
        public void Clear()
        {
           InitMapIfNecessary();
           _encodedForm = null;
           _properties.Clear();
           _encodedSize = 0;
        }

        /// <summary>
        /// Adds all the items from one field table in this one. Will overwrite any items in the current table
        /// with the same key.
        /// </summary>
        /// <param name="ft">the source field table</param>
        public void AddAll(IFieldTable ft)
        {
           foreach ( DictionaryEntry dictionaryEntry in ft )
           {
              this[(string)dictionaryEntry.Key] = dictionaryEntry.Value;
           }
        }

        /// <summary>
        /// Get a enumerator over the internal property set.
        /// Notice the enumerator will DictionaryEntry objects with 
        /// a string as the Key and an <see cref="AMQTypedValue"/> instance as the value
        /// </summary>
        /// <returns>The enumerator object</returns>
        public IEnumerator GetEnumerator()
        {
           InitMapIfNecessary();
           return _properties.GetEnumerator();
        }

        /// <summary>
        /// Indicates if a property with the given name exists
        /// </summary>
        /// <param name="s">Property name to check</param>
        /// <returns>True if the property exists</returns>
        public bool Contains(string s)
        {
           InitMapIfNecessary();
           return _properties.Contains(s);
        }
        
        /// <summary>
        /// Returns a dictionary mapping Property Names to the corresponding
        /// <see cref="AMQTypedValue"/> value
        /// </summary>
        /// <returns>The internal dictionary</returns>
        public IDictionary AsDictionary()
        {
           InitMapIfNecessary();
           return _properties;
        }

        /// <summary>
        /// Returns a string representation of this field table
        /// </summary>
        /// <returns>A string</returns>
        public override string ToString()
        {
           StringBuilder sb = new StringBuilder("FieldTable {");

           bool first = true;
           InitMapIfNecessary();
           foreach ( DictionaryEntry entry in _properties )
           {
              if ( !first )
              {
                 sb.Append(", ");
              }
              first = false;
              sb.Append(entry.Key).Append(" => ").Append(entry.Value);
           }

           sb.Append("}");
           return sb.ToString();
        }
        
        /// <summary>
        /// Serializes this instance to the specified <see cref="ByteBuffer"/>.
        /// </summary>
        /// <param name="buffer">The buffer to write to</param>
        public void WriteToBuffer(ByteBuffer buffer)
        {
            if ( _log.IsDebugEnabled )
            {
               _log.Debug("FieldTable::writeToBuffer: Writing encoded length of " + EncodedSize + "...");
            }

           EncodingUtils.WriteUnsignedInteger(buffer, EncodedSize);
           WritePayload(buffer);
        }

        /// <summary>
        /// Returns a byte array with the serialized representation
        /// of this field table
        /// </summary>
        /// <returns>An array of bytes</returns>
        public byte[] GetDataAsBytes()
        {
           ByteBuffer buffer = ByteBuffer.Allocate((int)_encodedSize);
           WritePayload(buffer);
           byte[] result = new byte[_encodedSize];
           buffer.Flip();
           buffer.GetBytes(result);
           //buffer.Release();
           return result;
        }

        #endregion // Public Methods

        #region Private Methods
        //
        // Private Methods
        //

        private static void CheckPropertyName(string propertyName)
        {
           if ( propertyName == null || propertyName.Length == 0 )
              throw new ArgumentNullException("propertyName");
           CheckIdentifierFormat(propertyName);
        }

        private static void CheckIdentifierFormat(string propertyName)
        {
           //        AMQP Spec: 4.2.5.5 Field Tables
           //        Guidelines for implementers:
           //           * Field names MUST start with a letter, '$' or '#' and may continue with
           //             letters, '$' or '#', digits, or underlines, to a maximum length of 128
           //             characters.
           //           * The server SHOULD validate field names and upon receiving an invalid
           //             field name, it SHOULD signal a connection exception with reply code
           //             503 (syntax error). Conformance test: amq_wlp_table_01.
           //           * A peer MUST handle duplicate fields by using only the first instance.


           // AMQP length limit
           if ( propertyName.Length > 128 )
           {
              throw new ArgumentException("AMQP limits property names to 128 characters");
           }

           // AMQ start character
           if ( !(Char.IsLetter(propertyName[0])
                 || propertyName[0] == '$'
                 || propertyName[0] == '#'
                 || propertyName[0] == '_' ) )// Not official AMQP added for JMS.
           {
              throw new ArgumentException("Identifier '" + propertyName + "' does not start with a valid AMQP start character");
           }
        }

        private object GetObject(string key)
        {
           AMQTypedValue value = GetProperty(key);
           return value != null ? value.Value : null;
        }
        
        private void SetObject(string key, object value)
        {
           if ( value is bool )
           {
              SetBoolean(key, (bool)value);
           } else if ( value is byte )
           {
              SetByte(key, (byte)value);
           } else if ( value is sbyte )
           {
              SetSByte(key, (sbyte)value);
           } else if ( value is short )
           {
              SetInt16(key, (short)value);
           } else if ( value is ushort )
           {
              SetUInt16(key, (ushort)value);
           } else if ( value is int )
           {
              SetInt32(key, (int) value);
           } else if ( value is uint )
           {
              SetUInt32(key, (uint)value);
           } else if ( value is long )
           {
              SetInt64(key, (long) value);
           } else if ( value is ulong )
           {
              SetUInt64(key, (ulong)value);
           } else if ( value is char )
           {
              SetChar(key, (char) value);
           } else if ( value is float )
           {
              SetFloat(key, (float) value);
           } else if ( value is double )
           {
              SetDouble(key, (double) value);
           } else if ( value is decimal )
           {
              SetDecimal(key, (decimal) value);
           } else if ( value is string )
           {
              SetString(key, (string) value);
           } else if ( value is byte[] )
           {
              SetBytes(key, (byte[])value);
           } else
           {
              throw new ArgumentException("Data type not supported yet");
           }
        }
        
        private AMQTypedValue GetProperty(string name)
        {
           InitMapIfNecessary();
           return (AMQTypedValue) _properties[name];
        }

        private void PopulateFromBuffer()
        {
           try
           {
              ByteBuffer buffer = _encodedForm;
              _encodedForm = null;
              if ( buffer != null )
                 SetFromBuffer(buffer, _encodedSize);
           } catch ( AMQFrameDecodingException e )
           {
              _log.Error("Error decoding FieldTable in deferred decoding mode ", e);
              throw;
           }
        }

        private void SetFromBuffer(ByteBuffer buffer, uint length)
        {
           bool trace = _log.IsDebugEnabled;
           if ( length > 0 )
           {
              int expectedRemaining = buffer.Remaining - (int)length;
              _properties = new LinkedHashtable();

              do
              {
                 string key = EncodingUtils.ReadShortString(buffer);
                 AMQTypedValue value = AMQTypedValue.ReadFromBuffer(buffer);
                 if ( trace )
                 {
                    _log.Debug(string.Format("FieldTable::PropFieldTable(buffer,{0}): Read type '{1}', key '{2}', value '{3}'", length, value.Type, key, value.Value));
                 }
                 _properties.Add(key, value);

              } while ( buffer.Remaining > expectedRemaining );
              _encodedSize = length;
           }
           if ( trace )
           {
              _log.Debug("FieldTable::FieldTable(buffer," + length + "): Done.");
           }
        }

        private void InitMapIfNecessary()
        {
           lock ( _syncLock )
           {
              if ( _properties == null )
              {
                 if ( _encodedForm == null )
                 {
                    _properties = new LinkedHashtable();
                 } else
                 {
                    PopulateFromBuffer();
                 }
              }
           }
        }

        private AMQTypedValue SetProperty(string key, AMQTypedValue value)
        {
           InitMapIfNecessary();
           _encodedForm = null;
           if ( value == null )
           {
              RemoveKey(key);
           }
           AMQTypedValue oldVal = (AMQTypedValue)_properties[key];
           _properties.Add(key, value);
           if ( oldVal != null )
           {
              _encodedSize -= oldVal.EncodingLength;
           } else
           {
              _encodedSize += EncodingUtils.EncodedShortStringLength(key) + (uint)1;
           }
           if ( value != null )
           {
              _encodedSize += value.EncodingLength;
           }

           return oldVal;
        }

        public void WritePayload(ByteBuffer buffer)
        {
           if ( _encodedForm != null )
           {
              lock ( _syncLock )
              {
                 buffer.Put(_encodedForm);
                 _encodedForm.Flip();
              }
           } else if ( _properties != null )
           {
              foreach ( DictionaryEntry de in _properties )
              {
                 string key = (string)de.Key;
                 AMQTypedValue value = (AMQTypedValue)de.Value;
                 try
                 {
                    if ( _log.IsDebugEnabled )
                    {
                       _log.Debug("Writing Property:" + key +
                                  " Type:" + value.Type +
                                  " Value:" + value.Value);
                       _log.Debug("Buffer Position:" + buffer.Position +
                                  " Remaining:" + buffer.Remaining);
                    }
                    //Write the actual parameter name
                    EncodingUtils.WriteShortStringBytes(buffer, key);
                    value.WriteToBuffer(buffer);
                 } catch ( Exception ex )
                 {
                    if ( _log.IsDebugEnabled )
                    {
                       _log.Debug("Exception thrown:" + ex);
                       _log.Debug("Writing Property:" + key +
                                  " Type:" + value.Type +
                                  " Value:" + value.Value);
                       _log.Debug("Buffer Position:" + buffer.Position +
                                  " Remaining:" + buffer.Remaining);
                    }
                    throw;
                 }
              }
           }
        }
        #endregion // Private Methods
    }
}
