context: // Copyright (c) The University of Dundee 2018-2019
  // This file is part of the Research Data Management Platform (RDMP).
  // RDMP 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.
  // RDMP 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 RDMP. If not, see <https://www.gnu.org/licenses/>.
  
  using System;
  using System.Collections.Generic;
  using System.Drawing;
  using System.Drawing.Drawing2D;
  using System.Linq;
  using System.Windows.Forms;
  using Rdmp.Core.CommandExecution;
  using Rdmp.Core.Curation.Data;
  using Rdmp.Core.DataQualityEngine.Data;
  using Rdmp.Core.Icons.IconProvision;
  using Rdmp.UI.DashboardTabs.Construction;
  using Rdmp.UI.ItemActivation;
  using Rdmp.UI.SimpleControls;
  
  namespace Rdmp.UI.Raceway
  {
      /// <summary>
      /// Part of DatasetRaceway, this control shows a miniature bar chart with 1 bar per month of data held within the dataset.  Bar colour indicates the proportion of records in that month
      /// which are passing/failing validation (green = passing, red = failing).  The month axis is shared across all datasets in the DatasetRaceway meaning that you see a continuous axis
      /// that spans the whole length of time you have been holding data for in any of your datasets.  The data for this graph comes from the Data Quality Engine evaluations database so if
      /// you have never run the DQE on a given dataset it will not appear in the DatasetRaceway.
      /// 
      /// <para>In DatasetRaceway you can adjust the scope of the axis down from 'All Time' to 'Last Decade',  'Last Year' or 'Last 6 months' if you just want to see how up-to-date each dataset
      /// is in finer detail.</para>
      /// 
      /// <para>The overall effect of this control is to allow you to rapidly identify any datasets that you host which have suddenly started failing validation, see where each dataset starts and
      /// if there are any gaps or periods of duplication (e.g. where the bars double in height for a period).</para>
      /// </summary>
      [TechnicalUI]
      public partial class RacewayRenderAreaUI : UserControl,INotifyMeOfEditState
      {
          private DateTime[] _buckets;
          private Pen _verticalLinesPen = new Pen(Color.FromArgb(150, Color.White));
  
          Timer _mouseHeldDownTimer = new Timer();
          ScrollActionUnderway _currentScrollAction = ScrollActionUnderway.None;
  
          public RacewayRenderAreaUI()
          {
              InitializeComponent();
  
              var colorPicker = new RainbowColorPicker(Color.Red, Color.LawnGreen,101);
              _brushes = colorPicker.Colors.Select(c=> new SolidBrush(c)).ToArray();
              DoubleBuffered = true;
  
              _mouseHeldDownTimer.Interval = 100;
              _mouseHeldDownTimer.Tick += _mouseHeldDownTimer_Tick;
              _mouseHeldDownTimer.Start();
              
          }
  
          public event Action<Catalogue> RequestDeletion;
  
          public void NotifyEditModeChange(bool isEditmodeOn)
          {
              _isEditModeOn = isEditmodeOn;
              Invalidate();
          }
  
          private object oPeriodicityDictionaryLock = new object();
          
          private bool _ignoreRowCounts;
          private bool _isEditModeOn;
          private const float MaximumRaceLaneRenderSpace = 30f;
          private SolidBrush[] _brushes;
          
          private Dictionary<Rectangle,Catalogue> rectNoDQE = new Dictionary<Rectangle,Catalogue>();
          private Dictionary<Rectangle, Catalogue> rectDeleteButtons = new Dictionary<Rectangle, Catalogue>(); 
          private IActivateItems _activator;
  
          private bool _allowScrollDown = false;
          private RectangleF _rectScrollDown;
          private int _scrollDownIndexOffset = 0;
          
          private bool _allowScrollUp;
          private RectangleF _rectScrollUp;
          private DateTime _currentScrollActionBegan;
          private Dictionary<Catalogue, Dictionary<DateTime, ArchivalPeriodicityCount>> _periodicityDictionary;
  
          const float MinimumRowHeight = 20;
  
          public void AddTracks(IActivateItems activator, Dictionary<Catalogue, Dictionary<DateTime, ArchivalPeriodicityCount>> periodicityDictionary, DateTime[] buckets, bool ignoreRows)
          {
              _activator = activator;
              _buckets = buckets;
              _ignoreRowCounts = ignoreRows;
              lock (oPeriodicityDictionaryLock)
              {
                  _periodicityDictionary = periodicityDictionary;
              }
          }
  
          private long frameLimiter = 0;
          protected override void OnMouseMove(MouseEventArgs e)
          {
              base.OnMouseMove(e);
  
  
              if (rectNoDQE.Any(r => r.Key.Contains(e.Location)) || rectDeleteButtons.Any(r => r.Key.Contains(e.Location)))
                  Cursor = Cursors.Hand;
              else
                  Cursor = Cursors.Arrow;
  
              //don't spam invalidate every time the mouse moves a pixel
  
              //Ticks is the number of 100-nanosecond intervals that have elapsed
              //Therefore this is 10 FPS
              if (DateTime.Now.Ticks - frameLimiter < 1000000)
                  return;
  
              frameLimiter = DateTime.Now.Ticks;
  
              Invalidate();
          }
          
  
  
          protected override void OnMouseClick(MouseEventArgs e)
          {
              base.OnMouseClick(e);
  
              foreach (KeyValuePair<Rectangle, Catalogue> kvp in rectNoDQE.Where(r => r.Key.Contains(e.Location)))
                  _activator.RequestItemEmphasis(this, new EmphasiseRequest(kvp.Value));
  
              foreach (KeyValuePair<Rectangle, Catalogue> kvp in rectDeleteButtons.Where(r => r.Key.Contains(e.Location)).ToList())
              {
                  if (RequestDeletion != null)
                  {
                      lock (oPeriodicityDictionaryLock)
                      {
                          RequestDeletion(kvp.Value);
                          //stop rendering this Catalogue
                          _periodicityDictionary.Remove(kvp.Value);
                          Invalidate();
                          
                      }
                  }
              }
          }
  
  
  
          protected override void OnMouseDown(MouseEventArgs e)
          {
              base.OnMouseDown(e);
  
  
              //scroll them down
              if (_rectScrollDown != RectangleF.Empty && _rectScrollDown.Contains(e.Location) && _allowScrollDown)
              {
                  _currentScrollActionBegan = DateTime.Now;
                  _currentScrollAction = ScrollActionUnderway.ScrollingDown;
                  ScrollDown();
              }
  
              //scroll them down 
              if (_rectScrollUp != RectangleF.Empty && _rectScrollUp.Contains(e.Location) && _allowScrollUp)
              {
                  _currentScrollActionBegan = DateTime.Now;
                  _currentScrollAction = ScrollActionUnderway.ScrollingUp;
                  ScrollUp();
              }
  
              if (_allowScrollDown || _allowScrollUp)
              {
                  var scrollBarMiddle = new RectangleF(_rectScrollUp.Left, _rectScrollUp.Bottom, 20, _rectScrollDown.Top - _rectScrollUp.Bottom);
                  if (scrollBarMiddle.Contains(e.Location))
                  {
                      _currentScrollActionBegan = DateTime.Now;
                      _currentScrollAction = ScrollActionUnderway.ScrollDragging;
                      ScrollToMouseLocation();
  
                  }
              }
          }
  
          protected override void OnMouseUp(MouseEventArgs e)
          {
              base.OnMouseUp(e);
              _currentScrollAction = ScrollActionUnderway.None;
          }
  
          void _mouseHeldDownTimer_Tick(object sender, EventArgs e)
          {
              if(IsDisposed)
              {
                  _mouseHeldDownTimer.Stop();
                  _mouseHeldDownTimer.Dispose();
                  return;
              }
  
              if (_currentScrollAction == ScrollActionUnderway.None || DateTime.Now.Subtract(_currentScrollActionBegan).TotalMilliseconds < 200)
                  return;
  
              if (_currentScrollAction == ScrollActionUnderway.ScrollingUp)
                  ScrollUp();
  
              if (_currentScrollAction == ScrollActionUnderway.ScrollingDown)
                  ScrollDown();
  
              if (_currentScrollAction == ScrollActionUnderway.ScrollDragging)
                  ScrollToMouseLocation();
  
              Invalidate();
  
          }
  
          protected override void OnMouseWheel(MouseEventArgs e)
          {
              base.OnMouseWheel(e);
  
              if(e.Delta < 0)
                  ScrollDown();
              else
                  ScrollUp();
  
              Invalidate();
          }
  
          private void ScrollUp()
          {
              _scrollDownIndexOffset = Math.Max(0, _scrollDownIndexOffset - 1);
          }
  
          private void ScrollDown()
          {
              _scrollDownIndexOffset = Math.Min(GetMaxScrollDownToIndex()+1, _scrollDownIndexOffset + 1);
          }
  
          private void ScrollToMouseLocation()
          {
              var mouseloc = PointToClient(Cursor.Position);
              var maxScroll = GetMaxScrollDownToIndex();
  
              var clickLocationY = mouseloc.Y - _rectScrollUp.Bottom;
              var fullDistanceBetweenUpAndDownArrows = _rectScrollDown.Top - _rectScrollUp.Bottom;
              var ratio = clickLocationY / fullDistanceBetweenUpAndDownArrows;
  
              _scrollDownIndexOffset = (int)(maxScroll * ratio);
  
              _scrollDownIndexOffset = Math.Max(0, _scrollDownIndexOffset);
              _scrollDownIndexOffset = Math.Min(maxScroll, _scrollDownIndexOffset);
          }
  
          private int GetMaxScrollDownToIndex()
          {
              var indexesVisibleOnScreen = (Height / MinimumRowHeight);
              var indexes = _periodicityDictionary.Count + 1;
              
              return (int) Math.Max(0,indexes - indexesVisibleOnScreen);
          }
  
          protected override void OnPaint(PaintEventArgs e)
          {
              base.OnPaint(e);
  
              e.Graphics.FillRectangle(Brushes.Black,0,0,Width,Height);
  
              //draw buckets
              if (_buckets == null)
                  return;
  
              float eachBucketHasThisManyPixelsOfX = ((float)Width) / _buckets.Length;
  
              float heightReservedForAxis = e.Graphics.MeasureString("TEST",Font).Height + 2;
  
              var mousePosition = PointToClient(new Point(MousePosition.X, MousePosition.Y));
              
              rectNoDQE.Clear();
              rectDeleteButtons.Clear();
  
              string hoverLabel = null;
              string hoverValue = null;
  
              _allowScrollUp = _scrollDownIndexOffset > 0;
  
              
              
              //draw the tracks
              lock (oPeriodicityDictionaryLock)
              {
                  
                  float eachRaceLaneHasThisMuchYSpace = Math.Max(MinimumRowHeight,Math.Min(MaximumRaceLaneRenderSpace, (float)Height/(_periodicityDictionary.Count +1)));
  
  
                  float startDrawingLaneAtY = 0;
                  _allowScrollDown = false;
  
                  int index = 0;
                  foreach (var kvp in _periodicityDictionary.OrderBy(kvp => kvp.Value == null ? "zzzzzz" + kvp.Key.Name : kvp.Key.Name))
                  {
                      index++;
  
                      //skip it because of scrolling
                      if(index<=_scrollDownIndexOffset)
                          continue;
  
                      //we have run out of space stop drawing
                      if (startDrawingLaneAtY + eachRaceLaneHasThisMuchYSpace > (Height - heightReservedForAxis))
                      {
                          _allowScrollDown = true;
                          break;
                      }
  
                      var catalogue = kvp.Key;
                      var dictionary = kvp.Value;
  
                      var middleLineOfCatalogueLabelY = ((eachRaceLaneHasThisMuchYSpace / 2) - (Font.Height / 2.0)) + startDrawingLaneAtY;
  
                      if (!_buckets.Any())
                      {
                          DrawErrorText("No DQE data exists for 'Show Period'",false, e, startDrawingLaneAtY, eachRaceLaneHasThisMuchYSpace, middleLineOfCatalogueLabelY);
                      }
                      else
                          if (dictionary == null)
                      {
                          var textWidth = DrawErrorText("No DQE Evaluation for " + catalogue,true,e, startDrawingLaneAtY, eachRaceLaneHasThisMuchYSpace, middleLineOfCatalogueLabelY);
                          rectNoDQE.Add(new Rectangle(0, (int)startDrawingLaneAtY, (int)textWidth, (int)eachRaceLaneHasThisMuchYSpace),kvp.Key);
                      }
                      else
                         if (!dictionary.Any())
                      {
                          var textWidth = DrawErrorText("Table(s) were empty for " + catalogue, true, e, startDrawingLaneAtY, eachRaceLaneHasThisMuchYSpace, middleLineOfCatalogueLabelY);
                          rectNoDQE.Add(new Rectangle(0, (int)startDrawingLaneAtY, (int)textWidth, (int)eachRaceLaneHasThisMuchYSpace), kvp.Key);
                      }
                      else
                      {
                          //get the maximum number of rows regardless of consequence found in any data month
                          int maxRowsInAnyMonth = dictionary.Max(r=>r.Value.Total);
                              
                          for (int i = 0; i < _buckets.Length; i++)
                          {
                              
                              Brush brush;
                              float lineHeightPercentage;
  
                              int good = 0;
                              int total = 0;
  
                              if (dictionary.ContainsKey(_buckets[i]))
                              {   
                                  good = dictionary[_buckets[i]].CountGood;
                                  total = dictionary[_buckets[i]].Total;
  
                                  float ratioGood = (float)good / total;
  
                                  brush = _brushes[(int) (ratioGood*100)];
  
                                  lineHeightPercentage = ((float)(total)) / maxRowsInAnyMonth;
                              }
                              else
                              {
                                  brush = new SolidBrush(Color.Black);
                                  lineHeightPercentage = 0;
                              }
  
                              //now draw the actual bar
                              //bar will occupy this much of the allowed Y space for the line e.g. 0.5 will be half way up the graph
                              var thicknessRatio = _ignoreRowCounts ? 1 : lineHeightPercentage;
  
                              var heightOfBarToDraw = eachRaceLaneHasThisMuchYSpace * thicknessRatio;
                              var emptyYSpace = eachRaceLaneHasThisMuchYSpace - heightOfBarToDraw;
  
                              var widthOfBarToDraw = eachBucketHasThisManyPixelsOfX;
  
                              var rectToFill = new RectangleF(i*widthOfBarToDraw,startDrawingLaneAtY + emptyYSpace,widthOfBarToDraw, heightOfBarToDraw);
                              e.Graphics.FillRectangle(brush, rectToFill);
  
                              var fullRectHitbox = new RectangleF(rectToFill.X, startDrawingLaneAtY, widthOfBarToDraw, eachRaceLaneHasThisMuchYSpace);
                              if (fullRectHitbox.Contains(mousePosition))
                              {
                                  hoverLabel = _buckets[i].ToString("Y");
                                  hoverValue = 
                                      string.Format("{0:n0}",good) + "/" +
                                      string.Format("{0:n0}",total);
                              }
                          }
  
                          e.Graphics.DrawString(catalogue.Name, Font, Brushes.White, new Point(0, (int)middleLineOfCatalogueLabelY));
                      }
  
                      if (hoverLabel != null)
                      {
                          float labelPadding = 3;
                          var labelSize = e.Graphics.MeasureString(hoverLabel, Font);
                          var valueSize = e.Graphics.MeasureString(hoverValue, Font);
  
                          //if there's a scroll down bar add 20 to the width to make space for the down arrow
                          if (_allowScrollDown)
                              valueSize.Width += 20;
  
                          var rectHoverLabel = new
                              RectangleF(Width - (labelSize.Width + valueSize.Width + (2*labelPadding)),
                                  Height - (heightReservedForAxis + labelSize.Height),
                                  labelSize.Width,
                                  labelSize.Height);
  
                          var rectHoverValue = new
                              RectangleF(rectHoverLabel.Right + labelPadding,
                                  rectHoverLabel.Top,
                                  valueSize.Width,
                                  valueSize.Height);
                          
                          e.Graphics.DrawString(hoverLabel, Font, Brushes.White, rectHoverLabel);
                          e.Graphics.DrawString(hoverValue, Font, Brushes.White, rectHoverValue);
                      }
  
                      if (_isEditModeOn)
                      {
                          var deleteIcon = FamFamFamIcons.delete;
                          var middleLineOfDeleteButtonY = ((eachRaceLaneHasThisMuchYSpace / 2) - (deleteIcon.Height / 2.0)) + startDrawingLaneAtY;
                          var buttonPoint = new Point((Width / 2), (int)middleLineOfDeleteButtonY);
  
                          e.Graphics.DrawImage(deleteIcon, buttonPoint);
  
                          rectDeleteButtons.Add(new Rectangle(buttonPoint.X, buttonPoint.Y, deleteIcon.Width, deleteIcon.Height), kvp.Key);
                      }
  
                      //move to next lane on graph
                      startDrawingLaneAtY += eachRaceLaneHasThisMuchYSpace;
                  }
              }
  
              float currentX = 0;
              float startDrawingAxisAtY = Height - heightReservedForAxis;
  
  
gt:             for (int i = 0; i < _buckets.Length; i++)