#include "TestCacheHeader.mqh"
#include "ExpTradeSummary.mqh"
#include "TestCacheInput.mqh"
#include "Mathematics.mqh"
#include "TestCacheRecord.mqh"
#include "TestCacheSymbolRecord.mqh"

template <typename T>
class TESTERCACHE
{
private:
  bool IsCorrectType( void ) const
  {
    return((typename(T) == "MATHEMATICS") && (this.Header.symbol[] == "")) ||
           ((typename(T) == "TestCacheSymbolRecord") && (this.Header.opt_mode == 2)) ||
           ((typename(T) == "ExpTradeSummary") && (this.Header.symbol[] != "") && (this.Header.opt_mode < 2));
  }

public:
  TestCacheHeader Header;
  TestCacheInput Inputs[];
  uchar ParametersBuffer[];
  TestCacheRecord<T> Record[];
  int UnknownNums[];

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

    if (Res)
    {
      ::ArrayFree(this.Inputs);
      ::ArrayFree(this.ParametersBuffer);
      ::ArrayFree(this.Record);
      ::ArrayFree(this.UnknownNums);

      ::FileReadStruct(handle, this.Header);

      if (Res = this.IsCorrectType() && this.Header.passes_passed)
      {
        ::FileReadArray(handle, this.Inputs, 0, this.Header.parameters_total);

        ::FileReadArray(handle, this.ParametersBuffer, 0, this.Header.parameters_size);

        ::FileReadArray(handle, this.UnknownNums, 0, this.Header.snapshot_size);

        const int Size = ::ArrayResize(this.Record, this.Header.passes_passed);
        const bool Offset = (this.Header.record_size - this.Header.opt_params_size - sizeof(T) == sizeof(long));

        for (int i = 0; i < Size; i++)
        {
          ::FileReadStruct(handle, this.Record[i].Result);

          ::FileReadArray(handle, this.Record[i].OptBuffer, 0, this.Header.opt_params_size);

          if (Offset)
            ::FileReadArray(handle, this.Record[i].Genetic, 0, sizeof(long) + this.Header.dwords_cnt * sizeof(int));
          else
            ::FileReadArray(handle, this.Record[i].Genetic, 0, this.Header.dwords_cnt * sizeof(int));
        }
      }

      ::FileClose(handle);
    }

    return(Res);
  }

  bool Load( const uchar &Bytes[] )
  {
    bool Res = ::ArraySize(Bytes);

    if (Res)
    {
      ::ArrayFree(this.Inputs);
      ::ArrayFree(this.ParametersBuffer);
      ::ArrayFree(this.Record);
      ::ArrayFree(this.UnknownNums);

      _W(this.Header) = Bytes;

      if (Res = this.IsCorrectType() && this.Header.passes_passed)
      {
        int Pos = sizeof(this.Header);

        Pos += ::_ArrayCopy(this.Inputs, Bytes, 0, Pos, this.Header.parameters_total * sizeof(TestCacheInput));
        Pos += ::_ArrayCopy(this.ParametersBuffer, Bytes, 0, Pos, this.Header.parameters_size);
        Pos += ::_ArrayCopy(this.UnknownNums, Bytes, 0, Pos, this.Header.snapshot_size * sizeof(int));

        const int Size = ::ArrayResize(this.Record, this.Header.passes_passed);
        const bool Offset = (this.Header.record_size - this.Header.opt_params_size - sizeof(T) == sizeof(long));

        for (int i = 0; i < Size; i++)
        {
          T Result[1];

          Pos += ::_ArrayCopy(Result, Bytes, 0, Pos, sizeof(T));
          this.Record[i].Result = Result[0];

          Pos += ::_ArrayCopy(this.Record[i].OptBuffer, Bytes, 0, Pos, this.Header.opt_params_size);

          if (Offset)
            Pos += ::_ArrayCopy(this.Record[i].Genetic, Bytes, 0, Pos, sizeof(long) + this.Header.dwords_cnt * sizeof(int));
          else
            Pos += ::_ArrayCopy(this.Record[i].Genetic, Bytes, 0, Pos, this.Header.dwords_cnt * sizeof(int));
        }
      }
    }

    return(Res);
  }

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

    if (Res)
    {
      ::FileWriteStruct(handle, this.Header);

      ::FileWriteArray(handle, this.Inputs);

      ::FileWriteArray(handle, this.ParametersBuffer);

      ::FileWriteArray(handle, this.UnknownNums);

      const int Size = ::ArraySize(this.Record);

      for (int i = 0; i < Size; i++)
      {
        ::FileWriteStruct(handle, this.Record[i].Result);

        ::FileWriteArray(handle, this.Record[i].OptBuffer);

        ::FileWriteArray(handle, this.Record[i].Genetic);
      }

      ::FileClose(handle);
    }

    return(Res);
  }
  TESTERCACHE( void )
  {
  }

  TESTERCACHE( const string FileName )
  {
    this.Load(FileName);
  }

  // https://www.mql5.com/ru/forum/324536/page20#comment_13850494
  TESTERCACHE( const uchar &Bytes[] )
  {
    this.Load(Bytes);
  }

#define PARAM_NAME 0
#define PARAM_VALUE 1
#define PARAM_START 2
#define PARAM_STEP 3
#define PARAM_STOP 4

#define MACROS(A, B)                                                                                                      \
  const string Str##A = ((Type == TYPE_FLOAT) || (Type == TYPE_DOUBLE)) ? (string)this.Inputs[i].StartStepStop.Number.##A \
                                                                      : (string)this.Inputs[i].StartStepStop.Integer.##A; \
  Params[j][PARAM_##B].type = Params[j][PARAM_VALUE].type;                                                                \
  Params[j][PARAM_##B].integer_value = (long)Str##A;                                                                      \
  Params[j][PARAM_##B].double_value = (double)Str##A;                                                                     \
  Params[j][PARAM_##B].string_value = Str##A;

  int GetInputs( const int Num, MqlParam &Params[][5], const bool Full = false ) const
  {
    int j = 0;
    int Count = 0;
    const int Size = ::ArraySize(this.Inputs);

    ::ArrayResize(Params, Size);
    ::ZeroMemory(Params);

    for (int i = 0; i < Size; i++)
      if (Full || this.Inputs[i].flag)
      {
        Params[j][PARAM_NAME].type = TYPE_STRING;
        Params[j][PARAM_NAME].string_value = this.Inputs[i].name[];

        const int Type = this.Inputs[i].type - TYPE_OFFSET;
        string StrValue;

        if (this.Inputs[i].flag)
        {
          StrValue = ((Type == TYPE_FLOAT) || (Type == TYPE_DOUBLE)) ? (string)_R(this.Record[Num].OptBuffer)[(double)Count]
                                                                     : (string)_R(this.Record[Num].OptBuffer)[(long)Count];

          Count += sizeof(long);
        }
        else
        {
          const string Str = this.Inputs[i].ToString(this.ParametersBuffer);

          StrValue = (Type == TYPE_BOOL) ? ((Str == "true") ? "1" : "0") :
                     ((Type == TYPE_COLOR) ? (string)(long)(color)Str :
                     ((Type == TYPE_DATETIME) ? (string)(long)(datetime)Str : Str));
        }

        Params[j][PARAM_VALUE].type = (Type >= TYPE_BOOL) && (Type <= TYPE_STRING) ? (ENUM_DATATYPE)Type : TYPE_INT;
        Params[j][PARAM_VALUE].integer_value = (long)StrValue;
        Params[j][PARAM_VALUE].double_value = (double)StrValue;
        Params[j][PARAM_VALUE].string_value = StrValue;

        MACROS(Start, START)
        MACROS(Step, STEP)
        MACROS(Stop, STOP)

        j++;
      }

    return(::ArrayResize(Params, j));
  }
#undef MACROS

  bool SaveSet( const int Num, string FileName = NULL, const bool Details = true, string AddInformation = NULL )
  {
    if (FileName == NULL)
      FileName = this.Header.expert_name[] + "_" + (string)Num + ".set";

    const int handle = ::FileOpen(FileName, FILE_WRITE | FILE_UNICODE | FILE_TXT);
    const bool Res= (handle != INVALID_HANDLE);

    if (Res)
    {
      MqlParam Params[][5];
      const int Size = this.GetInputs(Num, Params, true);

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

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

        ::FileWriteString(handle, "; " + AddInformation);
      }

      if (Details)
      {
        ::FileWriteString(handle, ((AddInformation == NULL) ? NULL : "\n") +
                                  "; saved on " + (string)::TimeLocal() + "\n; " +
                                  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[Num].TesterStatistics(STAT_PROFIT), 0) + ", " +
                                  ::DoubleToString(this[Num].TesterStatistics(STAT_TRADES), 0) + ", " +
                                  ::DoubleToString(this[Num].TesterStatistics(STAT_PROFIT_FACTOR), 2) + ", " +
                                  ::DoubleToString(this[Num].TesterStatistics(STAT_EXPECTED_PAYOFF), 2) +  ", -" +
                                  ::DoubleToString(this[Num].TesterStatistics(STAT_EQUITY_DD), 2) +
                                  "\n;\n");

      }

      for (int i = 0; i < Size; i++)
        ::FileWriteString(handle, Params[i][PARAM_NAME].string_value == "" ? ";\n" :
                                  Params[i][PARAM_NAME].string_value + "=" +
                                  Params[i][PARAM_VALUE].string_value +
                                  ((!Params[i][PARAM_START].double_value &&
                                    !Params[i][PARAM_STEP].double_value &&
                                    !Params[i][PARAM_STOP].double_value) ? NULL : "||" +
                                    Params[i][PARAM_START].string_value + "||" +
                                    Params[i][PARAM_STEP].string_value + "||" +
                                    Params[i][PARAM_STOP].string_value + "||" +
                                    (this.Inputs[i].flag ? "Y" : "N")) + "\n");

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

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

        ::FileWriteString(handle, Str);
      }

      ::FileClose(handle);
    }

    return(Res);
  }

  const T operator[]( const int Num ) const
  {
    return(this.Record[Num].Result);
  }

  int GetAmount( void ) const
  {
    return(::ArraySize(this.Record));
  }
};