using MainShell.EventArgsFolder;
using Stylet;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace MainShell.Hardware
{
    public interface IDeviceIoMonitorService
    {
        event EventHandler<DeviceIoChangedEventArgs> IoChanged;
        IReadOnlyList<DeviceIoPointDefinition> Definitions { get; }
        IReadOnlyDictionary<string, List<DeviceIoPointDefinition>> DefinitionsByModule { get; }
        IReadOnlyDictionary<string, DeviceIoPointState> LatestPoints { get; }
        bool IsOnline { get; }
        bool TryGetPoint(int id, out DeviceIoPointState point);
        bool TryGetPoint(string name, out DeviceIoPointState point);
        bool TryGetPointByPointKey(string pointKey, out DeviceIoPointState point);
        bool IsPointOn(int id);
        bool IsPointOn(string name);
        bool IsPointOnByPointKey(string pointKey);
        Task<bool> WaitForPointStateAsync(int id, bool expectedState, TimeSpan timeout, CancellationToken cancellationToken = default(CancellationToken));
        Task<bool> WaitForPointStateAsync(string name, bool expectedState, TimeSpan timeout, CancellationToken cancellationToken = default(CancellationToken));
        Task<bool> WaitForPointStateByPointKeyAsync(string pointKey, bool expectedState, TimeSpan timeout, CancellationToken cancellationToken = default(CancellationToken));
        IReadOnlyList<DeviceIoPointState> GetPointsByModule(string module);
        void Start();
        void Stop();
        void RequestRefresh();
        bool TryRefreshNow(TimeSpan timeout);
        Task<bool> TryRefreshNowAsync(TimeSpan timeout, CancellationToken cancellationToken = default(CancellationToken));
        bool TrySetOutputState(int id, bool outputOn);
        bool TrySetOutputState(string name, bool outputOn);
        bool TrySetOutputStateByPointKey(string pointKey, bool outputOn);
        bool TrySetOutputStates(IReadOnlyList<DeviceIoOutputWriteRequest> requests, out string failurePointReference);
        bool TrySetOutputStatesByPointKey(IReadOnlyDictionary<string, bool> outputStates, out string failurePointKey);
    }

    public class DeviceIoMonitorService : IDeviceIoMonitorService
    {
        private readonly HardwareManager _hardwareManager;
        private readonly AutoResetEvent _refreshSignal = new AutoResetEvent(false);
        private readonly TimeSpan _pollInterval = TimeSpan.FromMilliseconds(100);
        private readonly object _syncRoot = new object();
        private readonly List<DeviceIoPointDefinition> _definitions = new List<DeviceIoPointDefinition>();
        private readonly Dictionary<string, List<DeviceIoPointDefinition>> _definitionsByModule = new Dictionary<string, List<DeviceIoPointDefinition>>(StringComparer.OrdinalIgnoreCase);
        private readonly Dictionary<string, DeviceIoPointDefinition> _definitionsByName = new Dictionary<string, DeviceIoPointDefinition>(StringComparer.OrdinalIgnoreCase);
        private readonly Dictionary<string, DeviceIoPointDefinition> _definitionsByPointKey = new Dictionary<string, DeviceIoPointDefinition>(StringComparer.OrdinalIgnoreCase);
        private readonly object _refreshWaitersSyncRoot = new object();
        private readonly List<TaskCompletionSource<bool>> _refreshWaiters = new List<TaskCompletionSource<bool>>();
        private const string DeviceIoConfigFileName = "DeviceIoPoints.csv";

        private readonly Dictionary<string, DeviceIoPointState> _latestPoints = new Dictionary<string, DeviceIoPointState>(StringComparer.OrdinalIgnoreCase);
        private CancellationTokenSource _cts;

        public event EventHandler<DeviceIoChangedEventArgs> IoChanged;

        public IReadOnlyList<DeviceIoPointDefinition> Definitions => _definitions;
        public IReadOnlyDictionary<string, List<DeviceIoPointDefinition>> DefinitionsByModule => _definitionsByModule;
        public IReadOnlyDictionary<string, DeviceIoPointState> LatestPoints => GetLatestPointsSnapshot();
        public bool IsOnline => _hardwareManager.TdCard != null || _hardwareManager.AcsCard != null;
        

        public DeviceIoMonitorService(HardwareManager hardwareManager)
        {
            _hardwareManager = hardwareManager;
            LoadIoPointDefinitions();
        }

        public void Start()
        {
            if (_cts != null)
            {
                return;
            }

            LoadIoPointDefinitions();

            _cts = new CancellationTokenSource();
            var token = _cts.Token;

            Task.Run(async () =>
            {
                while (!token.IsCancellationRequested)
                {
                    _refreshSignal.WaitOne(_pollInterval);
                    if (token.IsCancellationRequested)
                    {
                        break;
                    }

                    IList<DeviceIoPointState> readPoints;
                    if (!TryReadIoPoints(out readPoints) || readPoints == null || readPoints.Count == 0)
                    {
                        CompleteRefreshWaiters(false);
                        await Task.Yield();
                        continue;
                    }

                    var changed = CollectChangedPoints(readPoints);
                    CompleteRefreshWaiters(true);
                    if (changed.Count == 0)
                    {
                        await Task.Yield();
                        continue;
                    }

                    var handler = IoChanged;
                    if (handler != null)
                    {
                        var changedCopy = changed.Select(x => x.Clone()).ToList();
                        var allCopy = GetLatestPointsSnapshot();
                        Execute.OnUIThread(() => handler(this, new DeviceIoChangedEventArgs(changedCopy, allCopy)));
                    }

                    await Task.Yield();
                }
            }, token);
        }

        public void Stop()
        {
            if (_cts == null)
            {
                return;
            }

            _cts.Cancel();
            _refreshSignal.Set();
            CompleteRefreshWaiters(false);
            _cts.Dispose();
            _cts = null;
        }

        public void RequestRefresh()
        {
            _refreshSignal.Set();
        }

        public bool TryRefreshNow(TimeSpan timeout)
        {
            return TryRefreshNowAsync(timeout).GetAwaiter().GetResult();
        }

        public async Task<bool> TryRefreshNowAsync(TimeSpan timeout, CancellationToken cancellationToken = default(CancellationToken))
        {
            Start();

            var waiter = new TaskCompletionSource<bool>();
            CancellationTokenRegistration registration = default(CancellationTokenRegistration);
            lock (_refreshWaitersSyncRoot)
            {
                _refreshWaiters.Add(waiter);
            }

            if (cancellationToken.CanBeCanceled)
            {
                registration = cancellationToken.Register(() => waiter.TrySetCanceled());
            }

            try
            {
                RequestRefresh();

                if (timeout <= TimeSpan.Zero)
                {
                    timeout = _pollInterval;
                }

                var completedTask = await Task.WhenAny(waiter.Task, Task.Delay(timeout, cancellationToken));
                if (completedTask != waiter.Task)
                {
                    RemoveRefreshWaiter(waiter);
                    return false;
                }

                return await waiter.Task;
            }
            finally
            {
                registration.Dispose();
            }
        }

        public bool TrySetOutputState(int id, bool outputOn)
        {
            var definition = ResolveUniqueDefinitionById(id, IoPointType.Output);
            if (definition == null)
            {
                return false;
            }

            var result = _hardwareManager.TryWriteIoPointValue(definition, outputOn);
            if (result)
            {
                RequestRefresh();
            }

            return result;
        }

        public bool TrySetOutputStateByPointKey(string pointKey, bool outputOn)
        {
            DeviceIoPointDefinition definition;
            if (!_definitionsByPointKey.TryGetValue(pointKey ?? string.Empty, out definition) || definition.Type != IoPointType.Output || !definition.Enabled)
            {
                return false;
            }

            var result = _hardwareManager.TryWriteIoPointValue(definition, outputOn);
            if (result)
            {
                RequestRefresh();
            }

            return result;
        }

        public bool TrySetOutputStates(IReadOnlyList<DeviceIoOutputWriteRequest> requests, out string failurePointReference)
        {
            failurePointReference = string.Empty;
            if (requests == null || requests.Count == 0)
            {
                return true;
            }

            var definitions = new List<Tuple<DeviceIoPointDefinition, bool, string>>();
            foreach (var request in requests)
            {
                if (request == null || string.IsNullOrWhiteSpace(request.PointReference))
                {
                    failurePointReference = string.Empty;
                    return false;
                }

                DeviceIoPointDefinition definition;
                if (!TryResolveOutputDefinition(request.PointReference, out definition))
                {
                    failurePointReference = request.PointReference;
                    return false;
                }

                definitions.Add(Tuple.Create(definition, request.OutputOn, request.PointReference));
            }

            foreach (var item in definitions)
            {
                if (!_hardwareManager.TryWriteIoPointValue(item.Item1, item.Item2))
                {
                    failurePointReference = item.Item3;
                    return false;
                }
            }

            RequestRefresh();
            return true;
        }

        public bool TrySetOutputStatesByPointKey(IReadOnlyDictionary<string, bool> outputStates, out string failurePointKey)
        {
            failurePointKey = string.Empty;
            if (outputStates == null || outputStates.Count == 0)
            {
                return true;
            }

            var definitions = new List<Tuple<DeviceIoPointDefinition, bool, string>>();
            foreach (var item in outputStates)
            {
                DeviceIoPointDefinition definition;
                if (!_definitionsByPointKey.TryGetValue(item.Key ?? string.Empty, out definition) || definition.Type != IoPointType.Output || !definition.Enabled)
                {
                    failurePointKey = item.Key;
                    return false;
                }

                definitions.Add(Tuple.Create(definition, item.Value, item.Key));
            }

            foreach (var item in definitions)
            {
                if (!_hardwareManager.TryWriteIoPointValue(item.Item1, item.Item2))
                {
                    failurePointKey = item.Item3;
                    return false;
                }
            }

            RequestRefresh();
            return true;
        }

        public bool TrySetOutputState(string name, bool outputOn)
        {
            DeviceIoPointDefinition definition;
            if (!_definitionsByName.TryGetValue(name ?? string.Empty, out definition) || definition.Type != IoPointType.Output || !definition.Enabled)
            {
                return false;
            }

            var result = _hardwareManager.TryWriteIoPointValue(definition, outputOn);
            if (result)
            {
                RequestRefresh();
            }

            return result;
        }

        public bool TryGetPoint(int id, out DeviceIoPointState point)
        {
            var definition = ResolveUniqueDefinitionById(id, null);
            if (definition == null)
            {
                point = null;
                return false;
            }

            lock (_syncRoot)
            {
                if (_latestPoints.TryGetValue(definition.PointKey, out point))
                {
                    point = point.Clone();
                    return true;
                }
            }

            point = null;
            return false;
        }

        public bool TryGetPointByPointKey(string pointKey, out DeviceIoPointState point)
        {
            lock (_syncRoot)
            {
                if (_latestPoints.TryGetValue(pointKey ?? string.Empty, out point))
                {
                    point = point.Clone();
                    return true;
                }
            }

            point = null;
            return false;
        }

        public bool IsPointOn(int id)
        {
            DeviceIoPointState point;
            return TryGetPoint(id, out point) && point.Value;
        }

        public bool IsPointOn(string name)
        {
            DeviceIoPointState point;
            return TryGetPoint(name, out point) && point.Value;
        }

        public bool IsPointOnByPointKey(string pointKey)
        {
            DeviceIoPointState point;
            return TryGetPointByPointKey(pointKey, out point) && point.Value;
        }

        public async Task<bool> WaitForPointStateAsync(int id, bool expectedState, TimeSpan timeout, CancellationToken cancellationToken = default(CancellationToken))
        {
            var start = DateTime.UtcNow;
            while (DateTime.UtcNow - start < timeout)
            {
                cancellationToken.ThrowIfCancellationRequested();

                var remaining = timeout - (DateTime.UtcNow - start);
                var refreshTimeout = remaining < _pollInterval ? remaining : _pollInterval;
                if (refreshTimeout > TimeSpan.Zero)
                {
                    await TryRefreshNowAsync(refreshTimeout, cancellationToken);
                }

                if (IsPointOn(id) == expectedState)
                {
                    return true;
                }

                await Task.Delay(50, cancellationToken);
            }

            return false;
        }

        public async Task<bool> WaitForPointStateByPointKeyAsync(string pointKey, bool expectedState, TimeSpan timeout, CancellationToken cancellationToken = default(CancellationToken))
        {
            var start = DateTime.UtcNow;
            while (DateTime.UtcNow - start < timeout)
            {
                cancellationToken.ThrowIfCancellationRequested();

                var remaining = timeout - (DateTime.UtcNow - start);
                var refreshTimeout = remaining < _pollInterval ? remaining : _pollInterval;
                if (refreshTimeout > TimeSpan.Zero)
                {
                    await TryRefreshNowAsync(refreshTimeout, cancellationToken);
                }

                if (IsPointOnByPointKey(pointKey) == expectedState)
                {
                    return true;
                }

                await Task.Delay(50, cancellationToken);
            }

            return false;
        }

        public async Task<bool> WaitForPointStateAsync(string name, bool expectedState, TimeSpan timeout, CancellationToken cancellationToken = default(CancellationToken))
        {
            var start = DateTime.UtcNow;
            while (DateTime.UtcNow - start < timeout)
            {
                cancellationToken.ThrowIfCancellationRequested();

                var remaining = timeout - (DateTime.UtcNow - start);
                var refreshTimeout = remaining < _pollInterval ? remaining : _pollInterval;
                if (refreshTimeout > TimeSpan.Zero)
                {
                    await TryRefreshNowAsync(refreshTimeout, cancellationToken);
                }

                if (IsPointOn(name) == expectedState)
                {
                    return true;
                }

                await Task.Delay(50, cancellationToken);
            }

            return false;
        }

        public bool TryGetPoint(string name, out DeviceIoPointState point)
        {
            DeviceIoPointDefinition definition;
            if (!_definitionsByName.TryGetValue(name ?? string.Empty, out definition) && !_definitionsByPointKey.TryGetValue(name ?? string.Empty, out definition))
            {
                point = null;
                return false;
            }

            lock (_syncRoot)
            {
                if (_latestPoints.TryGetValue(definition.PointKey, out point))
                {
                    point = point.Clone();
                    return true;
                }
            }

            point = null;
            return false;
        }

        public IReadOnlyList<DeviceIoPointState> GetPointsByModule(string module)
        {
            lock (_syncRoot)
            {
                return _latestPoints.Values
                    .Where(x => string.Equals(x.Module, module, StringComparison.OrdinalIgnoreCase))
                    .Select(x => x.Clone())
                    .ToList();
            }
        }

        private bool TryReadIoPoints(out IList<DeviceIoPointState> points)
        {
            points = new List<DeviceIoPointState>();

            if (Definitions == null || Definitions.Count == 0)
            {
                return false;
            }

            Dictionary<string, bool> readValues;
            bool hasBatchValues = _hardwareManager.TryReadIoPointValues(Definitions, out readValues);

            foreach (DeviceIoPointDefinition definition in Definitions)
            {
                bool value;
                if (!definition.Enabled)
                {
                    value = definition.DefaultValue;
                }
                else if (hasBatchValues && readValues != null && readValues.TryGetValue(definition.PointKey, out value))
                {
                }
                else if (!_hardwareManager.TryReadIoPointValue(definition, out value))
                {
                    value = definition.DefaultValue;
                }

                DeviceIoPointState state;
                if (definition.Type == IoPointType.Output)
                {
                    state = new DeviceOutputPointState
                    {
                        Id = definition.Id,
                        PointKey = definition.PointKey,
                        Module = definition.Module,
                        Name = definition.Name,
                        Card = definition.Card,
                        StationNo = definition.StationNo,
                        LineNo = definition.LineNo,
                        Value = value,
                        IsInverse = definition.IsInverse,
                        CommandValue = value
                    };
                }
                else
                {
                    state = new DeviceInputPointState
                    {
                        Id = definition.Id,
                        PointKey = definition.PointKey,
                        Module = definition.Module,
                        Name = definition.Name,
                        Card = definition.Card,
                        StationNo = definition.StationNo,
                        LineNo = definition.LineNo,
                        Value = value,
                        IsInverse = definition.IsInverse
                    };
                }

                points.Add(state);
            }

            return points.Count > 0;
        }

        private void LoadIoPointDefinitions()
        {
            _definitions.Clear();
            _definitionsByModule.Clear();
            _definitionsByName.Clear();
            _definitionsByPointKey.Clear();

            var pointKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

            var path = ResolveConfigPath();
            if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
            {
                return;
            }

            var lines = File.ReadAllLines(path, Encoding.UTF8);
            for (var i = 0; i < lines.Length; i++)
            {
                var raw = lines[i];
                if (string.IsNullOrWhiteSpace(raw))
                {
                    continue;
                }

                var line = raw.Trim();
                if (line.StartsWith("#"))
                {
                    continue;
                }

                if (i == 0 && line.IndexOf("Id", StringComparison.OrdinalIgnoreCase) >= 0)
                {
                    continue;
                }

                var cols = line.Split(',');
                if (cols.Length < 11)
                {
                    continue;
                }

                int id;
                if (!int.TryParse(cols[0].Trim(), out id))
                {
                    continue;
                }

                var moduleIndex = 1;
                var nameIndex = 2;
                var typeIndex = 3;
                var cardIndex = 4;
                var stationIndex = 5;
                var indexColumnIndex = 6;
                var subIndexColumnIndex = 7;
                var lineNoIndex = 8;
                var enabledIndex = 9;
                var defaultValueIndex = 10;
                var isInverseIndex = cols.Length >= 12 ? 11 : -1;
                var descriptionIndex = isInverseIndex >= 0 ? 12 : 11;

                var name = cols[nameIndex].Trim();
                if (string.IsNullOrWhiteSpace(name))
                {
                    continue;
                }

                var module = cols[moduleIndex].Trim();
                if (string.IsNullOrWhiteSpace(module))
                {
                    module = "Default";
                }

                var type = ParseIoPointType(cols[typeIndex]);
                var card = ParseIoCardType(cols[cardIndex]);
                var stationNo = ParseInt(cols[stationIndex]);
                var index = ParseInt(cols[indexColumnIndex]);
                var subIndex = ParseInt(cols[subIndexColumnIndex]);
                var lineNo = cols[lineNoIndex].Trim();
                var pointKey = DeviceIoPointDefinition.BuildPointKey(type, card, stationNo, index, subIndex, lineNo);

                if (!pointKeys.Add(pointKey) || !names.Add(name))
                {
                    continue;
                }

                var definition = new DeviceIoPointDefinition
                {
                    Id = id,
                    PointKey = pointKey,
                    Module = module,
                    Name = name,
                    Type = type,
                    Card = card,
                    StationNo = stationNo,
                    Index = index,
                    SubIndex = subIndex,
                    LineNo = lineNo,
                    Enabled = ParseBoolean(cols[enabledIndex], true),
                    DefaultValue = ParseBoolean(cols[defaultValueIndex], false),
                    IsInverse = isInverseIndex >= 0 && isInverseIndex < cols.Length && ParseBoolean(cols[isInverseIndex], false),
                    Description = cols.Length > descriptionIndex ? string.Join(",", cols.Skip(descriptionIndex)).Trim() : string.Empty
                };

                _definitions.Add(definition);
                _definitionsByName[definition.Name] = definition;
                _definitionsByPointKey[definition.PointKey] = definition;

                List<DeviceIoPointDefinition> moduleDefinitions;
                if (!_definitionsByModule.TryGetValue(module, out moduleDefinitions))
                {
                    moduleDefinitions = new List<DeviceIoPointDefinition>();
                    _definitionsByModule[module] = moduleDefinitions;
                }

                moduleDefinitions.Add(definition);
            }
        }

        private static int ParseInt(string raw)
        {
            int value;
            return int.TryParse((raw ?? string.Empty).Trim(), out value) ? value : 0;
        }

        private static IoPointType ParseIoPointType(string raw)
        {
            if (string.IsNullOrWhiteSpace(raw))
            {
                return IoPointType.Input;
            }

            var text = raw.Trim();
            if (string.Equals(text, "Output", StringComparison.OrdinalIgnoreCase) ||
                string.Equals(text, "OUT", StringComparison.OrdinalIgnoreCase) ||
                string.Equals(text, "DO", StringComparison.OrdinalIgnoreCase) ||
                string.Equals(text, "O", StringComparison.OrdinalIgnoreCase))
            {
                return IoPointType.Output;
            }

            return IoPointType.Input;
        }

        private static IoCardType ParseIoCardType(string raw)
        {
            if (string.IsNullOrWhiteSpace(raw))
            {
                return IoCardType.Td;
            }

            var text = raw.Trim();
            if (string.Equals(text, "Acs", StringComparison.OrdinalIgnoreCase) ||
                string.Equals(text, "ACS", StringComparison.OrdinalIgnoreCase))
            {
                return IoCardType.Acs;
            }

            return IoCardType.Td;
        }

        private static bool ParseBoolean(string raw, bool defaultValue)
        {
            if (string.IsNullOrWhiteSpace(raw))
            {
                return defaultValue;
            }

            var text = raw.Trim();
            if (string.Equals(text, "1", StringComparison.OrdinalIgnoreCase) ||
                string.Equals(text, "true", StringComparison.OrdinalIgnoreCase) ||
                string.Equals(text, "yes", StringComparison.OrdinalIgnoreCase) ||
                string.Equals(text, "y", StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }

            if (string.Equals(text, "0", StringComparison.OrdinalIgnoreCase) ||
                string.Equals(text, "false", StringComparison.OrdinalIgnoreCase) ||
                string.Equals(text, "no", StringComparison.OrdinalIgnoreCase) ||
                string.Equals(text, "n", StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }

            return defaultValue;
        }

        private DeviceIoPointDefinition ResolveUniqueDefinitionById(int id, IoPointType? pointType)
        {
            var matches = _definitions
                .Where(x => x.Id == id && (!pointType.HasValue || x.Type == pointType.Value))
                .Take(2)
                .ToList();

            return matches.Count == 1 ? matches[0] : null;
        }

        private void CompleteRefreshWaiters(bool result)
        {
            List<TaskCompletionSource<bool>> waiters;
            lock (_refreshWaitersSyncRoot)
            {
                if (_refreshWaiters.Count == 0)
                {
                    return;
                }

                waiters = _refreshWaiters.ToList();
                _refreshWaiters.Clear();
            }

            foreach (var waiter in waiters)
            {
                waiter.TrySetResult(result);
            }
        }

        private void RemoveRefreshWaiter(TaskCompletionSource<bool> waiter)
        {
            lock (_refreshWaitersSyncRoot)
            {
                _refreshWaiters.Remove(waiter);
            }
        }

        private bool TryResolveOutputDefinition(string pointReference, out DeviceIoPointDefinition definition)
        {
            definition = null;
            if (string.IsNullOrWhiteSpace(pointReference))
            {
                return false;
            }

            if (_definitionsByPointKey.TryGetValue(pointReference, out definition))
            {
                return definition.Type == IoPointType.Output && definition.Enabled;
            }

            if (_definitionsByName.TryGetValue(pointReference, out definition))
            {
                return definition.Type == IoPointType.Output && definition.Enabled;
            }

            int id;
            if (int.TryParse(pointReference, out id))
            {
                definition = ResolveUniqueDefinitionById(id, IoPointType.Output);
                return definition != null && definition.Enabled;
            }

            return false;
        }

        private static string ResolveConfigPath()
        {
            var baseDir = AppDomain.CurrentDomain.BaseDirectory;
            var localPath = Path.Combine(baseDir, "Configuration", DeviceIoConfigFileName);
            var sourcePath = ResolveRootConfigPath(baseDir);

            if (TrySyncConfigToLocal(sourcePath, localPath))
            {
                return localPath;
            }

            return File.Exists(localPath) ? localPath : null;
        }

        private static string ResolveRootConfigPath(string baseDir)
        {
            var current = new DirectoryInfo(baseDir);
            while (current != null)
            {
                var rootConfigDir = Path.Combine(current.FullName, "Configuration");
                if (Directory.Exists(rootConfigDir) && Directory.Exists(Path.Combine(current.FullName, "MainShell")))
                {
                    var path = Path.Combine(rootConfigDir, DeviceIoConfigFileName);
                    if (File.Exists(path))
                    {
                        return path;
                    }
                }

                current = current.Parent;
            }

            return null;
        }

        private static bool TrySyncConfigToLocal(string sourcePath, string localPath)
        {
            if (string.IsNullOrWhiteSpace(sourcePath) || !File.Exists(sourcePath))
            {
                return false;
            }

            if (!string.Equals(Path.GetFullPath(sourcePath), Path.GetFullPath(localPath), StringComparison.OrdinalIgnoreCase))
            {
                var localDir = Path.GetDirectoryName(localPath);
                if (!string.IsNullOrWhiteSpace(localDir))
                {
                    Directory.CreateDirectory(localDir);
                }

                if (!File.Exists(localPath) || File.GetLastWriteTimeUtc(localPath) < File.GetLastWriteTimeUtc(sourcePath))
                {
                    File.Copy(sourcePath, localPath, true);
                }
            }

            return true;
        }

        private List<DeviceIoPointState> CollectChangedPoints(IList<DeviceIoPointState> readPoints)
        {
            var changed = new List<DeviceIoPointState>();

            lock (_syncRoot)
            {
                if (_latestPoints.Count == 0)
                {
                    foreach (var point in readPoints)
                    {
                        if (point == null)
                        {
                            continue;
                        }

                        _latestPoints[point.PointKey] = PreparePoint(point.Clone());
                    }

                    return changed;
                }

                foreach (var point in readPoints)
                {
                    if (point == null)
                    {
                        continue;
                    }

                    if (!_latestPoints.TryGetValue(point.PointKey, out DeviceIoPointState old))
                    {
                        _latestPoints[point.PointKey] = PreparePoint(point.Clone());
                        changed.Add(PreparePoint(point.Clone()));
                        continue;
                    }

                    if (HasPointChanged(old, point))
                    {
                        _latestPoints[point.PointKey] = PreparePoint(point.Clone());
                        changed.Add(PreparePoint(point.Clone()));
                    }
                }
            }

            return changed;
        }

        private IReadOnlyDictionary<string, DeviceIoPointState> GetLatestPointsSnapshot()
        {
            lock (_syncRoot)
            {
                return _latestPoints.ToDictionary(x => x.Key, x => PreparePoint(x.Value.Clone()));
            }
        }

        private DeviceIoPointState PreparePoint(DeviceIoPointState point)
        {
            var outputPoint = point as DeviceOutputPointState;
            DeviceIoPointDefinition definition;
            _definitionsByPointKey.TryGetValue(point.PointKey ?? string.Empty, out definition);
            if (outputPoint != null && definition != null && definition.Enabled)
            {
                outputPoint.BindWriteAction(outputOn => TrySetOutputState(definition.Name, outputOn));
            }

            return point;
        }

        private static bool HasPointChanged(DeviceIoPointState oldPoint, DeviceIoPointState newPoint)
        {
            if (oldPoint == null || newPoint == null)
            {
                return true;
            }

            if (oldPoint.Value != newPoint.Value ||
                oldPoint.PointType != newPoint.PointType ||
                !string.Equals(oldPoint.PointKey, newPoint.PointKey, StringComparison.OrdinalIgnoreCase) ||
                !string.Equals(oldPoint.Name, newPoint.Name, StringComparison.OrdinalIgnoreCase) ||
                !string.Equals(oldPoint.Module, newPoint.Module, StringComparison.OrdinalIgnoreCase) ||
                oldPoint.Card != newPoint.Card)
            {
                return true;
            }

            var oldOutput = oldPoint as DeviceOutputPointState;
            var newOutput = newPoint as DeviceOutputPointState;
            if (oldOutput != null && newOutput != null)
            {
                return oldOutput.CommandValue != newOutput.CommandValue;
            }

            return false;
        }
    }

}
