//+---------------------------------------------------------------------------+
//|                                                          PABacktester.mq4 |
//|                                                   Kilian19 @ ForexFactory |
//|                                                                           |
//| Copyright (c) 2015                                                        |
//|                                                                           |
//| Permission to use, copy, modify, and/or distribute this software for any  |
//| purpose with or without fee is hereby granted, provided that the above    |
//| copyright notice and this permission notice appear in all copies.         |
//|                                                                           |
//| THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES  |
//| WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF          |
//| MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR   |
//| ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES    |
//| WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN     |
//| ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF   |
//| OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.            |
//|                                                                           |
//+---------------------------------------------------------------------------+

/*
   Changelog: 
    v3: Fixed a bug that covered half the last candle. (line 298 +PeriodSeconds());
  
   I tried to work with rectangles before but those do not cover up the candles due to they either being drawn in the background with filling or in the foreground with just their border.
   This means we sadly have to work with labels due to which we have to convert back and forth between shift time and pixel values.    
*/

#property copyright "Kilian19 @ ForexFactory"
#property link      "kilian19f@gmail.com"
#property version   "1.00"
#property strict
#property indicator_chart_window

#include <Controls/Dialog.mqh>
#include <Controls/DatePicker.mqh>
#include <Controls/Button.mqh>
#include <Controls/CheckBox.mqh>
#include <Controls/Edit.mqh>
#include <Controls/ComboBox.mqh>
#include <Controls/Panel.mqh>
#include <ChartObjects/ChartObjectsTxtControls.mqh>
#include <ChartObjects/ChartObjectsLines.mqh>
#include <Arrays/List.mqh>


//--- input parameters 
input color hideAreaColor = clrNONE; 
input color trendlineColor = Red; //Border Color
bool autoDisableAutoScrolling = true; //Disable Autoscrolling. Initially and after using jump.
input int initialMSSpeed = 500; //Scroll Delay Speed

//Alignment

#define MARGIN_LEFT         (11);
#define MARGIN_TOP          (11);
#define SPACING             (10); 

   
#define BUTTON_WIDTH       (100)     // size by X coordinate
#define BUTTON_HEIGHT      (20)      // size by Y coordinate
#define DATE_HEIGHT        (20)      // size by Y coordinate
#define DATE_WIDTH         (150)     // size by X coordinate

#define PANEL_HEIGHT       (290)

#define DISTANCE_AUTOSCROLL (200)


color areaColor;
bool chaceScroll;

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
   //Recieve chart settings
   
   //we want to leave everything how we found it after deinitalisation.
   chaceScroll = ChartGetInteger(ChartID(),CHART_AUTOSCROLL);
   
   //Disable autoscroll
   if(chaceScroll && autoDisableAutoScrolling)
      ChartSetInteger(ChartID(),CHART_AUTOSCROLL,false);
   
   //It's distracting to see where the price currently is if you want to backtest.
   ChartSetInteger(ChartID(),CHART_SHOW_LAST_LINE,false); 
   
   if(hideAreaColor == clrNONE) 
      areaColor = (color)ChartGetInteger(ChartID(),CHART_COLOR_BACKGROUND);
   else
      areaColor = hideAreaColor;


   //Spawn the panel in the upper right corner of the chart
   int chartWidth = (int)ChartGetInteger(ChartID(),CHART_WIDTH_IN_PIXELS,0);
   int panelWidth = 180; //180
   int x = chartWidth - panelWidth;
   
   bool create = pan.Create(ChartID(),"PA Backtester",0,x,0,x+panelWidth,PANEL_HEIGHT);
 
   pan.Run();

   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//---
   
//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
      //we have to update hide area label every time the chart changes dimension or the we scroll. 
      if(id == CHARTEVENT_CHART_CHANGE)
         pan.calcAndSetXDistance();
   
      //Handle chart events in our class.
      pan.ChartEvent(id,lparam,dparam,sparam);
   
  }
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{

   Print(GetLastError());
   //Reverse settings
   ChartSetInteger(ChartID(),CHART_AUTOSCROLL,chaceScroll);
   ChartSetInteger(ChartID(),CHART_SHOW_LAST_LINE,true); 
   pan.Destroy(reason);       
}




class Panel : public CAppDialog
  {
private:
   //controls  
   CComboBox           mSymbol;
   CComboBox           mPeriod;
   CEdit               mDelayBeforeShift;
   CEdit               mTimeMin;
   CEdit               mTimeHour;
   CButton             mBtnJump;
   CButton             mBtnPlayStop;
   CButton             mBtnSkip;
   CDatePicker         mDate;      
   CCheckBox           mSimulateAutoScroll;
   
   CList                 hideAreaList;

   
   int curPosition;  //at what position is the hiding area currently loacted
   int shiftSpeed;   //how fast is the autoplay speed in ms
   bool autoScroll;  //autoscrolling activated
   
   //Recalculate the necessary position for the label.
   void calcAndSetXDistance(datetime d,bool recalc = true){
      int xArea,unused;
      if(recalc)
         curPosition = iBarShift(Symbol(),PERIOD_CURRENT,d); 
      
       //If our custom autoscroll is enabled we might need to move the chart first 
       if(autoScroll)
       {
         
         ChartTimePriceToXY(ChartID(),0,d,0,xArea,unused);
         
         int chartWidth = ChartGetInteger(ChartID(),CHART_WIDTH_IN_PIXELS);
         int distanceAwayFromEdge = chartWidth - xArea;
         
         //We need to move the chart
         if(distanceAwayFromEdge < DISTANCE_AUTOSCROLL)
         {
            //these amount of pixels are missing
            int difference  = DISTANCE_AUTOSCROLL - distanceAwayFromEdge;
            int barsOnOneScreen = ChartGetInteger(ChartID(),CHART_WIDTH_IN_BARS);           
            
            int shiftNeeded = MathCeil((barsOnOneScreen/(double)chartWidth)*difference);
            
            //If we are at the end of the chart
            if(shiftNeeded < 0)
            {
               autoScroll = false;
               mSimulateAutoScroll.Checked(false);
            }
            else
               ChartNavigate(ChartID(),CHART_CURRENT_POS,shiftNeeded);
         } 
      }            
      //Due to the new shift we need to recalculate the distance needed again.
      ChartTimePriceToXY(ChartID(),0,d,0,xArea,unused);  

      //Adjust the label x distance
      backgroundLabelControler(xArea);

 
   }
   
   //Set the hours and numbers edit box to the specified datetime
   void setControls(datetime d){
      MqlDateTime time;
      TimeToStruct(d,time);
      mTimeMin.Text(time.min);
      mTimeHour.Text(time.hour);
      mDate.Value(d);
   }
   
   //Navigate to the specified datetime on the chart
   void navigate(datetime to)
   {
      int barsShift = iBarShift(NULL,0,to);
      //Naviagate to the predefined place.
      int barsInWindow = (int)ChartGetInteger(ChartID(),CHART_WIDTH_IN_BARS);
      int finalShift = (barsShift-barsInWindow/2);
      ChartNavigate(ChartID(),CHART_END,-finalShift);
      calcAndSetXDistance(to,true);
   }
   
   
   
    /* Gets called after panel sucessfully initalised.
    * If we changed timeframe or symbol through this indicator we cand not perform the chart shift before deinitialization. */
   void postCreation(){  
      string vName = "Hide_"+Symbol()+"_"+Period();   
      if(GlobalVariableCheck(vName))
      {
         datetime destination = GlobalVariableGet(vName);
         calcAndSetXDistance(destination,true);
         //Delete the variable
         GlobalVariableDel(vName);    
         //We should set the hour and minute values to the last jump time
         setControls(destination);
      }
      
      Print("post creation end: " + TimeLocal());
   }
   
   //Check if there is a window without a label 
   void backgroundLabelControler(int xArea){
    
      int winTotal = WindowsTotal();

      for(int i = 0; i < winTotal; i++)
      {
         //we need a unique name for the obj and we take the first indicator name of this subwindow.
         //This will break if you have to indicators with the same name and parameters on the chart. !!!
            string indiName = "";
            
            if(i!= 0)
               indiName = ChartIndicatorName(ChartID(),i,0);
            
            string objectToSearchFor = "background"+indiName;
         //This might break as well if we swap out indicator windows after the creation of the backtester.   
            
            if(ObjectFind(ChartID(),objectToSearchFor) < 0)
            {
               //we need to create explicitly using the new creater to get a dynamic pointer instead of a automatic
               CChartObjectRectLabel *label = new CChartObjectRectLabel();
               
               label.Create(ChartID(),objectToSearchFor,i,-100,-100,10000,10000);
               label.Color(trendlineColor);
               label.BackColor(areaColor);
               label.BorderType(BORDER_FLAT);
               
               hideAreaList.Add(GetPointer(label));
            }
      }
            
      CChartObjectRectLabel *node = hideAreaList.GetFirstNode();
      
      //Calculate the width of a bar in pixel...
      int firstVisBar = ChartGetInteger(ChartID(),CHART_FIRST_VISIBLE_BAR);
      
      int x,x1,y,y1;            
      //First bar
      ChartTimePriceToXY(ChartID(),0,Time[firstVisBar],0,x,y);
      //Second bar
      ChartTimePriceToXY(ChartID(),0,Time[firstVisBar-1],0,x1,y1);      
      
      int widthBarInPixel = x1 - x;
      
      //If an indicator window was deleted we do not want to update the lable anymore
      do{
         if(CheckPointer(node)== POINTER_INVALID)
            hideAreaList.DeleteCurrent();
              
         node.SetInteger(OBJPROP_XDISTANCE,xArea+(widthBarInPixel/2));
      }
      while( (node = hideAreaList.GetNextNode()) != NULL );
      
   }

   
   
public:
   Panel(void){ shiftSpeed = initialMSSpeed; autoScroll = false;};
   ~Panel(void){};

   
   //Wrapper function  
   void calcAndSetXDistance(){calcAndSetXDistance(Time[curPosition],false);} 
   
   //call this if we want to move to the next bar                 
   void nextBar(){
      curPosition--;
      if(curPosition >= 0){
         calcAndSetXDistance();   
      }
      else //We reached the end of the chart
      {
         curPosition = 0;
         if(mBtnPlayStop.Pressed()){  
            mBtnPlayStop.Pressed(false);
            mBtnPlayStop.Text("Play");
         }  
      }
      //Write the updated time value to the hour minute and datepicker controls
      setControls(Time[curPosition]);
      
   }                 
   
   //Event handling
   virtual bool      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
   
   
   void  OnClickButtonJump(void){
      
      //autoscroll breaks during jump so we have to disable it in the meantime.
      autoScroll = false;
      
      //Get the current date value
      CDateTime dateTime;
      dateTime.Date(mDate.Value());
           
      //Parse the text box
      string minutes = StringTrimLeft(StringTrimRight(mTimeMin.Text()));
      string hours = StringTrimLeft(StringTrimRight(mTimeHour.Text()));
      dateTime.Min((int)StringToInteger(minutes));
      dateTime.Hour((int)StringToInteger(hours));

      int barsShift = iBarShift(NULL,0,dateTime.DateTime(),false); 
             
      //It makes no sense to navigate if outoscroll is enabled
      ChartSetInteger(ChartID(),CHART_AUTOSCROLL,!autoDisableAutoScrolling);
      
      //Desired symbol and time frame
      string symbol = mSymbol.Select();
      int tf = mPeriod.Value();
      
      //If symbol or tf changes the indicator will be deinitialized
      if(Symbol() != symbol || tf != Period())
      {
         //Prepare deinitialisation.
         string vName = "Hide_"+symbol+"_"+tf;
         GlobalVariableTemp(vName);
         GlobalVariableSet(vName,dateTime.DateTime()); //buffer value for after reinitialisation
         ChartSetSymbolPeriod(ChartID(),symbol,tf);
      } 
      
      //If we don't change tf or symbol we can simply navigate to the specified time.
      navigate(dateTime.DateTime()); 
      
      //Reenable autoscroll
      autoScroll = mSimulateAutoScroll.Checked();
      setControls(Time[curPosition]);
      
   };
   
   void  OnClickButtonSkip(void){
      nextBar();
   };
   
   
   void OnClickButtonPlayStop(){
      if(mBtnPlayStop.Pressed())
      {
         mBtnPlayStop.Text("Pause");
         EventSetMillisecondTimer(shiftSpeed);
      }
      else
      {
         mBtnPlayStop.Text("Play");
         EventKillTimer();
      }
   }
   
   
   void  OnChangeDelay(){
      int newMsSpeed = StringToInteger(StringTrimRight(StringTrimLeft(mDelayBeforeShift.Text()))); //only default functions for public release
      if(newMsSpeed > 0 && MathIsValidNumber(newMsSpeed))   //Only update the value if we got a valid number > 0
      {
         shiftSpeed = newMsSpeed;
         EventKillTimer();
         EventSetMillisecondTimer(shiftSpeed);
      }
   }
   
   void OnStateChangeAutoScroll(){
     autoScroll = mSimulateAutoScroll.Checked();
   }
   
   
   //Creation code
   virtual bool Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2)
   {
       //Create the "hide area" first so everything else will be drawn on top of it.
       
      //Grabbing the first bar in the chart can sometimes throw errors. This way we are save    
      curPosition = WindowFirstVisibleBar()-WindowBarsPerChart()/2;   
       
      datetime begin = Time[curPosition];
      
      /*
      hideArea.Create(ChartID(),"HideArea",0,-100,-100,10000,10000);
      hideArea.Color(clrRed);
      hideArea.BackColor(areaColor);
      hideArea.BorderType(BORDER_FLAT);
      */
      
      curPosition = 50;
      calcAndSetXDistance();
      
      //create parent
      if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2))
         return false;
   
      //Create controls
      
      //Sym picker
      int x = MARGIN_LEFT;
      int y = MARGIN_TOP;
      int toX = x + DATE_WIDTH/1.7;
      int toY = y +DATE_HEIGHT;
      
      if(!mSymbol.Create(chart,"SymbolPicker",subwin,x,y,toX,toY))
         return false;
         
      if(!Add(mSymbol))
         return false;  

      int numSymbols = SymbolsTotal(true);
      for(int i= 0; i < numSymbols; i++)
         mSymbol.AddItem(SymbolName(i,true),i);
       
       mSymbol.SelectByText(Symbol());
      
      //Period picker
      if(!mPeriod.Create(chart,"PeriodPicker",subwin,toX + 5,y,toX+ + DATE_WIDTH/2.3,toY))
         return false;
         
      //no library functions for public release
      mPeriod.AddItem("M1",1);
      mPeriod.AddItem("M5",5);
      mPeriod.AddItem("M15",15);
      mPeriod.AddItem("M30",30);
      mPeriod.AddItem("H1",60);
      mPeriod.AddItem("H4",240);
      mPeriod.AddItem("D1",1440);
      mPeriod.AddItem("W1",10080);      
      mPeriod.AddItem("MN",43200);

      mPeriod.SelectByValue(Period());

      if(!Add(mPeriod))
         return false;  
       
      //Date picker
      y  = toY + SPACING;
      toX = x + DATE_WIDTH;
      toY = y + DATE_HEIGHT;
      
      //Chace the values for date. We have to create the object last because the datepicker needs to be drawn on top of all other controls.
      //this took me hours of time to debug ...
      int xDate =  x;
      int yDate = y;
      int x1Date = toX;
      int y1Date = toY;

       //Time picker
      y  = toY + SPACING;
      toX = x + DATE_WIDTH/2;
      toY = y + DATE_HEIGHT;
      
      if(!mTimeHour.Create(chart,"TimeHour",subwin,x,y,toX,toY))
         return false;
         
       if(!Add(mTimeHour))
         return false;
                   
      mTimeHour.TextAlign(ALIGN_CENTER);
      mTimeHour.Text("HH");
      
      if(!mTimeMin.Create(chart,"TimeMin",subwin,toX + 5 ,y,toX + DATE_WIDTH/2 ,toY))
         return false;
         
       if(!Add(mTimeMin))
         return false;
                
      mTimeMin.TextAlign(ALIGN_CENTER);
      mTimeMin.Text("MM");
      
      y  = toY + SPACING;
      toX = x + DATE_WIDTH;
      toY = y + DATE_HEIGHT;
      
      if(!mDelayBeforeShift.Create(ChartID(),"DelayBeforeStep",subwin,x,y,toX,toY))
         return false;
        
       if(!Add(mDelayBeforeShift))
         return false;


      mDelayBeforeShift.TextAlign(ALIGN_CENTER);
      mDelayBeforeShift.Text(initialMSSpeed);
        
      //Jump Button 
      x = (int)(x*4);
      y  = toY+ SPACING; 
      toX = x + BUTTON_WIDTH;
      toY = y + BUTTON_HEIGHT;
      
      if(!mBtnJump.Create(chart,"BtnJump",subwin,x,y,toX,toY))
         return false;
       
       mBtnJump.Text("Jump");
       
      if(!Add(mBtnJump))
         return false; 
       

      //Skip Button
      y  = toY+ SPACING; 
      toY = y + BUTTON_HEIGHT;   
        
      if(!mBtnSkip.Create(chart,"BtnSkip",subwin,x,y,toX,toY))
         return false;
         
      mBtnSkip.Text("Skip");
      if(!Add(mBtnSkip))
         return false;
      
      //Autoscrolling checkbox
           
      y  = toY+ SPACING; 
      toY = y + BUTTON_HEIGHT;
       
      if(!mSimulateAutoScroll.Create(ChartID(),"CheckBoxAuto",subwin,(int)(x/4)+5,y,toX,toY))
         return false;
                    
      mSimulateAutoScroll.Text("Simulate scrolling");   
      
      mSimulateAutoScroll.Checked(true);
      
      if(!Add(mSimulateAutoScroll))
         return false;
      
      //Play Stop Button 
      y  = toY+ SPACING; 
      toY = y + BUTTON_HEIGHT;  
       
      if(!mBtnPlayStop.Create(chart,"BtnStartStop",subwin,x,y,toX,toY))
         return false;
      
      mBtnPlayStop.Locking(true);
       
      mBtnPlayStop.Text("Play");
      if(!Add(mBtnPlayStop))
         return false;

      autoScroll = true; 

      //Datepicker. Now implement it
      if(!mDate.Create(chart,"DatePicker",subwin,xDate,yDate,x1Date,y1Date))
         return false;
         
      if(!Add(mDate))
         return false;  
      
      mDate.Value(TimeCurrent());
      
      postCreation();
      
      return true;
   }  
};

//Mapping macro
   EVENT_MAP_BEGIN(Panel)

      ON_EVENT(ON_END_EDIT,mDelayBeforeShift,OnChangeDelay)
      ON_EVENT(ON_CLICK,mBtnJump,OnClickButtonJump)
      ON_EVENT(ON_CLICK,mBtnSkip,OnClickButtonSkip)
      ON_EVENT(ON_CLICK,mBtnPlayStop,OnClickButtonPlayStop)
      ON_EVENT(ON_CHANGE,mSimulateAutoScroll,OnStateChangeAutoScroll);  
   EVENT_MAP_END(CAppDialog)

Panel pan;
   
void OnTimer()
{
   pan.nextBar();
}


/*
class IndicatorWindowLabel{

public: 

   int indicatorWindow;
   string indicatorShortName;
   CChartObjectRectLabel hideArea;
   
   IndicatorWindowLabel(){
   
   }
   
   
}
*/