namespace Unosquare.RaspberryIO.Gpio
{
    using Native;
    using Swan.Abstractions;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Linq;

    /// <summary>
    /// A simple wrapper for the I2c bus on the Raspberry Pi
    /// </summary>
    public class I2CBus : SingletonBase<I2CBus>
    {
        // TODO: It would be nice to integrate i2c device detection.
        private static readonly object SyncRoot = new object();
        private readonly Dictionary<int, I2CDevice> _devices = new Dictionary<int, I2CDevice>();

        /// <summary>
        /// Prevents a default instance of the <see cref="I2CBus"/> class from being created.
        /// </summary>
        private I2CBus()
        {
            // placeholder
        }

        /// <summary>
        /// Gets the registered devices as a read only collection.
        /// </summary>
        public ReadOnlyCollection<I2CDevice> Devices => new ReadOnlyCollection<I2CDevice>(_devices.Values.ToArray());

        /// <summary>
        /// Gets the <see cref="I2CDevice"/> with the specified device identifier.
        /// </summary>
        /// <value>
        /// The <see cref="I2CDevice"/>.
        /// </value>
        /// <param name="deviceId">The device identifier.</param>
        /// <returns>A reference to an I2C device</returns>
        public I2CDevice this[int deviceId] => GetDeviceById(deviceId);

        /// <summary>
        /// Gets the device by identifier.
        /// </summary>
        /// <param name="deviceId">The device identifier.</param>
        /// <returns>The device reference</returns>
        public I2CDevice GetDeviceById(int deviceId)
        {
            lock (SyncRoot)
            {
                return _devices[deviceId];
            }
        }

        /// <summary>
        /// Adds a device to the bus by its Id. If the device is already registered it simply returns the existing device.
        /// </summary>
        /// <param name="deviceId">The device identifier.</param>
        /// <returns>The device reference</returns>
        /// <exception cref="KeyNotFoundException">When the device file descriptor is not found</exception>
        public I2CDevice AddDevice(int deviceId)
        {
            lock (SyncRoot)
            {
                if (_devices.ContainsKey(deviceId))
                    return _devices[deviceId];

                var fileDescriptor = SetupFileDescriptor(deviceId);
                if (fileDescriptor < 0)
                    throw new KeyNotFoundException($"Device with id {deviceId} could not be registered with the I2C bus. Error Code: {fileDescriptor}.");

                var device = new I2CDevice(deviceId, fileDescriptor);
                _devices[deviceId] = device;
                return device;
            }
        }

        /// <summary>
        /// This initializes the I2C system with your given device identifier.
        /// The ID is the I2C number of the device and you can use the i2cdetect program to find this out.
        /// wiringPiI2CSetup() will work out which revision Raspberry Pi you have and open the appropriate device in /dev.
        /// The return value is the standard Linux filehandle, or -1 if any error – in which case, you can consult errno as usual.
        /// </summary>
        /// <param name="deviceId">The device identifier.</param>
        /// <returns>The Linux file handle</returns>
        private static int SetupFileDescriptor(int deviceId)
        {
            lock (SyncRoot)
            {
                return WiringPi.WiringPiI2CSetup(deviceId);
            }
        }
    }
}