fcf.module({
  name: "fcf:NClient/Wrapper.js",
  module: function(){
    var NClient = fcf.prepareObject(fcf, "NClient");

    var stUpdatableWrappers = {};
    var stUpdateCounter     = 0;
    var stUpdateActions     = undefined;
    var stArgRecursion = 0;
    var stCallsRecursion = {};

    fcf.NDetails._attachWrapperCounter = {};
    fcf.NDetails._noWrapperChilds = {};

    function insertTemplate(a_dstNode, a_template){
      let lstItems = fcf.select(a_dstNode, "[fcftemplate]");
      let lstId    = a_dstNode.id;
      a_dstNode.outerHTML = a_template.content;
      for(let i = 0; i < lstItems.length; ++i) {
        if (!document.getElementById(lstItems[i].id)) {
          let w = fcf.getWrapper(lstItems[i].id);
          if (w)
            w.destroy();
        }
      }
      if (a_dstNode.id != lstId){
        let w = fcf.getWrapper(lstId);
        if (w)
          w.destroy();
      }
    }

    function fillExternalRef(a_dst, a_rootId, a_id){
      let srcArgs = fcf.application.getLocalData().getSourceObject(a_id);
      if (srcArgs) {
        let objectValues = fcf.application.getLocalData().getObject(a_id);
        let parentWrapper   = fcf.getWrapper(a_id).getParent();
        let refParentObjectValues = parentWrapper ? fcf.application.getLocalData().getObject(parentWrapper.getId()) : undefined;

        if (Array.isArray(srcArgs.fcfReferences)){
          for(let i = 0; i < srcArgs.fcfReferences.length; ++i){
            let refId   = fcf.tokenize(srcArgs.fcfReferences[i][0], {args: objectValues, parent: refParentObjectValues});
            let refArg  = fcf.tokenize(srcArgs.fcfReferences[i][1], {args: objectValues, parent: refParentObjectValues});
            if (!a_dst[refId])
              a_dst[refId] = {};

            let refDst = fcf.resolveEx(a_dst[refId], refArg, true);
            let wrp = fcf.getWrapper(refId);
            if (wrp)
              refDst.object[refDst.key] = wrp.getArg(refArg);
          }
        }

        for(let key in srcArgs){
          if (!fcf.isArg(srcArgs[key]) || srcArgs[key].type !== "reference")
            continue;

          let refId   = fcf.tokenize(srcArgs[key].object.id, {args: objectValues, parent: refParentObjectValues});
          let refArg  = fcf.tokenize(srcArgs[key].arg, {args: objectValues, parent: refParentObjectValues});
          let isInner = fcf.getWrapper(a_rootId).select("[id="+refId+"]").length != 0;
          if (isInner)
            continue;

          if (!a_dst[refId])
            a_dst[refId] = {};

          let refDst = fcf.resolveEx(a_dst[refId], refArg, true);
          let wrp = fcf.getWrapper(refId);
          if (wrp)
            refDst.object[refDst.key] = wrp.getArg(refArg);
          else
            refDst.object[refDst.key] = undefined;
        }
      }
    }

    function recalculate(a_id){
      let wrapper    = fcf.getWrapper(a_id);
      let theme      = fcf.application.getTheme(wrapper.getArg("fcfTheme"));
      let sources    = fcf.application.getLocalData().getSourceObject(a_id);
      let modifyKeys = fcf.application.getLocalData().getModifyKeys(a_id);
      if (!modifyKeys)
        modifyKeys = {};
      let depsKeys = {};

      for(let key in sources){
        if (!fcf.isArg(sources[key]) || sources[key].type != "value")
          continue;
        if (key in modifyKeys)
          continue;
        depsKeys[key] = {};
        let deps = fcf.getVariablesString(sources[key].value);
        for(let i = 0; i < deps.length; ++i)
          if (!depsKeys[key][deps[i]])
            depsKeys[key][deps[i]] = fcf.parseObjectAddress(deps[i]);
      }
      let orderKeys = [];
      let processedKeys = {};
      while(true) {
        let fill = true;
        for(let key in depsKeys) {
          let free = true;
          for(let dep in depsKeys[key]){
            let depArr = depsKeys[key][dep];
            if (depArr[0] != "args")
              continue;
            if (depArr[1] in depsKeys){
              free = false;
              break;
            }
          }
          if (free) {
            fill = false;
            orderKeys.push(key);
            delete depsKeys[key];
          }
        }
        if (fill){
          for(let key in depsKeys)
            orderKeys.push(key);
          break;
        }
      }

      if (!orderKeys.length)
        return;

      let tokenizeData = {
        args:   wrapper.getArgs(),
        route:  fcf.getContext().route,
        decor:  theme.getDecor(),
      };

      for(let i =0; i < orderKeys.length; ++i){
        let key = orderKeys[i];
        if (fcf.isArg(sources[key]) && sources[key].type === "value"){
          tokenizeData.args[key] = fcf.tokenize(sources[key].value, tokenizeData);
          fcf.application.getLocalData().setItem(a_id, key, tokenizeData.args[key], true);
        }
      }
    }

    function rerender(a_id, a_template, a_theme, a_args, a_restoreState, a_resotreOriginState, a_url){
      let externalRefValues = {};
      let wrapper = fcf.getWrapper(a_id);

      var modifyChildArgs = {
        childs: {},
        args: {},
      };
      if (wrapper) {
        fillExternalRef(externalRefValues, a_id, a_id);
        var childs = wrapper.getChilds();
        function clFillChilds(a_wrapper, a_modifyChildArgs, a_ignoreFillData){
          var fcfSaveChildsState = a_wrapper.getArg("fcfSaveChildsState");
          if (fcfSaveChildsState === false || (!a_restoreState && !a_resotreOriginState))
            a_ignoreFillData = true;

          fcf.each(a_wrapper.getChilds(), function(a_key, a_child){
            var fcfCallPosition = a_child.getArg("fcfCP");
            var id = a_child.getId();
            var modifyKeys = fcf.application.getLocalData().getModifyKeys(id);
            var args = {};

            let clientRendering    = a_child.getOptions().clientRendering;
            if (clientRendering){
              let clientRenderingArg = a_child.getArg("fcfClientRendering");
              if (clientRenderingArg !== undefined)
                clientRendering = clientRenderingArg;
            }

            fillExternalRef(externalRefValues, a_id, id);

            if (!a_ignoreFillData && a_restoreState && !a_resotreOriginState) {
              let sources = fcf.application.getLocalData().getOriginSourceObject(id);
              for(var key in sources) {
                if (modifyKeys && key in modifyKeys)
                  args[key] = sources[key];
                if (clientRendering === "update_np" && fcf.isArg(sources[key]) && sources[key].type === "programmable")
                  args[key] = a_child.getArg(key);
              }
              for(var key in modifyKeys){
                let isArg = fcf.isArg(sources[key]);
                let type  = isArg ? sources[key].type : undefined;
                if(isArg && (type == "url" || type == "reference" || (type != "programmable" && sources[key].important)))
                  continue;
                args[key] = a_child.getArg(key);
              }
            } else if (!a_ignoreFillData && a_resotreOriginState){
              let sources  = fcf.application.getLocalData().getOriginSourceObject(id);
              let origArgs = fcf.application.getLocalData().getOriginObject(id);
              for(let key in origArgs){
                let type = fcf.isArg(sources[key]) ? sources[key].type : undefined;
                if(type && (type == "url" || type == "reference"))
                  args[key] = sources[key];
                else
                  args[key] = origArgs[key];
              }
            }

            if (!a_ignoreFillData) {
              args["fcfId"] = a_child.getId();
              var fcfevntid = a_child.getDomElement().getAttribute("fcfevntid");
              if (fcfevntid)
                args["fcfEvntid"] = fcfevntid;

              a_modifyChildArgs.childs[fcfCallPosition] = {
                childs: {},
                args: args,
              }
            }

            clFillChilds(a_child, a_modifyChildArgs && a_modifyChildArgs.childs ? a_modifyChildArgs.childs[fcfCallPosition]: {}, a_ignoreFillData);
          });
        }
        clFillChilds(wrapper, modifyChildArgs);
        a_args.fcfChildsArgs = modifyChildArgs;
      }
      return fcf.application.render({
        template:     a_template,
        theme:        a_theme,
        owner:        false,
        args:         a_args,
        update:       true,
        wrapper:      false,
        url:          a_url,
        externalArgs: externalRefValues,
      });
    }

    function stUpdateWrapper(a_info) {

      if (a_info.calcOnly){
        return rerender(a_info.id, a_info.template, a_info.theme, a_info.args, a_info.restoreState, a_info.restoreOriginState, a_info.url)
        .then((a_template)=>{
          a_info.cb(undefined, a_template, true);
        })
        .catch((a_error) => {
          a_info.cb(a_error, undefined, true);
        })
      }

      if (!stUpdateActions)
        stUpdateActions = fcf.actions();

      ++stUpdateCounter;
      var updateCounterLast = stUpdateCounter;
      stUpdatableWrappers[a_info.id] = a_info;

      function clUpdate() {
        if (updateCounterLast != stUpdateCounter){
          updateCounterLast = stUpdateCounter;
          setTimeout(clUpdate, 1);
          return;
        }

        var wrappers = stUpdatableWrappers;
        stUpdatableWrappers = {};

        var rm = {};
        function clFilter(a_ownerId, a_wrapper) {
          var chlids = a_wrapper.getChilds();
          for(var k in chlids) {
            var id = chlids[k].getId();
            if (id in wrappers){
              if (wrappers[a_ownerId]) {
                fcf.append(wrappers[a_ownerId].cbs, wrappers[id].cbs);
                if (!wrappers[a_ownerId].url && wrappers[id].url)
                  wrappers[a_ownerId].url = wrappers[id].url;
                wrappers[a_ownerId].cbs.push(wrappers[id].cb);
              }
              rm[id] = true;
            }
            clFilter(a_ownerId, chlids[k]);
          }
        }
        for(var id in wrappers){
          var wrapper = fcf.getWrapper(id);
          if (!wrapper){
            rm[id] = true;
            continue;
          }
          clFilter(id, wrapper);
        }
        for(var id in rm)
          delete wrappers[id];

        updateCounterLast = 0;


        fcf.each(wrappers, function(rootId, info){
          stUpdateActions.then((a_res, a_act)=>{
            fcf.actions()
            .then(()=>{
              let wrapper = fcf.getWrapper(rootId);
              wrapper.lock();
              return rerender(rootId, info.template, info.theme, info.args, info.restoreState, info.restoreOriginState, info.url)
              .then((a_template)=>{
                wrapper.unlock();
                info.cb(undefined, a_template, true);
                for(let i = 0; i < info.cbs.length; ++i) {
                  info.cbs[i](
                    undefined,
                    {
                      state: {
                        page: a_template && a_template.state ? a_template.state.page : { header: {} },
                      },
                      route: a_template.route,
                    },
                    false);
                }
              })
            })
            .catch((a_error)=>{
              fcf.log.err("FCF:RENDER", a_error);
              wrapper.unlock();
              info.cb(a_error, undefined, true);
              for(let i = 0; i < info.cbs.length; ++i) {
                info.cbs[i](
                  a_error,
                  undefined,
                  false);
              }
            })
            .finally(()=>{
              a_act.complete();
            })
          });
        });
      }

      setTimeout(clUpdate, 0);
    }

    var stReferences = {};

    function stAttachReferences(a_wrapper){
      let id = a_wrapper.getId();
      let srcArgs = fcf.application.getLocalData().getSourceObject(id);
      let rmMap = {};
      let args = {};

      for(let name in srcArgs) {
        let arg = srcArgs[name];
        if (fcf.isArg(arg) && arg.type == "reference") {
          if (arg.object.id.indexOf("${{") !== -1 || arg.object.id.indexOf("@{{")  !== -1 || arg.arg.indexOf("${{")  !== -1 || arg.arg.indexOf("@{{")  !== -1 )
            arg = fcf.tokenize(arg, {args: fcf.application.getLocalData().getObject(a_wrapper._id), route: fcf.getContext().route})

          args[name] = arg;

          if (stReferences[arg.object.id]) {
            if (!rmMap[arg.object.id] || !rmMap[arg.object.id][id]) {
              if (!rmMap[arg.object.id])
                rmMap[arg.object.id] = {};
              rmMap[arg.object.id][id] = true;
              for(let an in stReferences[arg.object.id]) {
                if (stReferences[arg.object.id][an][id]) {
                  delete stReferences[arg.object.id][an][id];
                }
              }
            }
          }
        }
      }

      for(let name in args) {
        let arg = args[name];
        let argPath = fcf.normalizeObjectAddress(arg.arg);
        let argName = fcf.parseObjectAddress(arg.arg)[0];
        if (!stReferences[arg.object.id])
          stReferences[arg.object.id] = {};
        if (!stReferences[arg.object.id][argName])
          stReferences[arg.object.id][argName] = {};
        if (!stReferences[arg.object.id][argName][id])
          stReferences[arg.object.id][argName][id] = {};
        stReferences[arg.object.id][argName][id][name] = argPath;
      }
    }


    class Wrapper {
      constructor(a_initializeOptions, a_ignoreReg){
        this._id                   = a_initializeOptions.domElement
                                       ? a_initializeOptions.domElement.getAttribute("id")
                                       : undefined;
        this._initializeOptions    = a_initializeOptions;
        this._callPositionsSetArg  = {};
        this._callPositionsCounter = 0;
        this._destroy              = false;

        if (!a_ignoreReg)
          fcf.NDetails._wrappers[this._id] = this;

        let element = this.getDomElement();
        if (element && !element.getAttribute("fcfparent")){
          let parent = fcf.getWrapper(element.parentNode);
          if (parent){
            element.setAttribute("fcfparent", parent.getId());
            this.setArg("fcfParent", parent.getId(), true);
          }
        }
      }

      async initialize() {
        stAttachReferences(this);
        await fcf.liven(undefined, this._id);
        this._attachInnerEvents();
        await this.attach();
      }

      async _reattach() {
        let self = this;
        fcf.removeDomListener(this);

        let element = this.getDomElement();
        if (element && !element.getAttribute("fcfparent")){
          let parent = fcf.getWrapper(element.parentNode);
          if (parent){
            element.setAttribute("fcfparent", parent.getId());
            this.setArg("fcfParent", parent.getId(), true);
          }
        }

        stAttachReferences(this);

        try {
          await fcf.liven(undefined, this._id);
          this._attachInnerEvents();
          await self.attach();
        } catch(e){
          fcf.log.err("FCF", "Failed reattach: ", e);
          throw e;
        }
      }

      recalculate(){
        recalculate(this.getId());
      }

      update(a_options) {
        let self = this;
        if (!this.getDomElement())
          return fcf.actions().then(()=>{ return { page: { header: {} } }; });
        var alias      = this.getDomElement().getAttribute("fcfalias");
        var template   = this.getDomElement().getAttribute("fcftemplate");
        var parentId   = this.getDomElement().getAttribute("fcfparent");
        if (parentId === null)
          parentId = undefined;
        var id         = this.getDomElement().getAttribute("id");
        var args       = fcf.append({}, fcf.application.getLocalData().getSourceObject(id));

        let clientRendering = this.getOptions().clientRendering;
        if (clientRendering) {
          let clientRenderingArg = this.getArg("fcfClientRendering");
          if (clientRenderingArg !== undefined)
            clientRendering = clientRenderingArg;
        }
        if (clientRendering === "update_np")
          for (let key in args)
            if (fcf.isArg(args[key]) && args[key].type === "programmable")
              args[key] = fcf.application.getLocalData().getItem(id, key);

        fcf.each(fcf.application.getLocalData().getModifyKeys(id), function(a_key){
          let srcArg = fcf.application.getLocalData().getSourceItem(id, a_key);
          if (fcf.isArg(srcArg) && srcArg.type == "url")
            return;
          args[a_key] = fcf.application.getLocalData().getItem(id, a_key);
        });

        if (!fcf.empty(alias))
          args.fcfAlias = alias;
        args.fcfId = id;
        args.fcfParent = parentId;
        if (this.getDomElement().getAttribute("fcfevntid"))
          args.fcfEvntid = this.getDomElement().getAttribute("fcfevntid");

        return fcf.actions()
        .then((a_res, a_act)=>{
          var updateInfo = {
            id:                 self.getId(),
            template:           template,
            args:               args,
            restoreState:       true,
            restoreOriginState: false,
            url:                a_options && a_options.url,
            calcOnly:           a_options && a_options.calcOnly,
            cbs:                [],
            cb: function(a_error, a_template, a_rootCallback){
              if (a_error){
                a_act.error(a_error);
                return;
              }

              if (!a_rootCallback || (a_options && a_options.calcOnly)){
                a_act.complete({content: a_template.content, route: a_template.route, page: a_template.state.page});
                return;
              }

              var domElement = self.getDomElement();
              if (!domElement){
                a_act.complete({content: a_template.content, route: a_template.route, page: a_template.state.page});
                return;
              }

              let activeElement = document.activeElement;
              let activeElementTag = activeElement ? activeElement.tagName : undefined;
              let wrapper = fcf.getWrapper(activeElement);
              let wrapperId = wrapper ? wrapper.getId() : undefined;
              let focusElementNumber = -1;
              let selectionStart = 0;

              if (wrapperId == self.getId() || self.select("[id='" + wrapperId + "']").length){
                selectionStart = activeElement.selectionEnd;
                let elements = activeElementTag ? self.select(activeElementTag) : [];
                for(let i = 0; i < elements.length; ++i) {
                  ++focusElementNumber;
                  if (elements[i] == activeElement)
                    break;
                }
              }

              insertTemplate(domElement, a_template);

              if (focusElementNumber != -1) {
                let elements = activeElementTag ? self.select(activeElementTag) : undefined;
                for(let i = 0; i < elements.length; ++i) {
                  if (i == focusElementNumber){
                    if (!fcf.empty(selectionStart))
                      elements[i].selectionStart = selectionStart;
                    elements[i].focus();
                    break;
                  }
                }
              }

              fcf.liven(domElement)
              .then(()=>{
                fcf.application.getEventChannel().send("render", {element: self.getDomElement()});
              })
              .then(()=>{
                a_act.complete({content: a_template.content, route: a_template.route, page: a_template.state.page});
              })
            }
          };
          stUpdateWrapper(updateInfo);
        });
      }

      refresh(a_options) {
        let self = this;
        if (!this.getDomElement())
          return fcf.actions().then(()=>{ return { page: { header: {} } }; });

        let alias      = this.getDomElement().getAttribute("fcfalias");
        let template   = this.getDomElement().getAttribute("fcftemplate");
        let parentId   = this.getDomElement().getAttribute("fcfparent");
        if (parentId === null)
          parentId = undefined;
        let id         = this.getDomElement().getAttribute("id");
        let args       = {};
        let sources    = fcf.application.getLocalData().getOriginSourceObject(id);
        let originSourcesKeys = fcf.application.getLocalData().getOriginSourcesKeys(id);
        fcf.each(originSourcesKeys, (i, k)=>{
          args[k] = sources[k];
        });

        let originData = fcf.application.getLocalData().getOriginObject(this.getId());
        for(let key in originData)
          args[key] = originData[key];

        if (!fcf.empty(alias))
          args.fcfAlias = alias;
        args.fcfId = id;
        args.fcfParent = parentId;
        if (this.getDomElement().getAttribute("fcfevntid"))
          args.fcfEvntid = this.getDomElement().getAttribute("fcfevntid");

        return fcf.actions()
        .then((a_res, a_act)=>{
          let updateInfo = {
            id:                 self.getId(),
            template:           template,
            args:               args,
            restoreState:       false,
            restoreOriginState: true,
            url:                a_options && a_options.url,
            cbs:                [],
            cb: function(a_error, a_template, a_rootCallback){
              if (!a_rootCallback){
                a_act.complete({page: a_template.state.page, route: a_template.route});
                return;
              }


              let domElement = self.getDomElement();
              if (!domElement){
                a_act.complete({page: a_template.state.page, route: a_template.route});
                return;
              }

              insertTemplate(domElement, a_template);

              fcf.liven(domElement)
              .then(()=>{
                fcf.application.getEventChannel().send("render", {element: self.getDomElement()});
              })
              .then(()=>{
                a_act.complete({page: a_template.state.page, route: a_template.route});
              });
            }
          };
          stUpdateWrapper(updateInfo);
        });
      }

      reload(a_options) {
        let self = this;
        if (!this.getDomElement())
          return fcf.actions().then(()=>{ return { page: { header: {} } }; });

        let alias      = this.getDomElement().getAttribute("fcfalias");
        let template   = this.getDomElement().getAttribute("fcftemplate");
        let parentId   = this.getDomElement().getAttribute("fcfparent");
        if (parentId === null)
          parentId = undefined;
        let id         = this.getDomElement().getAttribute("id");
        let args       = {};
        let sources    = fcf.application.getLocalData().getOriginSourceObject(id);
        let originSourcesKeys = fcf.application.getLocalData().getOriginSourcesKeys(id);
        fcf.each(originSourcesKeys, (i, k)=>{
          args[k] = sources[k];
        });
        if (!fcf.empty(alias))
          args.fcfAlias = alias;
        args.fcfId = id;
        args.fcfParent = parentId;
        if (this.getDomElement().getAttribute("fcfevntid"))
          args.fcfEvntid = this.getDomElement().getAttribute("fcfevntid");

        return fcf.actions()
        .then((a_res, a_act)=>{
          let updateInfo = {
            id:                 self.getId(),
            template:           template,
            args:               args,
            update:             true,
            restoreState:       false,
            restoreOriginState: false,
            url:                a_options && a_options.url,
            cbs:                [],
            cb: function(a_error, a_template, a_rootCallback){
              if (!a_rootCallback){
                a_act.complete({page: a_template.state.page, route: a_template.route});
                return;
              }

              let domElement = self.getDomElement();
              if (!domElement){
                a_act.complete({page: a_template.state.page, route: a_template.route});
                return;
              }

              insertTemplate(domElement, a_template);

              fcf.liven(domElement)
              .then(()=>{
                return fcf.application.getEventChannel().send("render", {element: self.getDomElement()});
              })
              .then(()=>{
                a_act.complete({page: a_template.state.page, route: a_template.route});
              });
            }
          };
          stUpdateWrapper(updateInfo);
        });
      }

      destroy(){
        if (this._destroy)
          return;
        this._destroy = true;
        if (typeof this.onDestroy === "function")
          this.onDestroy();
        fcf.removeDomListener(this);

        fcf.each(this.getChilds(), (a_key, a_child)=>{
          a_child.destroy();
        })

        var de = this.getDomElement();
        if (de)
          de.parentElement.removeChild(de);
        let id = this.getId();
        delete stReferences[id];
        delete fcf.NDetails._rwrappers[id];
        delete fcf.NDetails._wrappers[id];

        fcf.each(fcf.NDetails._noWrapperChilds[this.getId()], (a_childId)=>{
          delete stReferences[a_childId];
          fcf.application.getLocalData().removeObject(a_childId);
        })

        fcf.application.getLocalData().removeObject(id);
      }

      emit(a_event, a_exData){
        if (typeof a_event == "string"){
          let event = document.createEvent('Event');
          event.initEvent(a_event, true, true);
          event.name = a_event;
          if (!event.type)
            event.type = a_event;
          a_event = event;
        }
        if (a_exData)
          fcf.append(a_event, a_exData);
        return fcf.emitDomEvent(this.getActionDomElement(), a_event);
      }

      send(a_data, a_inputFiles, a_async, a_cb){
        let self = this;

        if (typeof a_inputFiles === "boolean"){
          a_cb = a_async;
          a_async = a_inputFiles;

        }

        if (typeof a_inputFiles === "function"){
          a_cb = a_inputFiles;
          a_inputFiles = undefined;
          a_async = true;
        }

        if (typeof a_async === "function"){
          a_cb = a_async;
          a_async = true;
        }

        return fcf.actions().then((a_res, a_act) => {
          const formData = new FormData();



          var filesAttributes = [];
          var counter = 0;
          fcf.each(a_inputFiles, function(k, input){
            let files = input instanceof File                                         ? [input] :
                        input instanceof HTMLElement && fcf.isEnumerable(input.files) ? input.files :
                                                                                        [];
            for(var i = 0; i < files.length; ++i){
              formData.append('files[' + (counter++) + ']', files[i]);
              var attributes = {};
              if (input instanceof HTMLElement)
                fcf.each(input.attributes, function(k, attr){ attributes[attr.name] = attr.value; });
              filesAttributes.push(attributes);
            }
          });

          function processFileVariables(a_object, a_path){
            for(let key in a_object) {
              if (typeof a_object !== "object" || !a_object)
                continue;
              let files = undefined;
              if (a_object[key] instanceof File)
                files = [a_object[key]];
              else if (a_object[key] instanceof HTMLElement)
                files = a_object[key].files;

              if (files) {
                let strPath = "";
                let path = [...a_path,key];
                for(let i= 0; i < path.length; ++i)
                  strPath += '["' + path[i] + '"]';

                for(var i = 0; i < files.length; ++i){
                  formData.append('files[' + (counter++) + ']', files[i]);
                  var attributes = {};
                  if (a_object[key] instanceof HTMLElement)
                    fcf.each(a_object[key].attributes, function(k, attr){ attributes[attr.name] = attr.value; });
                  attributes.objectPath = strPath;
                  filesAttributes.push(attributes);
                }
              } else {
                let newPath = [...a_path, key];
                processFileVariables(a_object[key], newPath);
              }
            }
          }
          processFileVariables(a_data, []);

          formData.append('filesAttributes', JSON.stringify(filesAttributes));
          formData.append('data', fcf.str(JSON.stringify(a_data)));

          fcf.load({
            path: "@url:fcfReceive?template=" + encodeURIComponent(self.getTemplate()),
            post: formData,
            async: a_async,
            onResult: function(a_error, a_response){
              if (a_cb)
                a_cb(a_error, a_response);
              if (a_error) {
                fcf.application.getEventChannel().send("error", {action: "method_send", error: a_error});
                a_act.error(a_error);
              } else {
                try {
                  a_response = a_response == "undefined" ? undefined : JSON.parse(a_response);
                } catch(e){ }
                a_act.complete(a_response);
              }
            }
          })
        });
      };

      getTemplate(){
        return this.getDomElement().getAttribute("fcftemplate");
      }

      getId(){
        return this._id;
      }

      getAlias() {
        return this.getDomElement().getAttribute("fcfalias");
      }

      getDomElement(){
        return this != fcf.application.getRootWrapper() ? document.getElementById(this._id) : document.body;
      }

      getActionDomElement(){
        return this.getDomElement();
      }

      getParent(){
        if (!this.getDomElement())
          return;
        var parentId = this.getDomElement().getAttribute("fcfparent");
        if (fcf.empty(parentId))
          return this != fcf.application.getRootWrapper() ? fcf.application.getRootWrapper() : undefined;
        return fcf.getWrapper(parentId);
      }

      getChilds() {
        var domChilds = fcf.select(document.getElementById(this._id), "[fcfparent=" + this._id + "]");
        var result = [];
        for(var i = 0; i < domChilds.length; ++i) {
          var wrapper = fcf.NDetails._wrappers[domChilds[i].getAttribute("id")];
          if (wrapper)
            result.push(wrapper);
        }
        return result;
      }

      getFirstChild() {
        var domChilds = fcf.select(document.getElementById(this._id), "[fcfparent=" + this._id + "]");
        for(var i = 0; i < domChilds.length; ++i) {
          var wrapper = fcf.NDetails._wrappers[domChilds[i].getAttribute("id")];
          return wrapper;
        }
        return;
      }

      getChild(a_alias) {
        var domChild = fcf.first(fcf.select(document.body, "[fcfparent=\"" + this._id + "\"][fcfalias=\"" + fcf.encodeHtml(a_alias) + "\"]"));
        if (!domChild)
          return;
        return fcf.NDetails._wrappers[domChild.getAttribute("id")];
      }

      isChild(a_wrapper){
        a_wrapper = a_wrapper.getParent();
        while(a_wrapper) {
          if (a_wrapper == this)
            return true;
          a_wrapper = a_wrapper.getParent();
        }
        return false;
      }

      isOwn(a_wrapper){
        return this.isChild(a_wrapper) || a_wrapper === this;
      }

      select(a_selector){
        return fcf.select(`#${this._id} ${a_selector}`);
      }

      processWritenValue(a_valuesInfo, a_defaultLanguage){
        if (this.getArg("translate"))
          return;
        for(let lang in a_valuesInfo)
          if (lang != a_defaultLanguage)
            a_valuesInfo[lang].ignore = true;
      }

      getOptions(){
        let info = fcf.NDetails.renderInstructions[this.getTemplate()];
        return info && info.options ? info.options : {};
      }

      getArg(a_key){
        return fcf.application.getLocalData().getItem(this._id, a_key);
      }

      getArgs() {
        let self = this;
        let result = {};
        fcf.each(fcf.application.getLocalData().getSourceObject(this._id), (a_key)=>{
          result[a_key] = self.getArg(a_key);
        });
        fcf.each(fcf.application.getLocalData().getModifyKeys(this._id), (a_index, a_key)=>{
          if (!(a_key in result))
            result[a_key] = self.getArg(a_key);
        });
        return result;
      }

      getShortArgs() {
        return fcf.application.getLocalData().getObject(this._id);
      }

      setArg(a_key, a_value, a_ignoreCallbacks, a_ignoreRedrawing, a_innerCall, a_editor, a_ignoreURLArgs){
        if (!a_editor)
          a_editor = this;
        let callPosition = a_key + ":" + this.getId();
        this._setArgImpl(a_key, a_value, a_ignoreCallbacks, a_ignoreRedrawing, a_innerCall, callPosition, a_editor, a_ignoreURLArgs);
      }

      validate(a_value, a_options, a_dsterros) {
        let childs = this.getChilds();
        for (let i = 0; i < childs.length; ++i)
          childs[i].validate(typeof a_value == "object" ? a_value[childs[i].getAlias()] : undefined, a_options, a_dsterros);
      }

      check() {
        let value = this.getArg("value");
        let errors = [];
        this.validate(value, {}, errors);
        return errors;
      }

      async lock(){
        let uuid = fcf.uuid();
        this._wrapperLocker = fcf.locker({selector: "#" + this.getId(), class: "fcf-update"});
        this._wrapperLocker.uuid = uuid;
        let div    = document.createElement("div");
        div.innerHTML = "&nbsp;";
        this._wrapperLocker.insertBefore(div, this._wrapperLocker.firstChild);
        let rectb = this._wrapperLocker.getBoundingClientRect();
        if (rectb.y < 0){
          div.style.top = "0px";
          div.style.position = "sticky";
        }
        let wltp = this.getArg("fcfLockTemplate") === true                                                 ? "@controls:lock" :
                   this.getArg("fcfLockTemplate") !== undefined                                             ? this.getArg("fcfLockTemplate") :
                   this.getOptions().lockTemplate !== true && this.getOptions().lockTemplate !== undefined  ? this.getOptions().lockTemplate :
                                                                                                              "@controls:lock";
        if (wltp) {
          let template = await fcf.application.render({template: wltp});
          if (this._wrapperLocker && this._wrapperLocker.uuid == uuid){
            div.innerHTML = template.content;
            fcf.liven(div);
          }
        }

      }

      unlock(){
        if (this._wrapperLocker) {
          this._wrapperLocker.remove();
          this._wrapperLocker = undefined;
        }
      }

      attach(){
        let self = this;
        let scripts = [];


        if (!fcf.application.isRun() || fcf.application.getRootWrapper() === this)
          return fcf.actions();

        if (!(this.getId() in fcf.NDetails._attachWrapperCounter))
          fcf.NDetails._attachWrapperCounter[this.getId()] = 1;
        else
          ++fcf.NDetails._attachWrapperCounter[this.getId()];
        let currentAttachCounter = fcf.NDetails._attachWrapperCounter[this.getId()];
        fcf.each(fcf.NDetails._noWrapperChilds[this.getId()], (a_childId, a_attachCounter)=>{
          if (a_attachCounter+1 < currentAttachCounter){
            delete stReferences[a_childId];
            fcf.application.getLocalData().removeObject(a_childId);
          }
        })


        fcf.each(this.select("script"), (a_key, a_script)=>{
          if (fcf.getWrapper(a_script) == self)
            scripts.push(a_script);
        });

        fcf.actions().asyncEach(scripts, async (a_key, a_script, a_res, a_act)=>{
          try {
            if (!fcf.empty(a_script.src)){
              const script = document.createElement('script');
              script.src = a_script.src;
              script.addEventListener('load', ()=>{
                a_act.complete();
              });
              script.addEventListener('error', (a_error)=>{
                a_act.complete();
                fcf.log.err("FCF", new fcf.Exception("ERROR", {error: "Failed execution script tag"}, a_error));
              });
              document.head.appendChild(script);
            } else {
              eval(a_script.innerHTML);
            }
          } catch(error){
            a_act.complete();
            fcf.log.err("FCF", new fcf.Exception("ERROR", {error: "Failed execution script tag"}, error))
          }
        });

        return fcf.actions();
      }

      onArg(a_argName, a_value, a_editor, a_ignoreRedrawing, a_isInnerCall, a_suffix) {
        let autoUpdate = this.getArg("fcfAutoUpdate");
        if (autoUpdate === undefined)
          autoUpdate = this.getOptions().autoUpdate;

        if (a_ignoreRedrawing){
          this.recalculate();
          return;
        }

        if ((autoUpdate === true || autoUpdate === "all") ||
            (autoUpdate === "external" && !this.isChild(a_editor)) ) {
          if (a_editor !== this || !a_isInnerCall)
            this.update();
          else
            this.recalculate();
        } else {
          this.recalculate();
        }
      }


      _getOriginArgs() {
        let self = this;
        let result = {};
        fcf.each(fcf.application.getLocalData().getSourceObject(this._id), (a_key)=>{
          result[a_key] = self._getOriginArg(a_key);
        });
        return result;
      }

      _getOriginArg(a_key){
        return fcf.application.getLocalData().getOriginItem(this._id, a_key);
      }

      _setArg(a_key, a_value, a_ignoreCallbacks, a_ignoreRedrawing){
        this.setArg(a_key, a_value, a_ignoreCallbacks, a_ignoreRedrawing, true, this);
      }

      _setArgImpl(a_key, a_value, a_ignoreCallbacks, a_ignoreRedrawing, a_innerCall, a_callPosition, a_editor, a_ignoreURLArgs) {
        let self = this;

        if (a_callPosition in this._callPositionsSetArg){
          fcf.application.getLocalData().setItem(this._id, a_key, a_value);
          return;
        }
        ++this._callPositionsCounter;
        this._callPositionsSetArg[a_callPosition] = true


        fcf.application.getLocalData().setItem(this._id, a_key, a_value);

        if (a_ignoreCallbacks){
          --this._callPositionsCounter;
          if (!this._callPositionsCounter)
            this._callPositionsSetArg = {};
          return;
        }

        let eventMap = {};

        let clGetArgInfo = (a_id, a_key) => {
          let parts = fcf.parseObjectAddress(a_key);
          let name = parts[0];
          let suffix = "";
          let source = fcf.application.getLocalData().getSourceItem(a_id, name);
          for(var i = 1; i < parts.length; ++i)
            suffix += '["' + parts[i] + '"]';

          if (fcf.isArg(source) && source.type == "reference"){
            if (source.object.id.indexOf("${{") !== -1 || source.object.id.indexOf("@{{")  !== -1 || source.arg.indexOf("${{")  !== -1 || source.arg.indexOf("@{{")  !== -1 ){
              source = fcf.tokenize(source, {args: fcf.application.getLocalData().getObject(a_id), route: fcf.getContext().route})
            }
          }

          return {
            path:   fcf.normalizeObjectAddress(a_key),
            arg:    name,
            suffix: suffix,
            id:     a_id,
            source: source,
            requestPath: "",
          };
        }


        let clProcessLink = (a_id, a_key, a_value, a_req, a_eventMap, a_protectDublicattes, a_isRoot, a_upDirection) => {
          a_key = fcf.normalizeObjectAddress(a_key);
          if (a_req[a_id + " -> " + a_key])
            return;
          a_req[a_id + " -> " + a_key] = true;

          let argInfo = clGetArgInfo(a_id, a_key);

          let duplicate = false;
          if ((a_id + argInfo.arg) in a_protectDublicattes) {
            let oldSuffixLength = fcf.parseObjectAddress(a_protectDublicattes[a_id + argInfo.arg]).length;
            let newSuffixLength = fcf.parseObjectAddress(argInfo.suffix).length;
            if (newSuffixLength >= oldSuffixLength)
              duplicate = true;
          }

          if (!duplicate) {
            a_protectDublicattes[a_id + argInfo.arg] = argInfo.suffix;
            let callInfo = {
              id:  a_id,
              arg: argInfo.arg,
              suffix: argInfo.suffix,
              value: fcf.application.getLocalData().getItem(a_id, argInfo.arg),
            };
            a_eventMap[a_id + "->"+ a_key] = callInfo;
            if(!a_ignoreURLArgs && fcf.isArg(argInfo.source) && argInfo.source.type == "url"){
              fcf.application.setUrlArg(argInfo.source.arg, callInfo.value, fcf.getWrapper(callInfo.id));
            }
          }

          //going down on graph
          if (stReferences[a_id] && stReferences[a_id][argInfo.arg]){
            let refsInfo = stReferences[a_id][argInfo.arg];
            for(let childId in refsInfo) {
              for(let childArgPath in refsInfo[childId]) {
                let refChlidsPath = refsInfo[childId][childArgPath];
                if (argInfo.path.indexOf(refChlidsPath) == 0 || refChlidsPath.indexOf(argInfo.path) == 0){
                  let child = fcf.application.getLocalData().getSourceItem(childId, childArgPath);
                  if (!child)
                    continue;
                  let childArg = fcf.tokenize(child.arg, {args: fcf.application.getLocalData().getObject(a_id), route: fcf.getContext().route})
                  let childParts = fcf.parseObjectAddress(childArg);
                  childParts.shift()
                  let ownerParts = fcf.parseObjectAddress(argInfo.suffix);
                  let newSuffixParts = [];
                  let fullCopy = true;
                  for(let i = 0; i < ownerParts.length; ++i){
                    if (i >= childParts.length)
                      newSuffixParts.push(ownerParts[i])
                  }
                  let newSuffix = "";
                  for(var i = 0; i < newSuffixParts.length; ++i)
                    newSuffix += "[\"" + newSuffixParts[i] + "\"]";
                  let value = fcf.application.getLocalData().getItem(childId, childArgPath + newSuffix);
                  clProcessLink(childId,  childArgPath + newSuffix, value, a_req, a_eventMap, a_protectDublicattes, false, false);
                }
              }
            }
          }

          if (argInfo.source === undefined)
            return;

          //going up on graph
          if (fcf.isArg(argInfo.source) && argInfo.source.type == "reference"){
            clProcessLink(argInfo.source.object.id, argInfo.source.arg + argInfo.suffix, a_value, a_req, a_eventMap, a_protectDublicattes, false, true);
          }
        }



        if (!(a_callPosition in stCallsRecursion)) {
          stCallsRecursion[a_callPosition] = true;
          ++stArgRecursion;
          clProcessLink(this._id, a_key, a_value, {}, eventMap, {}, true, true);
          a_key = fcf.normalizeObjectAddress(a_key);
          for(var i in eventMap){
            let id = eventMap[i].id;
            let eventInfo = eventMap[i];
            let wrp = fcf.getWrapper(id);
            if (!wrp)
              continue;
            try {
              if (wrp.onArg){
                wrp.onArg(eventInfo.arg, eventInfo.value, a_editor, a_ignoreRedrawing, a_innerCall && wrp == a_editor, eventInfo.suffix);
              }
            }
            catch(e){
              --this._callPositionsCounter;
              if (!this._callPositionsCounter)
                this._callPositionsSetArg = {};
              fcf.log.err("APPLICATION:Wrapper", e);
              throw e;
            }

            let method = "onArg" + eventInfo.arg.charAt(0).toUpperCase() + eventInfo.arg.slice(1);
            try {
              if (wrp[method])
                wrp[method](eventInfo.value, a_editor, a_ignoreRedrawing, a_innerCall && wrp == a_editor, eventInfo.suffix);
            } catch(e) {
              --this._callPositionsCounter;
              if (!this._callPositionsCounter)
                this._callPositionsSetArg = {};
              fcf.log.err("APPLICATION:Wrapper", e);
              throw e;
            }
          }

          --stArgRecursion;
          if (stArgRecursion == 0){
            stCallsRecursion = {};
          }
        }

        --this._callPositionsCounter;
        if (!this._callPositionsCounter)
          this._callPositionsSetArg = {};
      }


    };

    Wrapper.prototype._attachInnerEvents = function(){
      let self = this;
      // attach events by attributes
      function clAttach(a_element){
        if (!a_element)
          return;

        let fcfEvents = {};
        for(let i = 0; i < a_element.attributes.length; ++i){
          if(a_element.attributes[i].name.toLowerCase().indexOf("fcfevent") == 0) {
            fcfEvents[a_element.attributes[i].name.substr(8)] = a_element.attributes[i].value;
          }
        };

        for(let eventName in fcfEvents) {
          let eventCode = fcfEvents[eventName];
          let options = {
            wrapper: self,
            parent: self.getParent(),
          };
          (function(options, eventCode){
            fcf.addDomListener(a_element, eventName, self, function(event){
              return fcf.scriptExecutor.execute(eventCode, options, "&event&" + eventCode);
            }, true);
          })(options, eventCode)
        }
        if (a_element.children) {
          for(var i = 0; i < a_element.children.length; ++i){
            if (a_element.children[i].getAttribute("fcftemplate"))
              continue;
            var r = clAttach(a_element.children[i]);
          }
        }
      }

      clAttach(this.getDomElement());

      // attach events by arguments
      var templateData = this.getShortArgs();
      for(var key in templateData){
        if (typeof key !== "string")
          continue;
        if (key.indexOf("fcfEvent") !== 0)
          continue;

        var eventName = key.substr(8);
            eventName = eventName[0].toLowerCase() + eventName.substr(1);
        var eventCode = templateData[key];
        var options = {
          wrapper: self,
          parent: self.getParent(),
        };
        (function(eventName, eventCode, options){
          fcf.addDomListener(self.getActionDomElement(), eventName, self, function(event){
            with(options){
              return eval(eventCode);
            }
          }, true)
        })(eventName, eventCode, options);
      }

    }


    NClient.Wrapper = Wrapper;
    return NClient.Wrapper;

  }
});
