(function (paw, utils) {
  function Deferred() {
    var self = this;
    this.resolve = null;
    this.reject = null;

    this.promise = new Promise(function (resolve, reject) {
      self.resolve = resolve;
      self.reject = reject;
    });
  }

  function AsyncUtils() {}

  AsyncUtils.prototype.defer = function () {
    return new Deferred();
  };

  AsyncUtils.prototype.promise = function () {
    let d = new Deferred();
    d.resolve();
    return d.promise;
  };

  AsyncUtils.prototype.reject = function () {
    let d = new Deferred();
    d.reject();
    return d.promise;
  };

  AsyncUtils.prototype.when = function (p) {
    if (paw.utils.value.notSet(p) || paw.utils.value.notFunction(p.then)) {
      return this.promise();
    }
    return p;
  };

  AsyncUtils.prototype.all = function (promises) {
    // Ensure that the input is an array
    if (!Array.isArray(promises)) {
      throw new Error("AsyncUtils: 'promises' must be an array.");
    }

    // Return a promise that resolves when all provided promises are resolved
    return Promise.all(promises);
  };

  AsyncUtils.prototype.getFile = function (settings) {
    if (paw.utils.value.notSet(settings.url)) {
      throw "AsyncUtils: URL not set!";
    }

    var xhr = new XMLHttpRequest();
    xhr.open("GET", settings.url, true);
    xhr.responseType = "blob";

    let d = paw.utils.async.defer();

    xhr.onload = function () {
      if (xhr.status === 200) {
        d.resolve(xhr.response);
      } else {
        console.error("Request failed with status:", xhr.status);
        d.reject(xhr.response);
      }
    };

    xhr.onerror = function () {
      console.error("Request failed");
      d.reject(xhr.response);
    };

    xhr.send();
    return d.promise;
  };

  AsyncUtils.prototype.delay = function (ms) {
    let d = new Deferred();
    setTimeout(function () {
      d.resolve();
    }, ms);
    return d.promise;
  };

  AsyncUtils.prototype.request = function (type, url, payload, settings) {
    var d = this.defer();
    $.ajax({
      type: type,
      url: paw.utils.value.isObject(url) ? url.url + qparams(url.params) : url,
      data: payload,
      cache: settings ? settings.cache : undefined,
      success: function (data) {
        d.resolve(data);
      },
      error: function () {
        d.reject();
      },
    });
    return d.promise;
  };

  AsyncUtils.prototype.get = function (url, settings) {
    return this.request("get", url, null, settings);
  };

  AsyncUtils.prototype.post = function (url, payload) {
    return this.request("post", url, payload);
  };

  AsyncUtils.prototype.put = function (url, payload) {
    return this.request("put", url, payload);
  };

  AsyncUtils.prototype.delete = function (url, payload) {
    return this.request("delete", url, payload);
  };

  AsyncUtils.prototype.asyncAction = function (
    request,
    $elt,
    $linkedActions,
    perpetual
  ) {
    var self = this;
    $elt.attr("disabled", true);
    this.actionIndicator($elt, true);

    var linkedActionsPreviousStates = [];
    if (paw.utils.value.isSet($linkedActions)) {
      utils.iterable.iterate($linkedActions, function ($action) {
        if ($action.is(":disabled")) {
          linkedActionsPreviousStates.push("disabled");
          return;
        }
        linkedActionsPreviousStates.push("enabled");
        $action.attr("disabled", true);
      });
    }
    if (perpetual) {
      return request();
    }

    return this.when(request()).finally(function () {
      // prevent double clicking
      return self.delay(200).then(function () {
        self.actionIndicator($elt, false);
        $elt.removeAttr("disabled");
        if (paw.utils.value.isSet($linkedActions)) {
          utils.iterable.iterate($linkedActions, function ($action, i) {
            var prevState = linkedActionsPreviousStates[i];
            if (prevState === "enabled") {
              $action.removeAttr("disabled");
            }
          });
        }
      });
    });
    return d.promise;
  };

  AsyncUtils.prototype.actionIndicator = function ($elt, isLoading) {
    if (utils.value.notSet(isLoading)) {
      isLoading = true;
    }
    var indic = $elt.attr("data-loader-indic");
    if (
      (isLoading && utils.value.isSet(indic)) ||
      (!isLoading && utils.value.notSet(indic))
    ) {
      return;
    }
    var $icon = $elt.find("> i");
    if ($icon.length === 0) {
      $elt.attr("data-loader-no-icon", true);
      $icon = $('<i class="fa fa-spinner fa-spin fa-fw"></i>');
      $elt.prepend($icon);
    }
    if (utils.value.notSet(indic)) {
      indic = $icon.attr("class");
      $elt.attr("data-loader-indic", indic);
      $icon.attr("class", "fa fa-spinner fa-spin fa-fw");
      return;
    }
    $icon.attr("class", indic);
    $elt.removeAttr("data-loader-indic");
    if (utils.value.isSet($elt.attr("data-loader-no-icon"))) {
      $icon.remove();
      $elt.removeAttr("data-loader-no-icon");
    }
  };

  AsyncUtils.prototype.debouncer = function (func, wait) {
    let timeout;
    return function () {
      const context = this;
      const args = arguments;
      const later = function () {
        timeout = null;
        func.apply(context, args);
      };
      clearTimeout(timeout);
      timeout = setTimeout(later, utils.value.isSet(wait) ? wait : 400);
    };
  };

  paw.utils.async = new AsyncUtils();
})(paw, paw.utils);
