// Created with MonoDevelop
//
//    Copyright (C) 2010 Hiperscan GmbH Dresden, Germany
//
//    This program is free software: you can redistribute it and/or modify
//    it under the terms of the GNU General Public License as published by
//    the Free Software Foundation, either version 3 of the License, or
//    (at your option) any later version.
//
//    This program is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU General Public License for more details.
//
//    You should have received a copy of the GNU General Public License
//    along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
using System.Runtime.Serialization.Formatters;
using System.Text;

using Hiperscan.SGS.Common;
using Hiperscan.Unix;


namespace Hiperscan.SGS.SerialInterface
{

    [Serializable()]
    public class SharedDeviceAllreadyUsedException : Exception
    {
        public SharedDeviceAllreadyUsedException()
            : this("", null) { }

        public SharedDeviceAllreadyUsedException(string message)
            : this(message, null) { }

        public SharedDeviceAllreadyUsedException(string message, Exception inner_exception)
            : base(message, inner_exception) { }

        public SharedDeviceAllreadyUsedException(System.Runtime.Serialization.SerializationInfo info,
                                                 System.Runtime.Serialization.StreamingContext context)
            : base(info, context) { }
    }

    public class RemoteSerialInterface : System.MarshalByRefObject
    {

        internal class InterfaceInfo
        {

            public InterfaceInfo(IDevice device, ISerialInterface serial_interface)
            {
                this.SerialInterface = serial_interface;
                this.DeviceInfo      = device.Info;
                this.VersionInfo     = device.VersionInfo;
                this.Channel         = null;
                this.IsConnected     = false;
                this.ClientName      = string.Empty;
                this.SessionId       = Guid.Empty;
            }

            public ISerialInterface SerialInterface { get; }
            public DeviceInfo DeviceInfo            { get; }
            public VersionInfo VersionInfo          { get; }

            public IChannel Channel  { get; internal set; }
            public bool IsConnected  { get; internal set; }
            public string ClientName { get; internal set; }
            public Guid SessionId    { get; internal set; }
        }

        private class RemoteSerialInterfaces
        {
            private readonly Dictionary<uint,InterfaceInfo> dict;

            public RemoteSerialInterfaces()
            {
                this.dict = new Dictionary<uint,InterfaceInfo>();
            }
            
            public bool Contains(uint port)
            {
                lock (this)
                    return this.dict.ContainsKey(port);
            }
            
            public void Add(uint port, InterfaceInfo info)
            {
                lock (this)
                    this.dict.Add(port, info);
            }
            
            public void Remove(uint port)
            {
                lock (this)
                    this.dict.Remove(port);
            }
            
            public uint[] Ports
            {
                get 
                { 
                    lock (this)
                        return new List<uint>(this.dict.Keys).ToArray();
                }
            }
            
            public InterfaceInfo this[uint port]
            {
                get 
                {
                    lock (this)
                        return this.dict[port];
                }
                set
                {
                    lock (this)
                        this.dict[port] = value;
                }
            }
            
            public int SharedCount
            {
                get 
                { 
                    lock (this)
                        return this.dict.Count; 
                }
            }
            
            public int ConnectedCount
            {
                get
                {
                    int count = 0;
                    lock (this)
                    {
                        foreach (InterfaceInfo info in this.dict.Values)
                        {
                            if (info.IsConnected)
                                ++count;
                        }
                    }
                    return count;
                }
            }
        }

        private static RemoteSerialInterfaces remote_interfaces = new RemoteSerialInterfaces();
        public static readonly string ServiceName = "hiperscan.remoteinterface";
        
        public delegate void StateChangedHandler();
        public static event  StateChangedHandler StateChanged;
        
        internal RemoteSerialInterface()
        {
        }
        
        internal Guid Open(uint port, string client_name)
        {
            if (remote_interfaces[port].IsConnected)
                throw new RemotingException(string.Format(Catalog.GetString("Shared device is already connected to a client ({0})."),
                                                          remote_interfaces[port].ClientName),
                                            new SharedDeviceAllreadyUsedException());
            remote_interfaces[port].IsConnected = true;
            remote_interfaces[port].ClientName = client_name;
            remote_interfaces[port].SessionId  = Guid.NewGuid();
            Console.WriteLine("Open shared device {0} from {1} (Session ID: {2}).", 
                              remote_interfaces[port].DeviceInfo.SerialNumber, 
                              remote_interfaces[port].ClientName,
                              remote_interfaces[port].SessionId);

            RemoteSerialInterface.StateChanged?.Invoke();

            return remote_interfaces[port].SessionId;
        }
        
        internal void Close(uint port)
        {
            Console.WriteLine("Close shared device {0} from {1}.", 
                              remote_interfaces[port].DeviceInfo.SerialNumber,
                              remote_interfaces[port].ClientName);
            remote_interfaces[port].IsConnected = false;
            remote_interfaces[port].ClientName = string.Empty;
            remote_interfaces[port].SessionId  = Guid.Empty;

            RemoteSerialInterface.StateChanged?.Invoke();
        }

        internal void ResetDevice(uint port, Guid session_id)
        {
            RemoteSerialInterface.CheckSessionId(port, session_id);
            remote_interfaces[port].SerialInterface.ResetDevice();
        }
        
        internal void SetTimeouts(uint port, Guid session_id, int read_timeout, int write_timeout)
        {
            RemoteSerialInterface.CheckSessionId(port, session_id);
            remote_interfaces[port].SerialInterface.SetTimeouts(read_timeout, write_timeout);
        }
        
        internal void SetTimeouts(uint port, Guid session_id)
        {
            RemoteSerialInterface.CheckSessionId(port, session_id);
            remote_interfaces[port].SerialInterface.SetTimeouts();
        }
        
        internal void SetLatency(uint port, Guid session_id)
        {
            RemoteSerialInterface.CheckSessionId(port, session_id);
            remote_interfaces[port].SerialInterface.SetLatency();
        }
        
        internal void Purge(uint port, Guid session_id)
        {
            RemoteSerialInterface.CheckSessionId(port, session_id);
            remote_interfaces[port].SerialInterface.Purge();
        }
        
        internal uint Write(uint port, Guid session_id, byte[] data)
        {
            RemoteSerialInterface.CheckSessionId(port, session_id);
            return remote_interfaces[port].SerialInterface.Write(data);
        }
        
        internal uint Read(uint port, Guid session_id, out byte[] data)
        {
            RemoteSerialInterface.CheckSessionId(port, session_id);
            return remote_interfaces[port].SerialInterface.Read(out data);
        }
        
        internal uint Read(uint port, Guid session_id, out byte[] data, uint len)
        {
            RemoteSerialInterface.CheckSessionId(port, session_id);
            return remote_interfaces[port].SerialInterface.Read(out data, len);
        }
        
        internal byte[] ReadBlocking(uint port, Guid session_id, uint len, int timeout)
        {
            RemoteSerialInterface.CheckSessionId(port, session_id);
            return remote_interfaces[port].SerialInterface.ReadBlocking(len, timeout, null);
        }
        
        internal DriverType GetDriverType(uint port)
        {
            return remote_interfaces[port].SerialInterface.DriverType;
        }
        
        internal DeviceInfo GetDeviceInfo(uint port)
        {
            return remote_interfaces[port].DeviceInfo;
        }
        
        internal VersionInfo GetVersionInfo(uint port)
        {
            return remote_interfaces[port].VersionInfo;
        }
        
        private static void CheckSessionId(uint port, Guid id)
        {
            if (id != remote_interfaces[port].SessionId)
                throw new RemotingException(Catalog.GetString("The session ID is invalid. Remoting session has been corrupted."));
        }

        internal static void Add(uint port, InterfaceInfo info)
        {
            if (remote_interfaces.Contains(port))
                throw new RemotingException(string.Format(Catalog.GetString("Port {0} is already in use by device {1}."), 
                                                          port, remote_interfaces[port].DeviceInfo.SerialNumber));
            
            string service_name = RemoteSerialInterface.ServiceName + port.ToString();
            
            IChannel channel = ChannelServices.GetChannel(service_name);
                
            if (channel == null)
            {
                System.Collections.IDictionary settings = new System.Collections.Hashtable
                {
                    ["name"] = service_name,
                    ["port"] = port,
                    ["secure"] = false,
                    ["rejectRemoteRequests"] = false
                };

                BinaryServerFormatterSinkProvider provider = new BinaryServerFormatterSinkProvider
                {
                    TypeFilterLevel = TypeFilterLevel.Full
                };
                channel = new TcpChannel(settings, null, provider);
                ChannelServices.RegisterChannel(channel, false);
            }

            try
            {
                RemotingConfiguration.RegisterWellKnownServiceType(typeof(RemoteSerialInterface), 
                                                                   service_name, 
                                                                   WellKnownObjectMode.SingleCall);
            }
            catch (RemotingException)
            {
                // uri may be already registered
            }
            
            info.Channel = channel;
            remote_interfaces.Add(port, info);
        }
        
        internal static void Remove(uint port)
        {
            if (remote_interfaces.Contains(port) == false)
                return;
            
            InterfaceInfo info = remote_interfaces[port];

            if (info.Channel != null)
                ChannelServices.UnregisterChannel(info.Channel);
            
            remote_interfaces.Remove(port);
        }
        
        public static uint GetTcpPort(IDevice device)
        {
            foreach (uint port in remote_interfaces.Ports)
            {
                if (remote_interfaces[port].DeviceInfo.SerialNumber == device.Info.SerialNumber)
                    return port;
            }
            throw new ArgumentException(Catalog.GetString("Device is not shared."));
        }
        
        public static bool IsConnected(IDevice device)
        {
            return remote_interfaces[RemoteSerialInterface.GetTcpPort(device)].IsConnected;
        }
        
        public static string GetClientName(IDevice device)
        {
            return remote_interfaces[RemoteSerialInterface.GetTcpPort(device)].ClientName;
        }
        
        public static RemoteSerialInterface GetRemoteObject(string server, uint port)
        {
            IChannel channel = ChannelServices.GetChannel(RemoteSerialInterface.ServiceName + server + port.ToString());

            if (channel == null)
            {
                System.Collections.IDictionary settings = new System.Collections.Hashtable
                {
                    ["name"] = ServiceName + server + port.ToString(),
                    ["port"] = 0,
                    ["secure"] = false
                };
                if (server == "localhost")
                    settings["bindTo"] = "127.0.0.1";

                BinaryServerFormatterSinkProvider provider = new BinaryServerFormatterSinkProvider
                {
                    TypeFilterLevel = TypeFilterLevel.Full
                };
                channel = new TcpChannel(settings, null, provider);
                ChannelServices.RegisterChannel(channel, false);
            }

            StringBuilder sb = new StringBuilder(@"tcp://");
            sb.Append(server);
            sb.Append(':');
            sb.Append(port);
            sb.Append('/');
            sb.Append(RemoteSerialInterface.ServiceName + port.ToString());

            return (RemoteSerialInterface)Activator.GetObject(typeof(RemoteSerialInterface), sb.ToString());
        }

        public void Ping()
        {
            Console.WriteLine("Ping() called.");
        }

        public static int SharedCount
        {
            get { return remote_interfaces.SharedCount; }
        }
        
        public static int ConnectedCount
        {
            get { return remote_interfaces.ConnectedCount; }
        }
    }
}

