fcf.module({
  name: "fcf:NSystem/Cron.js",
  dependencies: [],
  module: function() {
    var NServer = fcf.prepareObject(fcf, "NServer");

    fcf.addException("ERROR_CRON_INCORRECTFORMAT", "Задан не корректный CRON формат ${{string}}$")

    /**
    *@class fcf::NServer::Cron
    *lng_en @brief <b>[server only]</b> A class that implements CRON functionality
    *lng_ru @brief <b>[server only]</b> Класс реализующий функционал CRON
    **/
    NServer.Cron = function() {
      let self = this;
      this._tasks = {};
      this._date = undefined;

      /**
      * @fn void append(string a_taskName, string a_description, string a_cronFrequency, function a_cb)
      *
      *lng_en @brief Adds a CRON job
      *lng_en @param string a_taskName The name of the task
      *lng_en @param string a_cronFrequency Cron call frequency format
      *lng_en @param function a_cb(object a_done) The callback task execution
      *lng_en               - object a_done The object of the completion functions. Has 2 methods:
      *lng_en                 - void complete() The method is called when the callback processing completes successfully
      *lng_en                 - void error(object a_error) The method is called if an error occurs.
      *lng_en                       - object a_error Error object
      *lng_en @code
      *lng_en fcf.application.getCron().append( "TASK NAME",
      *lng_en                                   "0 * * * *",
      *lng_en                                   function(a_done){
      *lng_en                                     ...
      *lng_en                                     if (error){
      *lng_en                                       a_done.error(error);
      *lng_en                                       return;
      *lng_en                                     }
      *lng_en                                     fcf.log.log("Application", "Task on every hour in 00 minutes")
      *lng_en                                     a_done.complete();
      *lng_en                                   }
      *lng_en @endcode
      *
      *lng_ru @brief Добавляет задание CRON
      *lng_ru @param string a_taskName Наименование задачи
      *lng_ru @param string a_cronFrequency Формат частоты вызова CRON
      *lng_ru @param function a_cb(object a_done) Обратный вызов исполнения задачи
      *lng_ru               - object a_done Объект завершения выполнения функции. Имеет 2-а метода:
      *lng_ru                 - void complete() Метод вызывается при успешном завершении обработки обратного вызова
      *lng_ru                 - void error(object a_error) Метод вызывается в случае возникновения ошибки.
      *lng_ru                       - object a_error Объект ошибки
      *lng_ru @code
      *lng_ru fcf.application.getCron().append( "TASK NAME",
      *lng_ru                                   "0 * * * *",
      *lng_ru                                   function(a_done){
      *lng_ru                                     ...
      *lng_ru                                     if (error){
      *lng_ru                                       a_done.error(error);
      *lng_ru                                       return;
      *lng_ru                                     }
      *lng_ru                                     fcf.log.log("Application", "Task on every hour in 00 minutes")
      *lng_ru                                     a_done.complete();
      *lng_ru                                   }
      *lng_ru @endcode
      **/
      this.append = function(a_taskName, a_cronFrequency, a_extendOptions, a_cb){
        //test format a_cronFrequency
        this._equal(a_cronFrequency, new Date());

        if (typeof a_extendOptions == "function"){
          a_cb = a_extendOptions;
          a_extendOptions = {};
        }

        this._tasks[a_taskName] = {
          frequency:    a_cronFrequency,
          cb:           a_cb,
          hidden:       !!a_extendOptions.hidden,
        }
      }


      /**
      * @fn void run()
      *lng_en @brief Runs an object on task execution
      *lng_ru @brief Запускает объект на исполнения задач
      **/
      this.run = function(){
        let timeout = 1000;
        this._date  = new Date(fcf.dateFormat(new Date(), "Y-m-d H:i"));
        function clRound() {
          let date = new Date(fcf.dateFormat(new Date(), "Y-m-d H:i"));
          setTimeout(()=>{ clRound()}, timeout);
          while (self._date <= date){
            self._execute(self._date);
            self._date = new Date(self._date.getTime() + 60000);
          }
        }
        setTimeout(()=>{
          clRound();
        }, 10000)
      }

      this._execute = function(a_date) {
        let self = this;
        let currentTime = fcf.dateFormat(a_date, "Y-m-d H:i");
        let currentDate = new Date(currentTime);
        let lastTime = fcf.application.getSystemVariable("fcf:lastCron");
        if (lastTime == currentTime)
          return;

        function clExecute(a_date){
          fcf.application.getSystemActions(true)
          .asyncEach(self._tasks, (a_taskName, a_info, a_res, a_act)=>{
            let realFrequency = self._equal(a_info.frequency, a_date);
            if (!realFrequency){
              a_act.complete();
              return;
            }
            a_info.frequency = realFrequency;

            fcf.setContext(fcf.application.getSystemContext());

            if (!a_info.hidden)
              fcf.log.log("FCF:CRON", "Execute CRON emulator '" + realFrequency + "' (Action: '" + a_taskName + "')");

            fcf.actions()
            .then(()=>{
              return a_info.cb();
            })
            .catch((a_error)=>{
              fcf.log.err("FCF:CRON", "Failed execution CRON emulator (" + a_taskName + ")", a_error);
            })
            .finally(()=>{
              a_act.complete();
            })
          })
        }

        if (lastTime != "" && fcf.application.getConfiguration().restart) {
          let lastDate = new Date((new Date(lastTime)).getTime() + 60000);
          while (lastDate < currentDate) {
            clExecute(lastDate);
            lastDate = new Date(lastDate.getTime() + 60000);
          }
        }

        fcf.application.getSystemActions(true)
        .then(()=>{
          return fcf.application.setSystemVariable("fcf:lastCron", currentTime);
        })
        clExecute(a_date);
      }

      this._equal = function(a_str, a_dateTime){
        if (typeof a_str !== "string")
          throw new fcf.Exception("ERROR_CRON_INCORRECTFORMAT", {string: a_str} );

        var info = this._convertCronFormat(a_str);

        if (fcf.application.getConfiguration().master && info.mode == "slave")
          return false
        if (!fcf.application.getConfiguration().master && info.mode == "master")
          return false
        if (!this._equalItem(info.items[0], a_dateTime.getMinutes()))
          return false;
        if (!this._equalItem(info.items[1], a_dateTime.getHours()))
          return false;
        if (!this._equalItem(info.items[2], a_dateTime.getDate()))
          return false;
        if (!this._equalItem(info.items[3], a_dateTime.getMonth()+1))
          return false;
        let day = a_dateTime.getDay();
        day = day == 0 ? 7 : day;
        if (!this._equalItem(info.items[4], day))
          return false;

        return this._buildStr(info.items);
      }

      this._equalItem = function(a_subitems, a_value){
        return !!fcf.each(a_subitems, (a_key, a_obj) =>{
          if (a_obj.all)
            return true;
          if (a_obj.div !== undefined && (a_value % a_obj.div) == 0)
            return true;
          if (a_obj.range !== undefined && a_obj.range[0] <= a_value && a_obj.range[1] >= a_value)
            return true;
          if (a_obj.value !== undefined && a_obj.value == a_value)
            return true;
          if (a_obj.current){
            a_obj.current = undefined;
            a_obj.value = a_value;
            return true;
          }
        });
      }

      this._convertCronFormat = function(a_str){
        let rawItems = a_str.split(" ");
        let mode     = "all";
        if (rawItems.length != 5 && rawItems.length != 6)
          throw new fcf.Exception("ERROR_CRON_INCORRECTFORMAT", {string: a_str} );
        if (rawItems.length == 6) {
          mode = rawItems[5] == "master" ? "master" :
                 rawItems[5] == "slave"  ? "slave" :
                                           "all";
          rawItems.pop();
        }

        let items = [];
        let resItem = [];
        fcf.each(rawItems, (a_part, a_items)=>{
          resItem = [];
          fcf.each(a_items.split(","), (a_subkey, a_item)=>{
            let subItem = {all: undefined, current: undefined, div: undefined, range: undefined, value: undefined};
            if (a_item === "*") {
              subItem.all = true;
              resItem.push(subItem);
            } else if (a_item === "?") {
              subItem.current = true;
              resItem.push(subItem);
            } else if (a_item.indexOf("/") !== -1) {
              let value = this._normalize(a_item.split("/")[1], a_part);
              if (isNaN(value))
                throw new fcf.Exception("ERROR_CRON_INCORRECTFORMAT", {string: a_str} );
              subItem.div = value;
              resItem.push(subItem);
            } else if (a_item.indexOf("-") !== -1){
              let values = a_item.split("-");
              let value1 = this._normalize(values[0], a_part);
              let value2 = this._normalize(values[1], a_part);
              if (isNaN(value1) || isNaN(value2))
                throw new fcf.Exception("ERROR_CRON_INCORRECTFORMAT", {string: a_str} );
              subItem.range = [value1, value2];
              resItem.push(subItem);
            } else {
              let value = this._normalize(a_item, a_part);
              if (isNaN(value))
                throw new fcf.Exception("ERROR_CRON_INCORRECTFORMAT", {string: a_str} );
              subItem.value = value;
              resItem.push(subItem);
            }
          });
          items.push(resItem);
        });
        return {items: items, mode: mode};
      }

      this._normalize = function(a_value, a_part){
        if (a_part == 4 && typeof a_value == "string"){
          a_value = a_value.toLowerCase();
          a_value = a_value == "mon" ? 1 :
                    a_value == "tue" ? 2 :
                    a_value == "wed" ? 3 :
                    a_value == "thu" ? 4 :
                    a_value == "fri" ? 5 :
                    a_value == "sat" ? 6 :
                    a_value == "sun" ? 7 :
                                       a_value;
        }

        if (a_part === 4 && a_value == 0)
          a_value = 7;

        if (a_part == 3 && typeof a_value == "string"){
          a_value = a_value.toLowerCase();
          a_value = a_value == "jan"  ? 1 :
                    a_value == "feb"  ? 2 :
                    a_value == "mar"  ? 3 :
                    a_value == "apr"  ? 4 :
                    a_value == "may"  ? 5 :
                    a_value == "jun"  ? 6 :
                    a_value == "jul"  ? 7 :
                    a_value == "aug"  ? 8 :
                    a_value == "sept" ? 9 :
                    a_value == "oct"  ? 10 :
                    a_value == "nov"  ? 11 :
                    a_value == "dec"  ? 12 :
                                        a_value;
        }

        return Number.parseInt(a_value);
      }

      this._buildStr = function(a_cronItems){
        let result = "";
        fcf.each(a_cronItems, (a_subitemskey, a_subitems) => {
          if (a_subitemskey != 0)
            result += " ";
          fcf.each(a_subitems, (a_itemkey, a_item) => {
            if (a_itemkey != 0)
              result += ",";
            result += a_item.all !== undefined    ? "*"             :
                      a_item.div !== undefined    ? "*/"+a_item.div :
                      a_item.range !== undefined  ? a_item.range[0] + "-" + a_item.range[1] :
                                                    a_item.value;
          });
        });
        return result;
      }

    }



    return NServer.Cron;
  }
});
