//+------------------------------------------------------------------+
//|                                                        Timer.mqh |
//|                                    Copyright 2020, Nikolai Semko |
//|                         https://www.mql5.com/ru/users/nikolay7ko |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, Nikolai Semko"
#property link      "https://www.mql5.com/ru/users/nikolay7ko"
#property link      "SemkoNV@bk.ru"
#property version   "1.07"

//--- Что изменилось в 1.07 
//---
//--- * GetTickCount64 вместо GetTickCount. 32-битный счётчик переполняется
//---   через 49.7 суток, и из-за проверки 'start>t0' таймер после этого
//---   замолкал не на один цикл, а навсегда: start остаётся большим, t0
//---   начинает отсчёт заново, и условие срабатывает до конца жизни
//---   программы. На круглосуточно работающем терминале это рано или поздно
//---   случалось гарантированно. Все поля времени переведены в ulong.
//---
//--- * Первый вызов теперь происходит через заданный период, а не сразу
//---   при создании таймера. Причина была в том, что n стартовало с нуля,
//---   а сравнение нестрогое: v3>=0 истинно уже на первом тике. Лечится
//---   именно стартовым значением n=1, а не заменой >= на >: строка
//---   n=v3+1 ниже рассчитана на нестрогое сравнение, и при строгом период
//---   удваивается, а счётчик lost растёт на единицу с каждым
//---   срабатыванием, показывая пропуски, которых нет.
//---
//--- * KillTimer больше не пропускает дубликаты. RemoveTimer переставляет
//---   в позицию i последний элемент массива, и его нужно осмотреть, а не
//---   перешагнуть через i++.
//---
//--- * Дескриптор таймера теперь собственный счётчик id, а не момент
//---   создания: два таймера, созданных в одну миллисекунду, раньше были
//---   для KillTimer неразличимы. Тип остался uint, поэтому вызывающий
//---   код менять не нужно.
//---
//--- * Период меньше 1 мс приводил к делению на ноль - теперь поджимается.

#ifndef  timer_define
#define timer_define

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
typedef void (*TFuncTimer)();
class CTimer {
private:
   struct iTimer {
      TFuncTimer     f;
      ulong          t;     // период, мс
      ulong          n;     // номер следующего ожидаемого кадра
      ulong          lost;  // сколько потеряно вызовов
      ulong          start; // время старта таймера
      uint           id;    // дескриптор, выдаётся NewTimer
   };
   uint              N;
   iTimer            mt[];
   bool              loop;
   uint              last_id;    // 0 зарезервирован под "таймер не создан"
   bool              RemoveTimer(uint i) {
      if (N<1 || i>=N) return false;
      N--;
      if (i<N)  mt[i]=mt[N];
      ArrayResize(mt,N);
      return true;
   }

public:
                     CTimer() {
      N=0;
      loop=false;
      last_id=0;
   };
                    ~CTimer();
   void              OnTimer();                                   // эту функцию нужно поставить в штатный OnTimer
   void              KillTimer(TFuncTimer fun);                   // Удаляет все таймеры с функцией обработки fun
   void              KillTimer(int milliseconds, TFuncTimer fun); // Удаляет таймер с функцией обработки fun и периодичностью milliseconds
   void              KillTimer(uint id);                          // Удаляет таймер по дескриптору, полученному от NewTimer
   uint              NewTimer(int milliseconds, TFuncTimer fun);  // Создает таймер с периодичностью milliseconds и фукцией обработки fun.
   // Возвращает дескриптор таймера, или 0, если таймер с такими параметрами уже есть.
   void              NewPeriod(int old_milliseconds, TFuncTimer fun, int new_milliseconds); // Меняет периодичность данного таймера.Это позволяет
   // создавать таймеры с динамическим периодом обращения, или автоподстройкой.
   void              NewPeriod(TFuncTimer fun, int new_milliseconds); // Меняет периодичность данного таймера с функцией обработки fun. Если существуют
   // несколько таймеров с такой функцией обработки, то остальные таймеры удаляются.
   uint              GetN() {
      return N;
   };                     // Получаем текущее количество работающих таймеров
   int               GetLost(int milliseconds, TFuncTimer fun);   // Получаем количество пропущенных событий таймера для контроля стабильности работы.
   int               GetNbyStart(uint id);                        // Получаем текущий номер кадра таймера по его дескриптору
   // Если ноль, то пропусков нет. Если -1, то не найден такой таймер.
};
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CTimer::~CTimer() {
   if (N>0) {
      ArrayFree(mt);
      EventKillTimer();
   }
}
//+------------------------------------------------------------------+
void CTimer::OnTimer() {
   if (loop) return;
   loop=true;
   ulong t0=GetTickCount64();
   uint n=N;
   for (uint i=0; i<N; i++) {
      // Ветка на случай, если start намеренно выставлен в будущее. С 64-битным
      // счётчиком сработать из-за переполнения она больше не может.
      if (mt[i].start>t0) continue;
      ulong v3=(t0-mt[i].start)/mt[i].t;
      if (v3>=mt[i].n) {
         mt[i].lost+=v3-mt[i].n;
         mt[i].n=v3+1;
         if (mt[i].f!=NULL) mt[i].f();
         // Обработчик мог снять таймер, в том числе сам себя. Тогда в позицию i
         // переехал последний элемент, и его надо осмотреть, а не перешагнуть.
         if (n>N) {
            i--;
            n=N;
         }
      }
   }
   loop=false;
}
//+------------------------------------------------------------------+
uint CTimer::NewTimer(int milliseconds, TFuncTimer fun) {
   if (milliseconds<1) milliseconds=1;   // иначе деление на ноль в OnTimer
   if (N==0) EventSetMillisecondTimer(15);
   else for (uint i=0; i<N; i++) {   // проверяем нет ли уже таймера с такими параметрами, чтобы избежать дубликатов.
         if (mt[i].f== fun && mt[i].t==(ulong)milliseconds) return 0;
      }
   ArrayResize(mt,N+1);
   mt[N].t=milliseconds;
   mt[N].f=fun;
   mt[N].start=GetTickCount64();
   mt[N].n=1;      // 1, а не 0: первый вызов должен состояться через период, а не сразу
   mt[N].lost=0;
   mt[N].id=++last_id;
   N++;
   return mt[N-1].id;
}
//+------------------------------------------------------------------+
void CTimer::NewPeriod(int old_milliseconds, TFuncTimer fun, int new_milliseconds) {
   if (new_milliseconds<1) new_milliseconds=1;
   bool dubl=false;
   for (uint i=0; i<N; i++)
      if (mt[i].f== fun && mt[i].t==(ulong)new_milliseconds) dubl=true;// проверяем нет ли уже таймера с такими параметрами, чтобы избежать дубликатов.
   for (uint i=0; i<N; i++)
      if (mt[i].f== fun && mt[i].t==(ulong)old_milliseconds) {
         if (dubl) {
            KillTimer(old_milliseconds, fun);
            break;
         }
         mt[i].t= new_milliseconds;
         mt[i].start=GetTickCount64();
         mt[i].n=1;
         break;
      }
}
//+------------------------------------------------------------------+
void CTimer::NewPeriod(TFuncTimer fun, int new_milliseconds) {
   if (new_milliseconds<1) new_milliseconds=1;
   bool dubl=false;
   for (uint i=0; i<N; i++)
      if (mt[i].f== fun) {
         if (dubl) {
            if (RemoveTimer(i)) i--;
         } else {
            mt[i].t= new_milliseconds;
            mt[i].start=GetTickCount64();
            mt[i].n=1;
            dubl=true;
         }
      }
}
//+------------------------------------------------------------------+
void CTimer::KillTimer(TFuncTimer fun) {
   for (uint i=0; i<N; i++) {
      if (mt[i].f== fun) {
         if (RemoveTimer(i)) i--;   // на место i переехал последний элемент, осмотреть и его
      }
   }
   if (N==0) EventKillTimer();
}
//+------------------------------------------------------------------+
void CTimer::KillTimer(int milliseconds, TFuncTimer fun) {
   for (uint i=0; i<N; i++) {
      if (mt[i].f== fun && mt[i].t==(ulong)milliseconds) {
         if (RemoveTimer(i)) i--;
      }
   }
   if (N==0) EventKillTimer();
}
//+------------------------------------------------------------------+
void CTimer::KillTimer(uint id) {
   for (uint i=0; i<N; i++) {
      if (mt[i].id== id) {
         if (RemoveTimer(i)) i--;
      }
   }
   if (N==0) EventKillTimer();
}
//+------------------------------------------------------------------+
int CTimer::GetLost(int milliseconds, TFuncTimer fun) {
   for (uint i=0; i<N; i++) {
      if (mt[i].f== fun && mt[i].t==(ulong)milliseconds)
         return mt[i].lost>INT_MAX?INT_MAX:int(mt[i].lost);
   }
   return -1;
}
//+------------------------------------------------------------------+
int CTimer::GetNbyStart(uint id) {
   for (uint i=0; i<N; i++) {
      if (mt[i].id== id) return mt[i].n>INT_MAX?INT_MAX:int(mt[i].n);
   }
   return -1;
}
//+------------------------------------------------------------------+


CTimer timers; // создаём единственный объект

//+------------------------------------------------------------------+
void OnTimer() {
   timers.OnTimer();
}
//+------------------------------------------------------------------+
#endif
//+------------------------------------------------------------------+
