//
// Copyright (c) 2008-2011, Kenneth Bell
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
//

namespace DiscUtils.Registry
{
    using System;
    using System.Globalization;
    using System.Text;

    /// <summary>
    /// A registry value.
    /// </summary>
    internal sealed class RegistryValue
    {
        private RegistryHive _hive;
        private ValueCell _cell;

        internal RegistryValue(RegistryHive hive, ValueCell cell)
        {
            _hive = hive;
            _cell = cell;
        }

        /// <summary>
        /// Gets the name of the value, or empty string if unnamed.
        /// </summary>
        public string Name
        {
            get { return _cell.Name ?? string.Empty; }
        }

        /// <summary>
        /// Gets the type of the value.
        /// </summary>
        public RegistryValueType DataType
        {
            get { return _cell.DataType; }
        }

        /// <summary>
        /// Gets the value data mapped to a .net object.
        /// </summary>
        /// <remarks>The mapping from registry type of .NET type is as follows:
        /// <list type="table">
        ///   <listheader>
        ///     <term>Value Type</term>
        ///     <term>.NET type</term>
        ///   </listheader>
        ///   <item>
        ///     <description>String</description>
        ///     <description>string</description>
        ///   </item>
        ///   <item>
        ///     <description>ExpandString</description>
        ///     <description>string</description>
        ///   </item>
        ///   <item>
        ///     <description>Link</description>
        ///     <description>string</description>
        ///   </item>
        ///   <item>
        ///     <description>DWord</description>
        ///     <description>uint</description>
        ///   </item>
        ///   <item>
        ///     <description>DWordBigEndian</description>
        ///     <description>uint</description>
        ///   </item>
        ///   <item>
        ///     <description>MultiString</description>
        ///     <description>string[]</description>
        ///   </item>
        ///   <item>
        ///     <description>QWord</description>
        ///     <description>ulong</description>
        ///   </item>
        /// </list>
        /// </remarks>
        public object Value
        {
            get
            {
                return ConvertToObject(GetData(), DataType);
            }
        }

        /// <summary>
        /// The raw value data as a byte array.
        /// </summary>
        /// <returns>The value as a raw byte array.</returns>
        public byte[] GetData()
        {
            if (_cell.DataLength < 0)
            {
                int len = _cell.DataLength & 0x7FFFFFFF;
                byte[] buffer = new byte[4];
                Utilities.WriteBytesLittleEndian(_cell.DataIndex, buffer, 0);

                byte[] result = new byte[len];
                Array.Copy(buffer, result, len);
                return result;
            }

            return _hive.RawCellData(_cell.DataIndex, _cell.DataLength);
        }

        /// <summary>
        /// Sets the value as raw bytes, with no validation that enough data is specified for the given value type.
        /// </summary>
        /// <param name="data">The data to store.</param>
        /// <param name="offset">The offset within <c>data</c> of the first byte to store.</param>
        /// <param name="count">The number of bytes to store.</param>
        /// <param name="valueType">The type of the data.</param>
        public void SetData(byte[] data, int offset, int count, RegistryValueType valueType)
        {
            // If we can place the data in the DataIndex field, do that to save space / allocation
            if ((valueType == RegistryValueType.Dword || valueType == RegistryValueType.DwordBigEndian) && count <= 4)
            {
                if (_cell.DataLength >= 0)
                {
                    _hive.FreeCell(_cell.DataIndex);
                }

                _cell.DataLength = (int)((uint)count | 0x80000000);
                _cell.DataIndex = Utilities.ToInt32LittleEndian(data, offset);
                _cell.DataType = valueType;
            }
            else
            {
                if (_cell.DataIndex == -1 || _cell.DataLength < 0)
                {
                    _cell.DataIndex = _hive.AllocateRawCell(count);
                }

                if (!_hive.WriteRawCellData(_cell.DataIndex, data, offset, count))
                {
                    int newDataIndex = _hive.AllocateRawCell(count);
                    _hive.WriteRawCellData(newDataIndex, data, offset, count);
                    _hive.FreeCell(_cell.DataIndex);
                    _cell.DataIndex = newDataIndex;
                }

                _cell.DataLength = count;
                _cell.DataType = valueType;
            }

            _hive.UpdateCell(_cell, false);
        }

        /// <summary>
        /// Sets the value stored.
        /// </summary>
        /// <param name="value">The value to store.</param>
        /// <param name="valueType">The registry type of the data.</param>
        public void SetValue(object value, RegistryValueType valueType)
        {
            if (valueType == RegistryValueType.None)
            {
                if (value is int)
                {
                    valueType = RegistryValueType.Dword;
                }
                else if (value is byte[])
                {
                    valueType = RegistryValueType.Binary;
                }
                else if (value is string[])
                {
                    valueType = RegistryValueType.MultiString;
                }
                else
                {
                    valueType = RegistryValueType.String;
                }
            }

            byte[] data = ConvertToData(value, valueType);
            SetData(data, 0, data.Length, valueType);
        }

        /// <summary>
        /// Gets a string representation of the registry value.
        /// </summary>
        /// <returns>The registry value as a string.</returns>
        public override string ToString()
        {
            return Name + ":" + DataType + ":" + DataAsString();
        }

        private static object ConvertToObject(byte[] data, RegistryValueType type)
        {
            switch (type)
            {
                case RegistryValueType.String:
                case RegistryValueType.ExpandString:
                case RegistryValueType.Link:
                    return Encoding.Unicode.GetString(data).Trim('\0');

                case RegistryValueType.Dword:
                    return Utilities.ToInt32LittleEndian(data, 0);

                case RegistryValueType.DwordBigEndian:
                    return Utilities.ToInt32BigEndian(data, 0);

                case RegistryValueType.MultiString:
                    string multiString = Encoding.Unicode.GetString(data).Trim('\0');
                    return multiString.Split('\0');

                case RegistryValueType.QWord:
                    return string.Empty + Utilities.ToUInt64LittleEndian(data, 0);

                default:
                    return data;
            }
        }

        private static byte[] ConvertToData(object value, RegistryValueType valueType)
        {
            if (valueType == RegistryValueType.None)
            {
                throw new ArgumentException("Specific registry value type must be specified", "valueType");
            }

            byte[] data;
            switch (valueType)
            {
                case RegistryValueType.String:
                case RegistryValueType.ExpandString:
                    string strValue = value.ToString();
                    data = new byte[(strValue.Length * 2) + 2];
                    Encoding.Unicode.GetBytes(strValue, 0, strValue.Length, data, 0);
                    break;

                case RegistryValueType.Dword:
                    data = new byte[4];
                    Utilities.WriteBytesLittleEndian((int)value, data, 0);
                    break;

                case RegistryValueType.DwordBigEndian:
                    data = new byte[4];
                    Utilities.WriteBytesBigEndian((int)value, data, 0);
                    break;

                case RegistryValueType.MultiString:
                    string multiStrValue = string.Join("\0", (string[])value) + "\0";
                    data = new byte[(multiStrValue.Length * 2) + 2];
                    Encoding.Unicode.GetBytes(multiStrValue, 0, multiStrValue.Length, data, 0);
                    break;

                default:
                    data = (byte[])value;
                    break;
            }

            return data;
        }

        private string DataAsString()
        {
            switch (DataType)
            {
                case RegistryValueType.String:
                case RegistryValueType.ExpandString:
                case RegistryValueType.Link:
                case RegistryValueType.Dword:
                case RegistryValueType.DwordBigEndian:
                case RegistryValueType.QWord:
                    return ConvertToObject(GetData(), DataType).ToString();

                case RegistryValueType.MultiString:
                    return string.Join(",", (string[])ConvertToObject(GetData(), DataType));

                default:
                    byte[] data = GetData();
                    string result = string.Empty;
                    for (int i = 0; i < Math.Min(data.Length, 8); ++i)
                    {
                        result += string.Format(CultureInfo.InvariantCulture, "{0:X2} ", (int)data[i]);
                    }

                    return result + string.Format(CultureInfo.InvariantCulture, " ({0} bytes)", data.Length);
            }
        }
    }
}
