//+------------------------------------------------------------------+
//| 44_MT5_M1_History_Sync_Monitor_v1_00.mq5                        |
//| Sequentially requests and audits MT5 minute-bar history.         |
//+------------------------------------------------------------------+
#property copyright "FX Omoshiro Lab"
#property link      "https://fx-omoshiro-lab.com/"
#property version   "1.00"
#property strict
#property indicator_chart_window
#property indicator_plots   0
#property indicator_buffers 12

input string          InpSymbolsCSV           = "EURUSD,GBPUSD,USDJPY";
input ENUM_TIMEFRAMES InpTargetPeriod         = PERIOD_M1;
input int             InpTargetBars           = 5000;
input int             InpStableChecks         = 2;
input int             InpMaxAttemptsPerSymbol = 60;
input bool            InpShowPanel            = true;

#define OBJECT_PREFIX  "MQL44_HISTORY_"
#define MAX_TARGETS    8

#define STATE_PENDING  0
#define STATE_SYNCING  1
#define STATE_READY    2
#define STATE_FAILED   3

double g_finished_buffer[];
double g_target_count_buffer[];
double g_completed_count_buffer[];
double g_synchronized_count_buffer[];
double g_selected_count_buffer[];
double g_failed_count_buffer[];
double g_request_count_buffer[];
double g_min_bars_buffer[];
double g_target_bars_buffer[];
double g_active_index_buffer[];
double g_elapsed_sec_buffer[];
double g_panel_objects_buffer[];

string g_requested_symbols[];
string g_resolved_symbols[];
int g_states[];
int g_attempts[];
int g_bar_counts[];
int g_last_copied[];
int g_stable_counts[];
int g_last_errors[];
int g_synchronized[];

int g_target_count = 0;
int g_active_index = 0;
datetime g_started_at = 0;

bool BindBuffers();
bool BuffersReady();
bool PrepareTargets();
bool UsesCurrentChartSeries();
string TrimText(string value);
string ResolveSymbol(const string requested);
bool IsDuplicateTarget(const string symbol_name, const int used_count);
void ProcessActiveTarget();
void AdvanceActiveIndex();
void UpdateBuffers();
void DrawPanel();
void SetPanelLine(const int row, const string text, const color text_color,
                  const int font_size = 10);
string StateText(const int state);
color StateColor(const int state);
string PeriodText(const ENUM_TIMEFRAMES timeframe);
int CountState(const int wanted_state);
int CountSelectedTargets();
int CountSynchronizedTargets();
int CountRequests();
int MinimumBars();
int CountPanelObjects();
void DeleteObjects();

int OnInit()
{
   if(InpTargetBars < 100 ||
      InpStableChecks < 1 ||
      InpStableChecks > 10 ||
      InpMaxAttemptsPerSymbol < InpStableChecks ||
      InpMaxAttemptsPerSymbol > 600)
   {
      Print("M1 history sync: invalid input parameters");
      return INIT_PARAMETERS_INCORRECT;
   }

   if(!BindBuffers() || !PrepareTargets())
      return INIT_FAILED;
   if(UsesCurrentChartSeries())
   {
      Print("M1 history sync: attach the monitor to a different timeframe "
            "from the requested current-symbol series");
      return INIT_PARAMETERS_INCORRECT;
   }

   IndicatorSetString(INDICATOR_SHORTNAME, "MT5 M1 History Sync Monitor");
   g_started_at = TimeLocal();
   ResetLastError();
   if(!EventSetTimer(1))
   {
      PrintFormat("M1 history sync: EventSetTimer failed error=%d",
                  GetLastError());
      return INIT_FAILED;
   }
   return INIT_SUCCEEDED;
}

void OnDeinit(const int reason)
{
   EventKillTimer();
   DeleteObjects();
}

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[])
{
   if(rates_total < 1)
      return 0;

   if(InpShowPanel)
      DrawPanel();
   else
      DeleteObjects();
   UpdateBuffers();
   return rates_total;
}

void OnTimer()
{
   ProcessActiveTarget();
   if(InpShowPanel)
      DrawPanel();
   else
      DeleteObjects();
   UpdateBuffers();
   ChartRedraw(0);
}

bool BindBuffers()
{
   if(!SetIndexBuffer(0, g_finished_buffer, INDICATOR_CALCULATIONS) ||
      !SetIndexBuffer(1, g_target_count_buffer, INDICATOR_CALCULATIONS) ||
      !SetIndexBuffer(2, g_completed_count_buffer, INDICATOR_CALCULATIONS) ||
      !SetIndexBuffer(3, g_synchronized_count_buffer, INDICATOR_CALCULATIONS) ||
      !SetIndexBuffer(4, g_selected_count_buffer, INDICATOR_CALCULATIONS) ||
      !SetIndexBuffer(5, g_failed_count_buffer, INDICATOR_CALCULATIONS) ||
      !SetIndexBuffer(6, g_request_count_buffer, INDICATOR_CALCULATIONS) ||
      !SetIndexBuffer(7, g_min_bars_buffer, INDICATOR_CALCULATIONS) ||
      !SetIndexBuffer(8, g_target_bars_buffer, INDICATOR_CALCULATIONS) ||
      !SetIndexBuffer(9, g_active_index_buffer, INDICATOR_CALCULATIONS) ||
      !SetIndexBuffer(10, g_elapsed_sec_buffer, INDICATOR_CALCULATIONS) ||
      !SetIndexBuffer(11, g_panel_objects_buffer, INDICATOR_CALCULATIONS))
   {
      PrintFormat("M1 history sync: SetIndexBuffer failed error=%d",
                  GetLastError());
      return false;
   }

   ArraySetAsSeries(g_finished_buffer, true);
   ArraySetAsSeries(g_target_count_buffer, true);
   ArraySetAsSeries(g_completed_count_buffer, true);
   ArraySetAsSeries(g_synchronized_count_buffer, true);
   ArraySetAsSeries(g_selected_count_buffer, true);
   ArraySetAsSeries(g_failed_count_buffer, true);
   ArraySetAsSeries(g_request_count_buffer, true);
   ArraySetAsSeries(g_min_bars_buffer, true);
   ArraySetAsSeries(g_target_bars_buffer, true);
   ArraySetAsSeries(g_active_index_buffer, true);
   ArraySetAsSeries(g_elapsed_sec_buffer, true);
   ArraySetAsSeries(g_panel_objects_buffer, true);
   return true;
}

bool BuffersReady()
{
   return ArraySize(g_finished_buffer) >= 1 &&
          ArraySize(g_target_count_buffer) >= 1 &&
          ArraySize(g_completed_count_buffer) >= 1 &&
          ArraySize(g_synchronized_count_buffer) >= 1 &&
          ArraySize(g_selected_count_buffer) >= 1 &&
          ArraySize(g_failed_count_buffer) >= 1 &&
          ArraySize(g_request_count_buffer) >= 1 &&
          ArraySize(g_min_bars_buffer) >= 1 &&
          ArraySize(g_target_bars_buffer) >= 1 &&
          ArraySize(g_active_index_buffer) >= 1 &&
          ArraySize(g_elapsed_sec_buffer) >= 1 &&
          ArraySize(g_panel_objects_buffer) >= 1;
}

bool PrepareTargets()
{
   string parts[];
   const ushort separator = (ushort)StringGetCharacter(",", 0);
   const int part_count = StringSplit(InpSymbolsCSV, separator, parts);
   if(part_count < 1)
   {
      Print("M1 history sync: no symbols were supplied");
      return false;
   }

   if(ArrayResize(g_requested_symbols, MAX_TARGETS) != MAX_TARGETS ||
      ArrayResize(g_resolved_symbols, MAX_TARGETS) != MAX_TARGETS ||
      ArrayResize(g_states, MAX_TARGETS) != MAX_TARGETS ||
      ArrayResize(g_attempts, MAX_TARGETS) != MAX_TARGETS ||
      ArrayResize(g_bar_counts, MAX_TARGETS) != MAX_TARGETS ||
      ArrayResize(g_last_copied, MAX_TARGETS) != MAX_TARGETS ||
      ArrayResize(g_stable_counts, MAX_TARGETS) != MAX_TARGETS ||
      ArrayResize(g_last_errors, MAX_TARGETS) != MAX_TARGETS ||
      ArrayResize(g_synchronized, MAX_TARGETS) != MAX_TARGETS)
   {
      Print("M1 history sync: target array allocation failed");
      return false;
   }

   for(int part = 0; part < part_count && g_target_count < MAX_TARGETS; ++part)
   {
      const string requested = TrimText(parts[part]);
      if(StringLen(requested) < 1)
         continue;

      const string resolved = ResolveSymbol(requested);
      if(StringLen(resolved) > 0 &&
         IsDuplicateTarget(resolved, g_target_count))
         continue;

      const int index = g_target_count;
      g_requested_symbols[index] = requested;
      g_resolved_symbols[index] = resolved;
      g_states[index] = StringLen(resolved) > 0
                           ? STATE_PENDING : STATE_FAILED;
      g_attempts[index] = 0;
      g_bar_counts[index] = 0;
      g_last_copied[index] = 0;
      g_stable_counts[index] = 0;
      g_last_errors[index] =
         StringLen(resolved) > 0 ? 0 : ERR_MARKET_UNKNOWN_SYMBOL;
      g_synchronized[index] = 0;
      ++g_target_count;
   }

   if(g_target_count < 1)
   {
      Print("M1 history sync: no valid targets were parsed");
      return false;
   }
   AdvanceActiveIndex();
   return true;
}

bool UsesCurrentChartSeries()
{
   if(_Period != InpTargetPeriod)
      return false;
   for(int index = 0; index < g_target_count; ++index)
   {
      if(g_resolved_symbols[index] == _Symbol)
         return true;
   }
   return false;
}

string TrimText(string value)
{
   StringTrimLeft(value);
   StringTrimRight(value);
   return value;
}

string ResolveSymbol(const string requested)
{
   bool custom = false;
   ResetLastError();
   if(SymbolExist(requested, custom))
      return requested;

   const int total = SymbolsTotal(false);
   for(int index = 0; index < total; ++index)
   {
      const string candidate = SymbolName(index, false);
      if(StringFind(candidate, requested) >= 0)
         return candidate;
   }
   return "";
}

bool IsDuplicateTarget(const string symbol_name, const int used_count)
{
   for(int index = 0; index < used_count; ++index)
   {
      if(g_resolved_symbols[index] == symbol_name)
         return true;
   }
   return false;
}

void ProcessActiveTarget()
{
   AdvanceActiveIndex();
   if(g_active_index >= g_target_count)
      return;

   const int index = g_active_index;
   const string symbol_name = g_resolved_symbols[index];
   if(StringLen(symbol_name) < 1)
   {
      g_states[index] = STATE_FAILED;
      AdvanceActiveIndex();
      return;
   }

   g_states[index] = STATE_SYNCING;
   ++g_attempts[index];
   ResetLastError();
   if(!SymbolSelect(symbol_name, true))
   {
      g_last_errors[index] = GetLastError();
      g_states[index] = STATE_FAILED;
      AdvanceActiveIndex();
      return;
   }

   MqlRates rates[];
   ArraySetAsSeries(rates, true);
   ResetLastError();
   const int copied =
      CopyRates(symbol_name, InpTargetPeriod, 0, InpTargetBars, rates);
   const int copy_error = GetLastError();
   g_last_copied[index] = copied > 0 ? copied : 0;
   g_last_errors[index] = copied < 0 ? copy_error : 0;

   long bars_value = 0;
   long synchronized_value = 0;
   ResetLastError();
   const bool bars_read =
      SeriesInfoInteger(symbol_name, InpTargetPeriod,
                        SERIES_BARS_COUNT, bars_value);
   const int bars_error = GetLastError();
   ResetLastError();
   const bool sync_read =
      SeriesInfoInteger(symbol_name, InpTargetPeriod,
                        SERIES_SYNCHRONIZED, synchronized_value);
   const int sync_error = GetLastError();

   g_bar_counts[index] =
      bars_read && bars_value > 0 ? (int)bars_value : 0;
   g_synchronized[index] =
      sync_read && synchronized_value != 0 ? 1 : 0;
   if(!bars_read)
      g_last_errors[index] = bars_error;
   else if(!sync_read)
      g_last_errors[index] = sync_error;

   const bool target_reached =
      g_synchronized[index] == 1 &&
      g_bar_counts[index] >= InpTargetBars &&
      g_last_copied[index] >= InpTargetBars;
   if(target_reached)
      ++g_stable_counts[index];
   else
      g_stable_counts[index] = 0;

   if(g_stable_counts[index] >= InpStableChecks)
   {
      g_states[index] = STATE_READY;
      AdvanceActiveIndex();
      return;
   }

   if(g_attempts[index] >= InpMaxAttemptsPerSymbol)
   {
      g_states[index] = STATE_FAILED;
      AdvanceActiveIndex();
   }
}

void AdvanceActiveIndex()
{
   while(g_active_index < g_target_count &&
         (g_states[g_active_index] == STATE_READY ||
          g_states[g_active_index] == STATE_FAILED))
   {
      ++g_active_index;
   }
}

void UpdateBuffers()
{
   if(!BuffersReady())
      return;

   const bool finished = g_active_index >= g_target_count;
   g_finished_buffer[0] = finished ? 1.0 : 0.0;
   g_target_count_buffer[0] = (double)g_target_count;
   g_completed_count_buffer[0] = (double)CountState(STATE_READY);
   g_synchronized_count_buffer[0] =
      (double)CountSynchronizedTargets();
   g_selected_count_buffer[0] = (double)CountSelectedTargets();
   g_failed_count_buffer[0] = (double)CountState(STATE_FAILED);
   g_request_count_buffer[0] = (double)CountRequests();
   g_min_bars_buffer[0] = (double)MinimumBars();
   g_target_bars_buffer[0] = (double)InpTargetBars;
   g_active_index_buffer[0] = (double)g_active_index;
   g_elapsed_sec_buffer[0] =
      (double)MathMax(0, (int)(TimeLocal() - g_started_at));
   g_panel_objects_buffer[0] = (double)CountPanelObjects();
}

void DrawPanel()
{
   const string background = OBJECT_PREFIX + "Background";
   if(ObjectFind(0, background) < 0 &&
      !ObjectCreate(0, background, OBJ_RECTANGLE_LABEL, 0, 0, 0))
   {
      PrintFormat("M1 history sync: background creation failed error=%d",
                  GetLastError());
      return;
   }
   ObjectSetInteger(0, background, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, background, OBJPROP_XDISTANCE, 24);
   ObjectSetInteger(0, background, OBJPROP_YDISTANCE, 42);
   ObjectSetInteger(0, background, OBJPROP_XSIZE, 825);
   ObjectSetInteger(0, background, OBJPROP_YSIZE,
                    245 + g_target_count * 34);
   ObjectSetInteger(0, background, OBJPROP_BGCOLOR, C'10,18,24');
   ObjectSetInteger(0, background, OBJPROP_BORDER_TYPE, BORDER_FLAT);
   ObjectSetInteger(0, background, OBJPROP_COLOR, clrDimGray);
   ObjectSetInteger(0, background, OBJPROP_BACK, false);
   ObjectSetInteger(0, background, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, background, OBJPROP_HIDDEN, true);

   SetPanelLine(0, "MT5 M1 HISTORY SYNC MONITOR", clrWhite, 15);
   SetPanelLine(1,
                StringFormat("Sequential CopyRates  |  target: %d %s bars  |  suffix aware",
                             InpTargetBars, PeriodText(InpTargetPeriod)),
                clrSilver);

   for(int index = 0; index < g_target_count; ++index)
   {
      const string resolved =
         StringLen(g_resolved_symbols[index]) > 0
            ? g_resolved_symbols[index] : "NOT FOUND";
      SetPanelLine(2 + index,
                   StringFormat("%d. %-8s -> %-12s  bars=%d  copied=%d  sync=%s  %s",
                                index + 1,
                                g_requested_symbols[index],
                                resolved,
                                g_bar_counts[index],
                                g_last_copied[index],
                                g_synchronized[index] == 1 ? "YES" : "NO",
                                StateText(g_states[index])),
                   StateColor(g_states[index]), 11);
   }

   const int summary_row = 2 + g_target_count;
   const int completed = CountState(STATE_READY);
   const int failed = CountState(STATE_FAILED);
   const bool finished = g_active_index >= g_target_count;
   SetPanelLine(summary_row,
                StringFormat("PROGRESS: %d/%d ready  |  synchronized=%d  |  selected=%d",
                             completed, g_target_count,
                             CountSynchronizedTargets(),
                             CountSelectedTargets()),
                completed == g_target_count ? clrLime : clrGold);
   SetPanelLine(summary_row + 1,
                StringFormat("REQUESTS: %d  |  elapsed=%d sec  |  one target at a time",
                             CountRequests(),
                             MathMax(0, (int)(TimeLocal() - g_started_at))),
                clrSilver);
   SetPanelLine(summary_row + 2,
                finished && failed == 0
                   ? "VERDICT: ALL TARGET HISTORIES READY  |  VALIDATION PASS"
                   : finished
                       ? StringFormat("VERDICT: FINISHED WITH %d FAILED TARGET(S)",
                                      failed)
                       : "VERDICT: SYNCHRONIZATION IN PROGRESS",
                finished && failed == 0
                   ? clrLime : finished ? clrTomato : clrGold, 11);
}

void SetPanelLine(const int row, const string text, const color text_color,
                  const int font_size)
{
   const string name = OBJECT_PREFIX + "Line_" + IntegerToString(row);
   if(ObjectFind(0, name) < 0 &&
      !ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0))
   {
      PrintFormat("M1 history sync: label creation failed row=%d error=%d",
                  row, GetLastError());
      return;
   }
   ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, name, OBJPROP_XDISTANCE, 46);
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, 60 + row * 34);
   ObjectSetInteger(0, name, OBJPROP_COLOR, text_color);
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, font_size);
   ObjectSetInteger(0, name, OBJPROP_ANCHOR, ANCHOR_LEFT_UPPER);
   ObjectSetString(0, name, OBJPROP_FONT, "Consolas");
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
}

string StateText(const int state)
{
   if(state == STATE_SYNCING)
      return "SYNCING";
   if(state == STATE_READY)
      return "READY";
   if(state == STATE_FAILED)
      return "FAILED";
   return "PENDING";
}

color StateColor(const int state)
{
   if(state == STATE_READY)
      return clrLime;
   if(state == STATE_FAILED)
      return clrTomato;
   if(state == STATE_SYNCING)
      return clrGold;
   return clrSilver;
}

string PeriodText(const ENUM_TIMEFRAMES timeframe)
{
   const string value = EnumToString(timeframe);
   if(StringFind(value, "PERIOD_") == 0)
      return StringSubstr(value, 7);
   return value;
}

int CountState(const int wanted_state)
{
   int count = 0;
   for(int index = 0; index < g_target_count; ++index)
   {
      if(g_states[index] == wanted_state)
         ++count;
   }
   return count;
}

int CountSelectedTargets()
{
   int count = 0;
   for(int index = 0; index < g_target_count; ++index)
   {
      const string symbol_name = g_resolved_symbols[index];
      if(StringLen(symbol_name) > 0 &&
         (bool)SymbolInfoInteger(symbol_name, SYMBOL_SELECT))
         ++count;
   }
   return count;
}

int CountSynchronizedTargets()
{
   int count = 0;
   for(int index = 0; index < g_target_count; ++index)
   {
      if(g_synchronized[index] == 1)
         ++count;
   }
   return count;
}

int CountRequests()
{
   int count = 0;
   for(int index = 0; index < g_target_count; ++index)
      count += g_attempts[index];
   return count;
}

int MinimumBars()
{
   if(g_target_count < 1)
      return 0;

   int minimum = INT_MAX;
   for(int index = 0; index < g_target_count; ++index)
   {
      if(g_bar_counts[index] < minimum)
         minimum = g_bar_counts[index];
   }
   return minimum == INT_MAX ? 0 : minimum;
}

int CountPanelObjects()
{
   int count = 0;
   const int total = ObjectsTotal(0, -1, -1);
   for(int index = 0; index < total; ++index)
   {
      const string name = ObjectName(0, index, -1, -1);
      if(StringFind(name, OBJECT_PREFIX) == 0)
         ++count;
   }
   return count;
}

void DeleteObjects()
{
   for(int index = ObjectsTotal(0, -1, -1) - 1; index >= 0; --index)
   {
      const string name = ObjectName(0, index, -1, -1);
      if(StringFind(name, OBJECT_PREFIX) == 0)
         ObjectDelete(0, name);
   }
}
