#include <TypeToBytes.mqh> // https://www.mql5.com/ru/code/16280

#include "SingleTestCacheHeader.mqh"
#include "ExpTradeSummaryExt.mqh"
#include "TradeDeal.mqh"
#include "TradeOrder.mqh"
#include "TesterPositionProfit.mqh"
#include "TesterTradeState.mqh"

#define __SINGLETESTERCACHE__

class SINGLETESTERCACHE
{
private:
  template <typename T>
  static void ToNull( T &Value )
  {
    uchar Bytes[sizeof(T)];
    ::ArrayInitialize(Bytes, 0);

    _W(Value) = Bytes;

    return;
  }

  bool SaveSettings( string FileName, const string StrMain, const bool Details = true,
                     string AddInformationBefore = NULL, const int Common_Flag = 0, string AddInformationAfter = NULL ) const
  {
    const int handle = ::FileOpen(FileName, FILE_WRITE | FILE_UNICODE | FILE_TXT | Common_Flag);
    const bool Res= (handle != INVALID_HANDLE);

    if (Res)
    {
      if (AddInformationBefore != NULL)
      {
        AddInformationBefore += "\n";

        ::StringReplace(AddInformationBefore, "\n", "\n; ");

        ::FileWriteString(handle, "; " + AddInformationBefore + (Details ? NULL : "\n"));
      }

      if (Details)
        ::FileWriteString(handle, ((AddInformationBefore == NULL) ? NULL : "\n") + "; saved on " + (string)::TimeLocal() + "\n; " + this.ToString() + "\n;\n");

      if (::StringLen(StrMain))
      {
        string StrArray[];

        const int Size = ::StringSplit(StrMain, '\n', StrArray);

        for (int i = 0; i < Size; i++)
          ::FileWriteString(handle, ::StringSubstr(StrArray[i], 0, ::StringLen(StrArray[i]) - (i != Size - 1)) +
                                    (::StringFind(StrArray[i], "||") > 0 ? "||Y" : NULL) + "\n");
      }

      if (Details)
      {
        string Str = ";\n" + this.Summary.ToString() + "\n" + this.Header.ToString();

        ::StringReplace(Str, "\n", "\n; ");

        ::FileWriteString(handle, Str);
      }

      if (AddInformationAfter != NULL)
      {
        AddInformationAfter = "\n" + AddInformationAfter;

        ::StringReplace(AddInformationAfter, "\n", "\n; ");

        ::FileWriteString(handle, AddInformationAfter);
      }

      ::FileClose(handle);
    }

    return(Res);
  }

public:
  SingleTestCacheHeader Header;
  ExpTradeSummaryExt Summary;
  string Inputs;
  TradeDeal Deals[];
  TradeOrder Orders[];
  TesterPositionProfit Positions[];
  TesterTradeState TradeState[];

  bool Load( const string FileName, const int Common_Flag = 0 )
  {
    const int handle = ::FileOpen(FileName, FILE_READ | FILE_BIN | Common_Flag);
    bool Res = (handle != INVALID_HANDLE);

    if (Res)
    {
      ::ArrayFree(this.Deals);
      ::ArrayFree(this.Orders);
      ::ArrayFree(this.Positions);
      ::ArrayFree(this.TradeState);

      this.Inputs = NULL;

      if (Res = ::FileReadStruct(handle, this.Header))
      {
        if (this.Header.parameters_size)
        {
          short Words[];
          ::FileReadArray(handle, Words, 0, this.Header.parameters_size);

          this.Inputs = ::ShortArrayToString(Words);
        }

        ::FileReadStruct(handle, this.Summary);

        ::FileReadArray(handle, this.Deals, 0, ::FileReadInteger(handle));
        ::FileReadArray(handle, this.Orders, 0, ::FileReadInteger(handle));

        if (this.Header.positions_total)
          ::FileReadArray(handle, this.Positions, 0, ::FileReadInteger(handle));

        ::FileReadArray(handle, this.TradeState, 0, ::FileReadInteger(handle));
      }

      ::FileClose(handle);
    }

    return(Res);
  }

  template <typename T>
  bool Load( const T &Array[] )
  {
    bool Res = ::ArraySize(Array);

    if (Res)
    {
      ::ArrayFree(this.Deals);
      ::ArrayFree(this.Orders);
      ::ArrayFree(this.Positions);
      ::ArrayFree(this.TradeState);

      this.Inputs = NULL;

      _W(this.Header) = Array;

      int Pos = sizeof(this.Header);

      if (this.Header.parameters_size)
      {
        short Words[];

        Pos += ::_ArrayCopy(Words, Array, 0, Pos, this.Header.parameters_size * sizeof(short));
        this.Inputs = ::ShortArrayToString(Words);
      }

      uchar Bytes[];

      Pos += ::_ArrayCopy(Bytes, Array, 0, Pos, sizeof(ExpTradeSummaryExt));
      _W(this.Summary) = Bytes;

      Pos += ::_ArrayCopy(this.Deals, Array, 0, Pos + sizeof(int), _R(Array)[Pos] * sizeof(TradeDeal)) + sizeof(int);
      Pos += ::_ArrayCopy(this.Orders, Array, 0, Pos + sizeof(int), _R(Array)[Pos] * sizeof(TradeOrder)) + sizeof(int);

      if (this.Header.positions_total)
        Pos += ::_ArrayCopy(this.Positions, Array, 0, Pos + sizeof(int), _R(Array)[Pos] * sizeof(TesterPositionProfit)) + sizeof(int);

//      if (Pos + sizeof(int) < ::ArraySize(Array))
        Pos += ::_ArrayCopy(this.TradeState, Array, 0, Pos + sizeof(int), _R(Array)[Pos] * sizeof(TesterTradeState)) + sizeof(int);
    }

    return(Res);
  }

  SINGLETESTERCACHE( void )
  {
  }

  SINGLETESTERCACHE( const string FileName, const int Common_Flag = 0 )
  {
    this.Load(FileName, Common_Flag);
  }

  template <typename T>
  SINGLETESTERCACHE( const T &Array[] )
  {
    this.Load(Array);
  }

  bool Save( const string FileName, const int Common_Flag = 0 )
  {
    const int handle = ::FileOpen(FileName, FILE_WRITE | FILE_BIN | Common_Flag);
    bool Res = (handle != INVALID_HANDLE);

    if (Res)
    {
      this.Header.version = ::MathMax(this.Header.version, 502);

      const string Name = "SingleTestCache";
      this.Header.name = Name;

      this.Header.period = ::MathMax(this.Header.period, PERIOD_M1);

      this.Header.deals_total = ::ArraySize(this.Deals);
      this.Header.orders_total = ::ArraySize(this.Orders);
      this.Header.positions_total = ::ArraySize(this.Positions);
      this.Header.equities_total = ::ArraySize(this.TradeState);

      if (!::StringLen(this.Header.symbol[]))
      {
        const string Str = this.Header.orders_total ? this.Orders[0].symbol[] : _Symbol;

        this.Header.symbol = Str;
      }

      short Words[];

      if (!::StringLen(this.Inputs))
      {
        this.Header.parameters_size = 0;
        this.Header.parameters_total = 0;
      }
      else
      {
        this.Header.parameters_size = ::ArrayResize(Words, ::StringLen(this.Inputs) + 2);
        ::ArrayInitialize(Words, 0);
        ::StringToShortArray(this.Inputs, Words);

        string StrTmp[];
        this.Header.parameters_total = ::StringSplit(this.Inputs, '\n', StrTmp);
      }

      ::FileWriteStruct(handle, this.Header);

      ::FileWriteArray(handle, Words);

      ::FileWriteStruct(handle, this.Summary);

      ::FileWriteInteger(handle, this.Header.deals_total);
      ::FileWriteArray(handle, this.Deals);

      ::FileWriteInteger(handle, this.Header.orders_total);
      ::FileWriteArray(handle, this.Orders);

      if (this.Header.positions_total)
      {
        ::FileWriteInteger(handle, this.Header.positions_total);
        ::FileWriteArray(handle, this.Positions);
      }

      ::FileWriteInteger(handle, this.Header.equities_total);
      ::FileWriteArray(handle, this.TradeState);

      ::FileClose(handle);
    }

    return(Res);
  }

  void ToNull( void )
  {
    SINGLETESTERCACHE::ToNull(this.Header);
    SINGLETESTERCACHE::ToNull(this.Summary);

    this.Inputs = NULL;

    ::ArrayFree(this.Deals);
    ::ArrayFree(this.Orders);
    ::ArrayFree(this.Positions);
    ::ArrayFree(this.TradeState);

    return;
  }

  bool Set( const datetime FromDate = 0, const datetime ToDate = INT_MAX )
  {
    const bool Res = ::HistorySelect(FromDate, ToDate);

    if (Res)
    {
      this.ToNull();

      for (int i = ::ArrayResize(this.Orders, ::HistoryOrdersTotal()) - 1; i >= 0; i--)
        this.Orders[i].Set(::HistoryOrderGetTicket(i));

      double Profit = 0;
      const int Size = ::ArrayResize(this.TradeState, ::ArrayResize(this.Deals, ::HistoryDealsTotal()));

    #define POS_ID 0
    #define POS_INDEX 1

    #define MAXTIME_VALUE 0
    #define MAXTIME_INDEX 1

      ulong PosID[][2];

      ::ArrayResize(PosID, Size + 1);
      PosID[Size][POS_ID] = ULONG_MAX;

      for (int i = 0; i < Size; i++)
      {
        const ulong Ticket = ::HistoryDealGetTicket(i);

        if (this.Deals[i].Set(Ticket))
        {
          Profit += this.Deals[i].profit + this.Deals[i].storage + this.Deals[i].commission;

          this.Deals[i].reserve = Profit;

          this.TradeState[i].balance = Profit;
          this.TradeState[i].equity = Profit;

          this.TradeState[i].time = this.Deals[i].time_create;

          this.Deals[i].time_create = (datetime)::HistoryDealGetInteger(Ticket, DEAL_TIME_MSC); // Для большей точности.
        }

        PosID[i][POS_ID] = ::HistoryDealGetInteger(Ticket, DEAL_POSITION_ID);
        PosID[i][POS_INDEX] = i;
      }

      ::ArraySort(PosID);

      TesterPositionProfit NewPositions[];
      ::ArrayResize(NewPositions, Size);

      long MaxTimes[][2];
      ::ArrayResize(MaxTimes, Size);

      int Amount = 0;
      ulong PrevPosID = 0;

      int Pos = 0;

      for (int i = 0; i <= Size; i++)
        if (PrevPosID != PosID[i][POS_ID])
        {
          if (PrevPosID)
          {
            int PosMinTime = -1;

            datetime MinTime = LONG_MAX;
            datetime MaxTime = 0;

            Profit = 0;

            for (int j = Pos; j < i; j++)
            {
              const int k = (int)PosID[j][POS_INDEX];

              Profit += this.Deals[k].profit + this.Deals[k].storage + this.Deals[k].commission;

              const datetime time = this.Deals[k].time_create;

              if (time > MaxTime)
                MaxTime = time;

              if ((time < MinTime) && (this.Deals[k].entry == DEAL_ENTRY_IN))
              {
                MinTime = time;

                PosMinTime = k;
              }
            }

            SINGLETESTERCACHE::ToNull(NewPositions[Amount]);

            if (PosMinTime != -1)
            {
              const double Price = this.Deals[PosMinTime].price_open;

              for (int j = Pos; j < i; j++)
              {
                const int k = (int)PosID[j][POS_INDEX];

                if (k != PosMinTime)
                  this.Deals[k].price_close = Price;
              }

              NewPositions[Amount].lifetime = (MaxTime - MinTime) / 1000;
            }

            NewPositions[Amount].id = PrevPosID;
            NewPositions[Amount].profit = Profit;

            MaxTimes[Amount][MAXTIME_VALUE] = MaxTime;
            MaxTimes[Amount][MAXTIME_INDEX] = Amount;

            Amount++;
          }

          Pos = i;
          PrevPosID = PosID[Pos][POS_ID];
        }

      ::ArrayResize(NewPositions, Amount);
      ::ArrayResize(MaxTimes, Amount);

      // Сортировка позиций по времени закрытия
      ::ArraySort(MaxTimes);

      for (int i = ::ArrayResize(this.Positions, Amount) - 1; i >= 0; i--)
        this.Positions[i] = NewPositions[(int)MaxTimes[i][MAXTIME_INDEX]];

      for (int i = 0; i < Size; i++)
        this.Deals[i].time_create /= 1000;

    #undef MAXTIME_INDEX
    #undef MAXTIME_VALUE

    #undef POS_INDEX
    #undef POS_ID
    }

    return(Res);
  }

  static void ChangeArray( double &Array[], const int Pos )
  {
    if (Pos >= 0)
    {
      const double Min = Array[::ArrayMinimum(Array)];
      const double Max = Array[::ArrayMaximum(Array)];

      if (Pos != -1)
        Array[Pos] = (Array[Pos] > (Min + Max) / 2) ? Min : Max;
    }

    return;
  }

  int GetBalance( double &Balance[], const datetime From = 0, const datetime To = 0, const bool WithoutDeposit = false ) const
  {
    const int Size = ::ArrayResize(Balance, ::ArraySize(this.TradeState));

    int PosFrom = -1;
    int PosTo = -1;

    const double Deposit = (WithoutDeposit && Size) ? this.TradeState[0].balance : 0;

    for (int i = 0; i < Size; i++)
    {
      Balance[i] = this.TradeState[i].balance - Deposit;

      if (From && i && (this.TradeState[i - 1].time < From) && (this.TradeState[i].time >= From))
        PosFrom = i;

      if (To && i && (this.TradeState[i - 1].time < To) && (this.TradeState[i].time >= To))
        PosTo = i;
    }

    SINGLETESTERCACHE::ChangeArray(Balance, PosFrom);
    SINGLETESTERCACHE::ChangeArray(Balance, PosTo);

    return(Size);
  }

  int GetEquity( double &Equity[], const datetime From = 0, const datetime To = 0, const bool WithoutDeposit = false ) const
  {
    const int Size = ::ArrayResize(Equity, ::ArraySize(this.TradeState));

    int PosFrom = -1;
    int PosTo = -1;

    const double Deposit = (WithoutDeposit && Size) ? this.TradeState[0].equity : 0;

    for (int i = 0; i < Size; i++)
    {
      Equity[i] = this.TradeState[i].equity - Deposit;

      if (From && i && (this.TradeState[i - 1].time < From) && (this.TradeState[i].time >= From))
        PosFrom = i;

      if (To && i && (this.TradeState[i - 1].time < To) && (this.TradeState[i].time >= To))
        PosTo = i;
    }

    SINGLETESTERCACHE::ChangeArray(Equity, PosFrom);
    SINGLETESTERCACHE::ChangeArray(Equity, PosTo);

    return(Size);
  }

  int GetTradeSymbols( string &Symbols[] ) const
  {
    int Size = 0;

    ::ArrayFree(Symbols);

    for (int i = ::ArraySize(this.Deals) - 1; i >= 0; i--)
    {
      const string Symb = this.Deals[i].GetProperty(DEAL_SYMBOL);

      if (Symb != "")
      {
        int j = 0;

        for (; (j < Size) && (Symbols[j] != Symb); j++)
          ;

        if (j == Size)
        {
          Size = ::ArrayResize(Symbols, Size + 1);

          Symbols[Size - 1] = Symb;
        }
      }
    }

    return(Size);
  }

  bool SaveSet( string FileName = NULL, const bool Details = true,
                string AddInformationBefore = NULL, const int Common_Flag = 0, string AddInformationAfter = NULL ) const
  {
    if (FileName == NULL)
      FileName = this.Header.expert_name[] + ".set";

    return(this.SaveSettings(FileName, this.Inputs, Details, AddInformationBefore, Common_Flag, AddInformationAfter));
  }

  string ToString( void ) const
  {
    return(this.Header.expert_path[] + "\n; " +
           this.Header.symbol[] + "\n; " +
           ::TimeToString(this.Header.date_from, TIME_DATE) + " - " + ::TimeToString(this.Header.date_to, TIME_DATE) + "\n; " +
           ::DoubleToString(this.Summary.TesterStatistics(STAT_PROFIT), 0) + ", " +
           ::DoubleToString(this.Summary.TesterStatistics(STAT_TRADES), 0) + ", " +
           ::DoubleToString(this.Summary.TesterStatistics(STAT_PROFIT_FACTOR), 2) + ", " +
           ::DoubleToString(this.Summary.TesterStatistics(STAT_EXPECTED_PAYOFF), 2) +  ", -" +
           ::DoubleToString(this.Summary.TesterStatistics(STAT_EQUITY_DD), 2));
  }

  string TesterString( void ) const
  {
    return(this.Header.TesterString() + "\n[TesterInputs]\n" + this.Inputs);
  }

  bool SaveIni( string FileName = NULL, const bool Details = true,
                string AddInformationBefore = NULL, const int Common_Flag = 0, string AddInformationAfter = NULL ) const
  {

    if (FileName == NULL)
      FileName = this.Header.expert_name[] + ".ini";

    string Str = this.TesterString();

    ::StringReplace(Str, "\r\n", "\n");
    ::StringReplace(Str, "\n", "\r\n");

    return(this.SaveSettings(FileName, Str, Details, AddInformationBefore, Common_Flag, AddInformationAfter));
  }

  // Максимальная продолжительность просадки с заданного времени.
  int GetMaxLengthDD( datetime &BeginDD, datetime &EndDD, const datetime From = 0 ) const
  {
    const int Total = ::ArraySize(this.Deals);

    double Profit = 0;
    double MaxProfit = 0;
    datetime Begin = 0;

    BeginDD = 0;
    EndDD = 0;

    for (int i = 0; i < Total; i++)
      if (this.Deals[i].GetProperty(DEAL_TYPE) <= DEAL_TYPE_SELL)
      {
        if (!Begin && (this.Deals[i].GetProperty(DEAL_TIME) > From))
          Begin = (datetime)this.Deals[i].GetProperty(DEAL_TIME);

        Profit += this.Deals[i].GetProperty(DEAL_PROFIT) +
                  this.Deals[i].GetProperty(DEAL_COMMISSION) +
                  this.Deals[i].GetProperty(DEAL_SWAP);

        if ((Profit > MaxProfit) || (i == Total - 1))
        {
          MaxProfit = Profit;

          const datetime End = (datetime)this.Deals[i].GetProperty(DEAL_TIME);

          if (Begin && i && (Begin != this.Deals[i - 1].GetProperty(DEAL_TIME)) &&
                       (End - Begin > EndDD - BeginDD))
          {
            BeginDD = Begin;
            EndDD = End;
          }

          if (Begin)
            Begin = End;
        }
      }

    return((int)(EndDD - BeginDD));
  }
};