using Autofac;
using Ion.Sdk.Ddr;
using Ion.Sdk.Ddr.Extensions;
using Ion.Sdk.DemoApp.Views.ViewModels;
using Ion.Sdk.Ici;
using Ion.Sdk.Idi;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using Ion.Sdk.DemoApp.Framework;
using static Ion.Sdk.Ici.Channel.BlackBox.Message;
using RegionInfo = Ion.Sdk.Ddr.RegionInfo;

namespace Ion.Sdk.DemoApp.Views
{
    /// <summary>
    /// Interaction logic for BlackBoxTester.xaml
    /// </summary>
    public partial class BlackBox : UserControl, IAppView
    {
        private readonly IApplication application;
        private readonly ViewModel viewModel;
        private readonly ILifetimeScope scope;
        public IAppViewInfo Info => ViewInfo;

        public string SelectedPort
        {
            get => (string)GetValue(SelectedPortProperty);
            set => SetValue(SelectedPortProperty, value);
        }

        public int SelectedBaudRate
        {
            get => (int)GetValue(SelectedBaudRateProperty);
            set => SetValue(SelectedBaudRateProperty, value);
        }

        public bool? IsOnline
        {
            get => (bool?)GetValue(IsOnlineProperty);
            set => SetValue(IsOnlineProperty, value);
        }

        public string[] AvailablePorts
        {
            get => GetValue(AvailablePortsProperty) as string[];
            set => SetValue(AvailablePortsProperty, value);
        }

        public int[] AvailableBaudRates => new[] { 9600, 57600, 115200 };

        public Framework.Command RefreshAvailablePortsCommand { get; }
        public Framework.Command StartSingleTestCommand { get; }
        public Framework.Command StartAutoSequenceCommand { get; }
        public Framework.Command StopCommand { get; }
        public Framework.Command RequestKeysCommand { get; }
        public Framework.Command SendKeyCommand { get; }
        public Framework.Command RequestActionsCommand { get; }
        public Framework.Command ExecuteActionCommand { get; }
        public Framework.Command SendResponseCommand { get; }
        public Framework.Command SelectRegionCommand { get; }
        public Framework.Command SelectMeasurementCommand { get; }

        public ProgressIndicator Progress { get => viewModel.Progress; }

        public BlackBox(IApplication application)
        {
            this.application = application;
            RefreshAvailablePortsCommand = Framework.Command.Create(parameter => AvailablePorts = System.IO.Ports.SerialPort.GetPortNames());
            StartSingleTestCommand = Framework.Command.CreateAsync(parameter => viewModel.StartSingleTest(SelectedPort), parameter => IsOnline == true && viewModel.SelectedRegion != null);
            StartAutoSequenceCommand = Framework.Command.CreateAsync(parameter => viewModel.StartAutoSequence(SelectedPort), parameter => IsOnline == true && !string.IsNullOrEmpty(viewModel.AutoSequenceName) && viewModel.SelectedRegion != null);
            StopCommand = Framework.Command.CreateAsync(parameter => viewModel.Stop(), parameter => IsOnline == true && viewModel?.BlackBoxProgress.IsBusy == true);
            RequestKeysCommand = Framework.Command.Create(parameter => viewModel.RequestKeys(), parameter => IsOnline == true && viewModel?.BlackBoxProgress.IsBusy == true);
            SendKeyCommand = Framework.Command.Create(parameter => viewModel.SendKey(parameter as string), parameter => IsOnline == true && viewModel?.BlackBoxProgress.IsBusy == true);
            RequestActionsCommand = Framework.Command.Create(parameter => viewModel.RequestActions(), parameter => IsOnline == true && viewModel?.BlackBoxProgress.IsBusy == true);
            ExecuteActionCommand = Framework.Command.Create(parameter => viewModel.ExecuteAction(parameter as string), parameter => IsOnline == true && viewModel?.BlackBoxProgress.IsBusy == true);
            SendResponseCommand = Framework.Command.Create(parameter => viewModel.SendResponse(parameter));
            SelectRegionCommand = Framework.Command.Create(parameter => viewModel.SelectedRegion = parameter as RegionInfo, parameter => IsOnline == true && viewModel?.BlackBoxProgress.IsBusy == false);
            SelectMeasurementCommand = Framework.Command.Create(parameter => viewModel.SelectedMeasurementInfo = parameter as MeasurementInfo, parameter => IsOnline == true && viewModel?.BlackBoxProgress.IsBusy == false);

            InitializeComponent();

            scope = application.BeginLifetimeScope();
            Root.DataContext = viewModel = scope.Resolve<ViewModel>();

            viewModel.PropertyChanged += ViewModel_PropertyChanged;
            viewModel.InfoTexts.CollectionChanged += InfoTexts_CollectionChanged;
        }

        private void EvaluateCommands()
        {
            RefreshAvailablePortsCommand.RaiseCanExecuteChanged();
            StartSingleTestCommand.RaiseCanExecuteChanged();
            StartAutoSequenceCommand.RaiseCanExecuteChanged();
            StopCommand.RaiseCanExecuteChanged();
            RequestKeysCommand.RaiseCanExecuteChanged();
            SendKeyCommand.RaiseCanExecuteChanged();
            RequestActionsCommand.RaiseCanExecuteChanged();
            ExecuteActionCommand.RaiseCanExecuteChanged();
            SelectRegionCommand.RaiseCanExecuteChanged();
            SelectMeasurementCommand.RaiseCanExecuteChanged();
        }

        private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
            => Dispatcher.Invoke(EvaluateCommands);

        private void InfoTexts_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
            => Dispatcher.Invoke(() => InfoTextsScrollViewer.ScrollToEnd());

        private void BlackBoxPasswordBox_OnPasswordChanged(object sender, RoutedEventArgs e)
        {
            if (sender is PasswordBox passwordBox)
                viewModel.BlackBoxPassword = passwordBox.Password;
        }

        private readonly Regex numericInputRegex = new Regex("[^0-9]+");
        private void ValueTextBox_OnPreviewTextInput(object sender, TextCompositionEventArgs e)
        {
            var keyboardMessage = (sender as TextBox)?.DataContext as Channel.BlackBox.Message.Keyboard;
            if (keyboardMessage?.Type == BlackBoxMessageTextTypes.Numeric)
            {
                e.Handled = numericInputRegex.IsMatch(e.Text);
            }
        }

        private void Button_Suppress_OnClick(object sender, RoutedEventArgs e) => e.Handled = true; // suppress routed event.

        #region -=Static members=-

        public static readonly IAppViewInfo ViewInfo = new AppViewInfo { Id = nameof(BlackBox), Name = "BlackBox" };

        public static readonly DependencyProperty AvailablePortsProperty = DependencyProperty.Register("AvailablePorts", typeof(string[]), typeof(BlackBox), new PropertyMetadata(System.IO.Ports.SerialPort.GetPortNames()));

        public static readonly DependencyProperty SelectedPortProperty = DependencyProperty.Register(
            "SelectedPort", typeof(string), typeof(BlackBox), new PropertyMetadata(default(string), OnSelectedPortPropertyChanged));

        public static readonly DependencyProperty SelectedBaudRateProperty = DependencyProperty.Register(
            "SelectedBaudRate", typeof(int), typeof(BlackBox), new PropertyMetadata(115200));


        public static readonly DependencyProperty IsOnlineProperty = DependencyProperty.Register("IsOnline", typeof(bool?), typeof(BlackBox),
            new UIPropertyMetadata(default(bool?), (sender, args) =>
            {
                var view = sender as BlackBox;
                view?.EvaluateCommands();
            }));
        private static async void OnSelectedPortPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is BlackBox view && e.NewValue is string port)
            {
                try
                {
                    view.IsOnline = null;
                    int baudRate = view.SelectedBaudRate;
                    using (var instrument = await Task.Run(() => Channel.Serial.Open(port, baudRate)))
                    {
                        var isOnline = await instrument.IsOnline();
                        if (port == view.SelectedPort)
                            view.IsOnline = isOnline;
                        if (isOnline)
                            view.viewModel.Instrument = await instrument.GetInstrumentInfo();
                    }
                }
                catch (Exception ex)
                {
                    view.application.Log(ex);
                    if (port == view.SelectedPort)
                        view.IsOnline = false;
                }
            }
        }
        #endregion

        #region -=Definitions=-

        public sealed class ViewModel : BindableObject, IDisposable
        {
            private readonly IApplication application;

            #region -=Properties/Fields=-

            private const int polarityMeasurementId = 147;

            private string selectedPort = string.Empty;

            private Lazy<IBlackBoxChannelTaskSource> lazyChannel;
            private readonly List<IDisposable> subscriptions = new List<IDisposable>();

            private const string StatusIdle = "Status - Idle";

            private Request currentRequest = null;

            public Request CurrentRequest
            {
                get => currentRequest;
                private set => Set(ref currentRequest, value);
            }

            private string actionInfo = StatusIdle;

            public string ActionInfo
            {
                get => actionInfo;
                set => Set(ref actionInfo, value);
            }

            private RegionInfo selectedRegion = null;

            public RegionInfo SelectedRegion
            {
                get => selectedRegion;
                set
                {
                    Set(ref selectedRegion, value);
                    LoadMeasurements(selectedRegion, instrument);
                }
            }

            public ObservableCollection<RegionInfo> RegionsList { get; } = new ObservableCollection<RegionInfo>();


            private MeasurementInfo selectedMeasurementInfo = null;

            public MeasurementInfo SelectedMeasurementInfo
            {
                get => selectedMeasurementInfo;
                set => Set(ref selectedMeasurementInfo, value);
            }


            private List<MeasurementInfo> measurementsList = new List<MeasurementInfo>();
            public List<MeasurementInfo> MeasurementsList
            {
                get => measurementsList;
                set => Set(ref measurementsList, value);
            }

            private bool touchPreTest = true;
            public bool TouchPreTest
            {
                get => touchPreTest;
                set => Set(ref touchPreTest, value);
            }

            private string blackBoxPassword;
            public string BlackBoxPassword
            {
                get => blackBoxPassword;
                set => Set(ref blackBoxPassword, value);
            }

            private bool showIntermediateResults = false;
            public bool ShowIntermediateResults
            {
                get => showIntermediateResults;
                set => Set(ref showIntermediateResults, value);
            }

            private string autoSequenceName = "";
            public string AutoSequenceName
            {
                get => autoSequenceName;
                set => Set(ref autoSequenceName, value);
            }

            public ObservableCollection<string> InfoTexts { get; } = new ObservableCollection<string>();

            private List<string> actions = new List<string>();

            public List<string> Actions
            {
                get => actions;
                set => Set(ref actions, value);
            }

            private List<string> keys = new List<string>();

            public List<string> Keys
            {
                get => keys;
                set => Set(ref keys, value);
            }

            public ProgressIndicator Progress { get; } = new ProgressIndicator();
            public ProgressIndicator BlackBoxProgress { get; } = new ProgressIndicator();
            private IBlackBoxChannelTaskSource Channel => lazyChannel.Value;

            private IInstrumentInfo instrument = null;
            public IInstrumentInfo Instrument
            {
                get => instrument;
                set
                {
                    if (Set(ref instrument, value))
                    {
                        LoadRegions(instrument);
                        LoadMeasurements(SelectedRegion, instrument);
                    }
                }
            }

            private string Port
            {
                get => selectedPort;
                set
                {
                    var newValue = value ?? string.Empty;
                    if (Set(ref selectedPort, newValue, () => string.CompareOrdinal(selectedPort, newValue) != 0))
                        ResetChannel();
                }
            }

            #endregion

            public ViewModel(IApplication application)
            {
                this.application = application;
                ResetChannel();

                #region -=Subscribe to BlackBox messages=-


                subscriptions.Add(Ici.Channel.BlackBox.Get<Error>()
                                                       .ObserveOnDispatcher()
                                                       .Subscribe(OnBlackBoxError));

                subscriptions.Add(Ici.Channel.BlackBox.Get<Notification>()
                                                       .ObserveOnDispatcher()
                                                       .Subscribe(OnBlackBoxRequest));

                subscriptions.Add(Ici.Channel.BlackBox.Get<Question>()
                                                       .ObserveOnDispatcher()
                                                       .Subscribe(OnBlackBoxRequest));
                subscriptions.Add(Ici.Channel.BlackBox.Get<Channel.BlackBox.Message.Keyboard>()
                                                       .ObserveOnDispatcher()
                                                       .Subscribe(OnBlackBoxRequest));

                subscriptions.Add(Ici.Channel.BlackBox.Get<AvailableActions>()
                                                       .ObserveOnDispatcher()
                                                       .Subscribe(message => Actions = message.Actions));

                subscriptions.Add(Ici.Channel.BlackBox.Get<AvailableKeys>()
                                                       .ObserveOnDispatcher()
                                                       .Subscribe(message => Keys = message.Keys));

                subscriptions.Add(Ici.Channel.BlackBox.Get<IntermediateResult>()
                                                       .ObserveOnDispatcher()
                                                       .Subscribe(message =>
                                                       {
                                                           InfoTexts.Add($"Intermediate result [{message.Result.Info.GetText(Configuration.DefaultLanguage)}] = {message.Result.Value?.GetText() ?? "null"}; Status = {message.Result.Status}");
                                                       }));

                subscriptions.Add(Ici.Channel.BlackBox.Get<TouchTest>()
                             .ObserveOnDispatcher()
                             .Subscribe(message =>
                             {
                                 InfoTexts.Add($"Touch test: {message.Option}");
                                 RefreshActionsAndKeys();
                             }));

                subscriptions.Add(Ici.Channel.BlackBox.Get<AutoSequencePause>()
                                                       .ObserveOnDispatcher()
                                                       .Subscribe(message =>
                                                        {
                                                            InfoTexts.Add("AutoSequence Paused...");
                                                            RefreshActionsAndKeys();
                                                        }));

                subscriptions.Add(Ici.Channel.BlackBox.Get<AutoSequenceStepEndDecision>()
                                                       .ObserveOnDispatcher()
                                                       .Subscribe(message =>
                                                       {
                                                           InfoTexts.Add("AutoSequence step ended. Awaiting decision...");
                                                           RefreshActionsAndKeys();
                                                       }));

                subscriptions.Add(Ici.Channel.BlackBox.Get<AutoSequenceResult>()
                    .ObserveOnDispatcher()
                    .Subscribe(message =>
                    {
                        InfoTexts.Add($"Measured: {message.Test.Measurement.Info.GetText(Configuration.DefaultLanguage)} =  {message.Test.Measurement.Results.FirstOrDefault()?.Value?.GetText() ?? "null"}; Status = {message.Test.Measurement.Status}");
                    }));

                #endregion
            }

            public async Task StartSingleTest(object parameter)
            {
                if (selectedRegion == null)
                {
                    InfoTexts.Add("Region not selected.");
                    return;
                }
                var region = new Region((uint)(selectedRegion.MasterRegionInfoId ?? 0), (uint)selectedRegion.Id);
                var measurementInfo = SelectedMeasurementInfo;
                if (parameter is string port && measurementInfo != null)
                {
                    try
                    {
                        InfoTexts.Clear();
                        InfoTexts.Add("Starting...");

                        BlackBoxProgress.IsBusy = true;
                        ActionInfo = "Starting single test...";

                        Port = port;
                        var measurement = new EmptyMeasurement(measurementInfo.Id, region);
                        var test = new SingleTest(measurement);

                        // resolve channel in background.
                        var channel = await Task.Run(() => Channel);

                        ActionInfo = "Single test - running...";
                        InfoTexts.Add("Running...");

                        Action timeoutAction = null;
                        if (!showIntermediateResults)
                            timeoutAction = () =>
                             {
                                 if (CurrentRequest == null)
                                     channel.Stop();
                             };
                        ThreadPool.QueueUserWorkItem(state =>
                        {
                            // request actions/keys after test started with some delay.
                            Thread.Sleep(1500);
                            channel.RequestAvailableActions();
                            channel.RequestAvailableKeys();
                        });

                        var result = await channel.Start(test, timeoutAction, sendIntermediateResuls: showIntermediateResults, touchTestEnabled: touchPreTest);

                        InfoTexts.Add($"Returned: {result.Measurement.Parameters.Count()} parameters, {result.Measurement.Results.Count()} limits, {result.Measurement.Results.Count()} results.");
                        InfoTexts.Add($"Finished (status = {result.Measurement.Status}).");
                    }
                    catch (Exception ex)
                    {
                        application.Log(ex);
                        InfoTexts.Add(ex.Message);
                    }
                    finally
                    {
                        Reset();
                    }
                }
            }

            public async Task StartAutoSequence(object parameter)
            {
                if (selectedRegion == null)
                {
                    InfoTexts.Add("Region not selected.");
                    return;
                }

                var region = new Region((uint)(selectedRegion.MasterRegionInfoId ?? 0), (uint)selectedRegion.Id);
                if (parameter is string port)
                {
                    try
                    {
                        InfoTexts.Clear();
                        InfoTexts.Add("Starting...");

                        BlackBoxProgress.IsBusy = true;
                        ActionInfo = "Starting AutoSequence...";

                        Port = port;

                        // resolve channel in background.
                        var channel = await Task.Run(() => Channel);

                        ActionInfo = "AutoSequence - running...";
                        InfoTexts.Add("Running...");

                        ThreadPool.QueueUserWorkItem(state =>
                        {
                            // request actions/keys after test started with some delay.
                            Thread.Sleep(1500);
                            channel.RequestAvailableActions();
                            channel.RequestAvailableKeys();
                        });

                        var status = await channel.StartAutoSequence(AutoSequenceName, region, false, showIntermediateResults);
                        InfoTexts.Add($"Finished (status = {status}).");
                    }
                    catch (Exception ex)
                    {
                        application.Log(ex);
                        InfoTexts.Add(ex.Message);
                    }
                    finally
                    {
                        Reset();
                    }
                }
            }

            public async Task Stop()
            {
                if (lazyChannel.IsValueCreated)
                {
                    await Task.Run(() => Channel?.Stop());
                }
            }

            public void RequestKeys()
                => Channel.RequestAvailableKeys();

            public void SendKey(string key)
            {
                if (string.IsNullOrEmpty(key)) return;
                Channel.SendKey(key);

                RequestActions();
                RequestKeys();
            }

            public void RequestActions()
                => Channel.RequestAvailableActions();

            public void ExecuteAction(string action)
            {
                if (string.IsNullOrEmpty(action)) return;
                Channel.ExecuteAction(action);

                RefreshActionsAndKeys();

            }

            public void SendResponse(object parameter)
            {
                var request = CurrentRequest;
                if (request == null) return;
                var response = request.CreateResponse(parameter);
                Ici.Channel.BlackBox.Publish(response);
                CurrentRequest = null;

                RefreshActionsAndKeys();
            }

            private void RefreshActionsAndKeys()
            {
                ThreadPool.QueueUserWorkItem(state =>
                {
                    Thread.Sleep(1500);
                    RequestActions();
                    RequestKeys();
                });
            }

            private void ResetChannel()
                => lazyChannel = new Lazy<IBlackBoxChannelTaskSource>(() => Ici.Channel.BlackBox.Create(
                    Ici.Channel.Serial.Open(selectedPort), blackBoxPassword: blackBoxPassword));

            private void Reset()
            {
                CurrentRequest = null;
                BlackBoxProgress.IsBusy = false;
                ActionInfo = StatusIdle;
            }

            private List<InstrumentVariantInfo> GetInstrumentsByProfile(IInstrumentInfo instrument)
            {
                if (instrument == null)
                    return new List<InstrumentVariantInfo>();

                var profile = instrument.GetAllProperties().First(_ => _.Key == "ProfileCode").Value;
                return new List<InstrumentVariantInfo>(from item in DataDisplayInfos.InstrumentVariantInfoList
                                                       where item.Code == profile
                                                       select item);
            }

            private void LoadRegions(IInstrumentInfo instrument)
            {
                RegionsList.Clear();
                List<InstrumentVariantInfo> instrumentsByProfile = GetInstrumentsByProfile(instrument);
                if (instrumentsByProfile.Any())
                {
                    var instrumentInfo = instrumentsByProfile.First();
                    foreach (var region in DataDisplayInfos.RegionInfoList.Where(
                        item => instrumentInfo.SimpleRegionInfoList.Exists(_ => _.Id == item.MasterRegionInfoId && _.GetChildRegions().Any(id => id == item.Id))))
                    {
                        RegionsList.Add(region);
                    }
                }

                SelectedRegion = RegionsList.FirstOrDefault();
            }

            private void LoadMeasurements(RegionInfo region, IInstrumentInfo instrument)
            {
                if (region == null) return;
                List<InstrumentVariantInfo> instrumentsByProfile = GetInstrumentsByProfile(instrument);

                this.MeasurementsList = new List<MeasurementInfo>(from item in DataDisplayInfos.MeasurementInfoList
                                                                  where item.SimpleRegionInfoList.Exists(_ => _.Id == region.MasterRegionInfoId && _.GetChildRegions().Any(id => id == region.Id))
                                                                        && (instrumentsByProfile == null || item.InstrumentSupportInfoList.Any(i => instrumentsByProfile.Any(info => info.Id == i.InstrumentVariant.Id)))
                                                                  orderby item.GetText(Configuration.DefaultLanguage)
                                                                  select item);

                if (selectedMeasurementInfo == null)
                    this.SelectedMeasurementInfo = this.MeasurementsList.FirstOrDefault(_ => _.Id == polarityMeasurementId); // select Polarity by default.
                else
                    this.SelectedMeasurementInfo = this.MeasurementsList.FirstOrDefault(_ => _.Id == selectedMeasurementInfo.Id);
            }

            #region -=IDisposable members=-

            private bool isDisposed = false;
            private readonly object disposeSync = new object();
            public void Dispose()
            {
                lock (disposeSync)
                {
                    if (!isDisposed)
                    {

                        foreach (var subscription in subscriptions)
                            subscription.Dispose();
                        subscriptions.Clear();
                        isDisposed = true;
                    }
                }
            }
            #endregion

            #region -=BlackBox message handlers=-

            private void OnBlackBoxRequest(Request request)
                => CurrentRequest = request;

            private void OnBlackBoxError(Error errorMessage)
            {
                Trace.TraceError($"BlackBox Error:\n{errorMessage.ErrorNumber}; {errorMessage.Description}");
                InfoTexts.Add($"ERROR: {errorMessage.ErrorNumber}; {errorMessage.Description}");
            }

            #endregion


        }

        #endregion

    }

    public class BlackBoxKeysTemplateSelector : DataTemplateSelector
    {
        private static Dictionary<string, string> mappings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
        {
            {"enter", "Button.Template.Key.Enter"},
            {"esc", "Button.Template.Key.Esc"},
            {"option", "Button.Template.Key.Option"},
            {"up", "Button.Template.Key.Up"},
            {"down", "Button.Template.Key.Down"},
            {"left", "Button.Template.Key.Left"},
            {"right", "Button.Template.Key.Right"}
        };
        public override DataTemplate SelectTemplate(object item, DependencyObject container)
        {
            if (container is FrameworkElement element && item is string key)
            {
                if (mappings.ContainsKey(key))
                {
                    var resource = element.TryFindResource($"{mappings[key]}") as DataTemplate;
                    return resource;
                }
            }

            return null;
        }
    }
}
