Spring – Rev 1

Subversion Repositories:
Rev:
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using System.Windows.Forms;
using Gma.System.MouseKeyHook;
using Spring.MouseKeyboard;
using Spring.Utilities;
using Spring.Utilities.Collections.RangeTree;
using SpringCombos;
using Debug = System.Diagnostics.Debug;

namespace Spring.Charging
{
    public class Charge : IDisposable
    {
#region Public Events & Delegates

        public event EventHandler<ChargeLoadedEventArgs> ChargeLoaded;

        public event EventHandler<ChargeChangedEventArgs> ChargeChanged;

        public event EventHandler<ChargeStartEventArgs> ChargeStart;

#endregion

#region Public Enums, Properties and Fields

        public Combos Combos { get; private set; }

        public int Fuzz { get; set; }

        public static Random Random { get; set; }

#endregion

#region Private Delegates, Events, Enums, Properties, Indexers and Fields

        private KeyWatch WatchKey { get; }

        private Key Key { get; }

        private IKeyboardMouseEvents KeyboardMouseEvents { get; }

        private Configuration.Configuration Configuration { get; }

        private BufferBlock<Combo> RecordKeyComboInputBlock { get; }

        private BufferBlock<Combo> RecordKeyComboOutputBlock { get; }

        private TransformBlock<Combo, Combo> RecordKeyTransformBlock { get; }

        private Stopwatch Stopwatch { get; }

        private int ChargeRepeat { get; set; }

        private RangeTreeFunc<TimeSpan, int> RangeTreeFunc { get; set; }

        private long _comboCount;

        private bool _disposing;

        private volatile bool _isCharging;

#endregion

#region Constructors, Destructors and Finalizers

        public Charge() => Stopwatch = new Stopwatch();

        public Charge(Combos springCombos,
                      Configuration.Configuration springConfiguration,
                      IKeyboardMouseEvents keyboardMouseEvents,
                      Key springKey,
                      KeyWatch watchKey) : this()
        {
            Combos = springCombos;
            Configuration = springConfiguration;
            KeyboardMouseEvents = keyboardMouseEvents;
            Key = springKey;
            WatchKey = watchKey;

            Random = new Random();

            RangeTreeFunc = new RangeTreeFunc<TimeSpan, int>
                            {
                                {
                                    TimeSpan.FromSeconds(0),
                                    TimeSpan.FromSeconds(Configuration.Spring.Charge.ChargeSeconds),
                                    FuncNoCharge
                                },
                                {
                                    TimeSpan.FromSeconds(Configuration.Spring.Charge.ChargeSeconds),
                                    TimeSpan.FromSeconds(Configuration.Spring.Charge.ChargeSeconds),
                                    FuncNoCharge
                                },
                                {
                                    TimeSpan.FromSeconds(Configuration.Spring.Charge.ChargeSeconds),
                                    TimeSpan.FromSeconds(Configuration.Spring.Charge.MaximumRepeats +
                                                         Configuration.Spring.Charge.ChargeSeconds),
                                    FuncCharge
                                },
                                {
                                    TimeSpan.FromSeconds(Configuration.Spring.Charge.MaximumRepeats +
                                                         Configuration.Spring.Charge.ChargeSeconds),
                                    TimeSpan.MaxValue, FuncNoCharge
                                }
                            };

            RecordKeyComboInputBlock = new BufferBlock<Combo>();

            RecordKeyTransformBlock =
                new TransformBlock<Combo, Combo>(TransformSpringCombos,
                    new ExecutionDataflowBlockOptions { EnsureOrdered = true });

            RecordKeyComboInputBlock.LinkTo(RecordKeyTransformBlock);

            RecordKeyComboOutputBlock = new BufferBlock<Combo>();
            RecordKeyTransformBlock.LinkTo(RecordKeyComboOutputBlock, combo => combo != null);
            RecordKeyTransformBlock.LinkTo(DataflowBlock.NullTarget<Combo>());

            Configuration.Spring.Charge.PropertyChanged += SpringLoading_PropertyChanged;
            Configuration.Spring.PropertyChanged += Spring_PropertyChanged;

            WatchKey.KeyUp += WatchKey_SpringKeyUp;
            WatchKey.KeyDown += WatchKey_KeyDown;
        }

        public void Dispose()
        {
            if (_disposing)
            {
                return;
            }

            _disposing = true;

            Configuration.Spring.Charge.PropertyChanged -= SpringLoading_PropertyChanged;
            Configuration.Spring.PropertyChanged -= Spring_PropertyChanged;

            WatchKey.KeyUp -= WatchKey_SpringKeyUp;
            WatchKey.KeyDown -= WatchKey_KeyDown;
        }

#endregion

#region Event Handlers

        private void Spring_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            Fuzz = ((Configuration.Spring) sender).Fuzz;
        }

        private void WatchKey_KeyDown(object sender, KeyDownEventArgs e)
        {
            if (_isCharging)
            {
                return;
            }

            ChargeRepeat = RangeTreeFunc.Query(e.Elapsed)
                                        .SingleOrDefault();

            $"Loaded repeat: {ChargeRepeat}".ToDebugConsole();
        }

        private void SpringLoading_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            Configuration.Spring.Charge = (Configuration.Charge) sender;

            RangeTreeFunc = new RangeTreeFunc<TimeSpan, int>
                            {
                                {
                                    TimeSpan.FromSeconds(0),
                                    TimeSpan.FromSeconds(Configuration.Spring.Charge.ChargeSeconds),
                                    FuncNoCharge
                                },
                                {
                                    TimeSpan.FromSeconds(Configuration.Spring.Charge.ChargeSeconds),
                                    TimeSpan.FromSeconds(Configuration.Spring.Charge.ChargeSeconds),
                                    FuncNoCharge
                                },
                                {
                                    TimeSpan.FromSeconds(Configuration.Spring.Charge.ChargeSeconds),
                                    TimeSpan.FromSeconds(Configuration.Spring.Charge.MaximumRepeats +
                                                         Configuration.Spring.Charge.ChargeSeconds),
                                    FuncCharge
                                },
                                {
                                    TimeSpan.FromSeconds(Configuration.Spring.Charge.MaximumRepeats +
                                                         Configuration.Spring.Charge.ChargeSeconds),
                                    TimeSpan.MaxValue, FuncNoCharge
                                }
                            };
        }

        private void WatchKey_SpringKeyUp(object sender, KeyUpEventArgs e)
        {
            if (!Key.IsConfigured)
            {
                return;
            }

            StopCharging();

            // Do not record if press time is smaller than two seconds.
            if (e.TimePressed < TimeSpan.FromSeconds(Configuration.Spring.Charge.ChargeSeconds))
            {
                return;
            }

            StartCharging();
        }

        private async void IKeyboardMouseEvents_MouseMove(object sender, MouseEventArgs e)
        {
            if (!Configuration.Spring.Charge.MouseMove)
            {
                return;
            }

            await StartCharge(new MouseCombo(e, ComboAction.Move));
        }

        private async void IKeyboardMouseEvents_MouseUp(object sender, MouseEventArgs e)
        {
            if (!Configuration.Spring.Charge.MouseClick)
            {
                return;
            }

            await StartCharge(new MouseCombo(e, ComboAction.Up));
        }

        private async void IKeyboardMouseEvents_MouseDown(object sender, MouseEventArgs e)
        {
            if (!Configuration.Spring.Charge.MouseClick)
            {
                return;
            }

            await StartCharge(new MouseCombo(e, ComboAction.Down));
        }

        private async void IKeyboardMouseEvents_KeyDown(object sender, KeyEventArgs e)
        {
            if (!Configuration.Spring.Charge.Keyboard)
            {
                return;
            }

            await StartCharge(new KeyboardCombo(e, ComboAction.Down));
        }

        private async void IKeyboardMouseEvents_KeyUp(object sender, KeyEventArgs e)
        {
            if (!Configuration.Spring.Charge.Keyboard)
            {
                return;
            }

            await StartCharge(new KeyboardCombo(e, ComboAction.Up));
        }

#endregion

#region Public Methods

        public void StartCharge(Combos combos)
        {
            Combos = combos;

            ChargeChanged?.Invoke(this, new ChargeChangedEventArgs(Combos));
        }

        public void Load(Combos combos)
        {
            Combos = combos;

            ChargeLoaded?.Invoke(this, new ChargeLoadedEventArgs(Combos));
        }

#endregion

#region Private Methods

        private int FuncCharge(TimeSpan x)
        {
            var value = x.Seconds - Configuration.Spring.Charge.ChargeSeconds;

            return value == 0 ? 0 : value;
        }

        private static int FuncNoCharge(TimeSpan _) => 0;

        private async Task StartCharge<T>(T eventArgs) where T : Combo
        {
            var fuzz = Random.Next(0, Configuration.Spring.Fuzz);

            var pauseCombo = new PauseCombo(Stopwatch.Elapsed) { Fuzz = fuzz };

            await RecordKeyComboInputBlock.SendAsync(pauseCombo);

            Stopwatch.Restart();

            await RecordKeyComboInputBlock.SendAsync(eventArgs);
        }

        private void StopCharging()
        {
            Stopwatch.Stop();

            KeyboardMouseEvents.KeyUp -= IKeyboardMouseEvents_KeyUp;
            KeyboardMouseEvents.KeyDown -= IKeyboardMouseEvents_KeyDown;
            KeyboardMouseEvents.MouseDown -= IKeyboardMouseEvents_MouseDown;
            KeyboardMouseEvents.MouseUp -= IKeyboardMouseEvents_MouseUp;
            KeyboardMouseEvents.MouseMove -= IKeyboardMouseEvents_MouseMove;

            if (!RecordKeyComboOutputBlock.TryReceiveAll(out var springCombos))
            {
                return;
            }

            if (!_isCharging)
            {
                return;
            }

            var cleanCombos = springCombos.SequenceSubtract(Key.KeyCombo, CompareSpringKeys);

            Combos = new Combos(cleanCombos, ChargeRepeat);

            ChargeChanged?.Invoke(this, new ChargeChangedEventArgs(Combos));

            _isCharging = false;
        }

        private void StartCharging()
        {
            _isCharging = true;

            Stopwatch.Restart();

            KeyboardMouseEvents.KeyUp += IKeyboardMouseEvents_KeyUp;
            KeyboardMouseEvents.KeyDown += IKeyboardMouseEvents_KeyDown;
            KeyboardMouseEvents.MouseDown += IKeyboardMouseEvents_MouseDown;
            KeyboardMouseEvents.MouseUp += IKeyboardMouseEvents_MouseUp;
            KeyboardMouseEvents.MouseMove += IKeyboardMouseEvents_MouseMove;

            Interlocked.Exchange(ref _comboCount, 0);

            ChargeStart?.Invoke(this, new ChargeStartEventArgs());
        }

        private Combo TransformSpringCombos(Combo arg)
        {
            arg.Index = (int) Interlocked.Read(ref _comboCount);

            switch (arg)
            {
                case PauseCombo _:
                    Interlocked.Increment(ref _comboCount);

                    return arg;

                case KeyboardCombo _:
                    if (Configuration.Spring.Charge.Keyboard)
                    {
                        Interlocked.Increment(ref _comboCount);

                        return arg;
                    }

                    break;

                case MouseCombo mouseCombo:

                    switch (mouseCombo.ComboAction)
                    {
                        case ComboAction.Move:
                            if (Configuration.Spring.Charge.MouseMove)
                            {
                                Interlocked.Increment(ref _comboCount);

                                return arg;
                            }

                            break;
                        case ComboAction.Up:
                        case ComboAction.Down:
                            if (Configuration.Spring.Charge.MouseClick)
                            {
                                Interlocked.Increment(ref _comboCount);

                                return arg;
                            }

                            break;
                    }

                    break;

                default:
                    Debug.Assert(true, "Unknown combo found in combo record charge queue.");

                    break;
            }

            return null;
        }

        private static bool CompareSpringKeys(Combo combo, KeyAction keyAction)
        {
            if (!(combo is KeyboardCombo springKeyEventArgs) || !(keyAction is KeyActionKeys keyActionKeys))
            {
                return false;
            }

            return springKeyEventArgs.Keys == keyActionKeys.Keys;
        }

#endregion
    }
}