/*global DateTimeShortcuts, SelectFilter*/
/**
 * Django admin inlines
 *
 * Based on jQuery Formset 1.1
 * @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com)
 * @requires jQuery 1.2.6 or later
 *
 * Copyright (c) 2009, Stanislaus Madueke
 * All rights reserved.
 *
 * Spiced up with Code from Zain Memon's GSoC project 2009
 * and modified for Django by Jannis Leidel, Travis Swicegood and Julien Phalip.
 *
 * Licensed under the New BSD License
 * See: https://opensource.org/licenses/bsd-license.php
 */
"use strict";
{
  const $ = django.jQuery;
  $.fn.formset = function (opts) {
    const options = $.extend({}, $.fn.formset.defaults, opts);
    const $this = $(this);
    const $parent = $this.parent();
    const updateElementIndex = function (el, prefix, ndx) {
      const id_regex = new RegExp("(" + prefix + "-(\\d+|__prefix__))");
      const replacement = prefix + "-" + ndx;
      if ($(el).prop("for")) {
        $(el).prop("for", $(el).prop("for").replace(id_regex, replacement));
      }
      if (el.id) {
        el.id = el.id.replace(id_regex, replacement);
      }
      if (el.name) {
        // !CHANGED from original
        // el.name = el.name.replace(id_regex, replacement);
        el.setAttribute("name", el.name.replace(id_regex, replacement));
      }
    };
    const totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").prop(
      "autocomplete",
      "off"
    );
    let nextIndex = parseInt(totalForms.val(), 10);
    const maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").prop(
      "autocomplete",
      "off"
    );
    const minForms = $("#id_" + options.prefix + "-MIN_NUM_FORMS").prop(
      "autocomplete",
      "off"
    );
    let addButton;

    /**
     * The "Add another MyModel" button below the inline forms.
     */
    const addInlineAddButton = function () {
      if (addButton === null) {
        if ($this.prop("tagName") === "TR") {
          // If forms are laid out as table rows, insert the
          // "add" button in a new table row:
          const numCols = $this.eq(-1).children().length;
          $parent.append(
            '<tr class="' +
              options.addCssClass +
              '"><td colspan="' +
              numCols +
              '"><a href="#">' +
              options.addText +
              "</a></tr>"
          );
          addButton = $parent.find("tr:last a");
        } else {
          // Otherwise, insert it immediately after the last form:
          $this
            .filter(":last")
            .after(
              '<div class="' +
                options.addCssClass +
                '"><a href="#">' +
                options.addText +
                "</a></div>"
            );
          addButton = $this.filter(":last").next().find("a");
        }
      }
      addButton.on("click", addInlineClickHandler);
    };

    const addInlineClickHandler = function (e) {
      e.preventDefault();
      const template = $("#" + options.prefix + "-empty");
      const row = template.clone(true);
      row
        .removeClass(options.emptyCssClass)
        .addClass(options.formCssClass)
        .attr("id", options.prefix + "-" + nextIndex);

      addInlineDeleteButton(row);
      row.find("*").each(function () {
        updateElementIndex(this, options.prefix, totalForms.val());
      });

      // Insert the new form when it has been fully edited.
      // !CHANGED from original
      if ($(template).parent().is("tbody")) {
        row
          .wrap('<tbody class="template"></tbody>')
          .parent()
          .insertBefore($(template).parent());
      } else {
        row.insertBefore($(template));
      }

      // Update number of total forms.
      $(totalForms).val(parseInt(totalForms.val(), 10) + 1);
      nextIndex += 1;
      // Hide the add button if there's a limit and it's been reached.
      if (maxForms.val() !== "" && maxForms.val() - totalForms.val() <= 0) {
        addButton.parent().hide();
      }
      // Show the remove buttons if there are more than min_num.
      toggleDeleteButtonVisibility(row.closest(".inline-group"));

      // Pass the new form to the post-add callback, if provided.
      if (options.added) {
        options.added(row);
      }
      row.get(0).dispatchEvent(
        new CustomEvent("formset:added", {
          bubbles: true,
          detail: {
            formsetName: options.prefix,
          },
        })
      );
    };

    /**
     * The "X" button that is part of every unsaved inline.
     * (When saved, it is replaced with a "Delete" checkbox.)
     */
    const addInlineDeleteButton = function (row) {
      if (row.is("tr")) {
        // If the forms are laid out in table rows, insert
        // the remove button into the last table cell:
        row
          .children(":last")
          .append(
            '<div><a class="' +
              options.deleteCssClass +
              '" href="#">' +
              options.deleteText +
              "</a></div>"
          );
      } else if (row.is("ul") || row.is("ol")) {
        // If they're laid out as an ordered/unordered list,
        // insert an <li> after the last list item:
        row.append(
          '<li><a class="' +
            options.deleteCssClass +
            '" href="#">' +
            options.deleteText +
            "</a></li>"
        );
      } else {
        // Otherwise, just insert the remove button as the
        // last child element of the form's container:
        row
          .children(":first")
          .append(
            '<span><a class="' +
              options.deleteCssClass +
              '" href="#">' +
              options.deleteText +
              "</a></span>"
          );
      }
      // Add delete handler for each row.
      row
        .find("a." + options.deleteCssClass)
        .on("click", inlineDeleteHandler.bind(this));
    };

    const inlineDeleteHandler = function (e1) {
      e1.preventDefault();
      const deleteButton = $(e1.target);
      const row = deleteButton.closest("." + options.formCssClass);
      const inlineGroup = row.closest(".inline-group");
      // Remove the parent form containing this button,
      // and also remove the relevant row with non-field errors:
      const prevRow = row.prev();
      if (prevRow.length && prevRow.hasClass("row-form-errors")) {
        prevRow.remove();
      }

      // !CHANGED from original
      if (deleteButton.parent().parent().parent().parent().is("tbody")) {
        row.parent().remove();
      } else {
        row.remove();
      }
      nextIndex -= 1;
      // Pass the deleted form to the post-delete callback, if provided.
      if (options.removed) {
        options.removed(row);
      }
      document.dispatchEvent(
        new CustomEvent("formset:removed", {
          detail: {
            formsetName: options.prefix,
          },
        })
      );
      // Update the TOTAL_FORMS form count.
      const forms = $("." + options.formCssClass);
      $("#id_" + options.prefix + "-TOTAL_FORMS").val(forms.length);
      // Show add button again once below maximum number.
      if (maxForms.val() === "" || maxForms.val() - forms.length > 0) {
        addButton.parent().show();
      }
      // Hide the remove buttons if at min_num.
      toggleDeleteButtonVisibility(inlineGroup);
      // Also, update names and ids for all remaining form controls so
      // they remain in sequence:
      let i, formCount;
      const updateElementCallback = function () {
        updateElementIndex(this, options.prefix, i);
      };
      for (i = 0, formCount = forms.length; i < formCount; i++) {
        updateElementIndex($(forms).get(i), options.prefix, i);
        $(forms.get(i)).find("*").each(updateElementCallback);
      }
    };

    const toggleDeleteButtonVisibility = function (inlineGroup) {
      if (minForms.val() !== "" && minForms.val() - totalForms.val() >= 0) {
        inlineGroup.find(".inline-deletelink").hide();
      } else {
        inlineGroup.find(".inline-deletelink").show();
      }
    };

    // !CHANGED from original. Business logic for tabular inlines is different.
    if ($this.parent().is("tbody")) {
      $this
        .parent()
        .parent()
        .find("tr.form-row")
        .each(function (i) {
          $(this)
            .not("." + options.emptyCssClass)
            .addClass(options.formCssClass);
        });
    } else {
      $this.each(function (i) {
        $(this)
          .not("." + options.emptyCssClass)
          .addClass(options.formCssClass);
      });
    }

    // Create the delete buttons for all unsaved inlines:
    // !CHANGED from original, added parent() and used find() instead of filter()
    $this
      .parent()
      .parent()
      .find(
        "." +
          options.formCssClass +
          ":not(.has_original):not(." +
          options.emptyCssClass +
          ")"
      )
      .each(function () {
        addInlineDeleteButton($(this));
      });
    toggleDeleteButtonVisibility($this);

    // Create the add button, initially hidden.
    addButton = options.addButton;
    addInlineAddButton();

    // Show the add button if allowed to add more items.
    // Note that max_num = None translates to a blank string.
    const showAddButton =
      maxForms.val() === "" || maxForms.val() - totalForms.val() > 0;
    if ($this.length && showAddButton) {
      addButton.parent().show();
    } else {
      addButton.parent().hide();
    }

    return this;
  };

  /* Setup plugin defaults */
  $.fn.formset.defaults = {
    prefix: "form", // The form prefix for your django formset
    addText: "add another", // Text for the add link
    deleteText: "remove", // Text for the delete link
    addCssClass: "add-row", // CSS class applied to the add link
    deleteCssClass: "delete-row", // CSS class applied to the delete link
    emptyCssClass: "empty-row", // CSS class applied to the empty row
    formCssClass: "dynamic-form", // CSS class applied to each form in a formset
    added: null, // Function called each time a new form is added
    removed: null, // Function called each time a form is deleted
    addButton: null, // Existing add button to use
  };

  // Tabular inlines ---------------------------------------------------------
  $.fn.tabularFormset = function (selector, options, callback = null) {
    const $rows = $(this);

    const reinitDateTimeShortCuts = function () {
      // Reinitialize the calendar and clock widgets by force
      if (typeof DateTimeShortcuts !== "undefined") {
        $(".datetimeshortcuts").remove();
        DateTimeShortcuts.init();
      }
    };

    const updateSelectFilter = function () {
      // If any SelectFilter widgets are a part of the new form,
      // instantiate a new SelectFilter instance for it.
      if (typeof SelectFilter !== "undefined") {
        $(".selectfilter").each(function (index, value) {
          SelectFilter.init(value.id, this.dataset.fieldName, false);
        });
        $(".selectfilterstacked").each(function (index, value) {
          SelectFilter.init(value.id, this.dataset.fieldName, true);
        });
      }
    };

    const initPrepopulatedFields = function (row) {
      row.find(".prepopulated_field").each(function () {
        const field = $(this),
          input = field.find("input, select, textarea"),
          dependency_list = input.data("dependency_list") || [],
          dependencies = [];
        $.each(dependency_list, function (i, field_name) {
          dependencies.push(
            "#" +
              row
                .find(".field-" + field_name)
                .find("input, select, textarea")
                .attr("id")
          );
        });
        if (dependencies.length) {
          input.prepopulate(dependencies, input.attr("maxlength"));
        }
      });
    };

    $rows.formset({
      prefix: options.prefix,
      addText: options.addText,
      formCssClass: "dynamic-" + options.prefix,
      deleteCssClass: "inline-deletelink",
      deleteText: options.deleteText,
      emptyCssClass: "empty-form",
      added: function (row) {
        initPrepopulatedFields(row);
        reinitDateTimeShortCuts();
        updateSelectFilter();
      },
      addButton: options.addButton,
    });

    if (typeof callback === "function") {
      callback();
    }

    return $rows;
  };

  // Stacked inlines ---------------------------------------------------------
  $.fn.stackedFormset = function (selector, options, callback = null) {
    const $rows = $(this);
    const updateInlineLabel = function (row) {
      $(selector)
        .find(".inline_label")
        .each(function (i) {
          const count = i + 1;
          $(this).html(
            $(this)
              .html()
              .replace(/(#\d+)/g, "#" + count)
          );
        });
    };

    const reinitDateTimeShortCuts = function () {
      // Reinitialize the calendar and clock widgets by force, yuck.
      if (typeof DateTimeShortcuts !== "undefined") {
        $(".datetimeshortcuts").remove();
        DateTimeShortcuts.init();
      }
    };

    const updateSelectFilter = function () {
      // If any SelectFilter widgets were added, instantiate a new instance.
      if (typeof SelectFilter !== "undefined") {
        $(".selectfilter").each(function (index, value) {
          SelectFilter.init(value.id, this.dataset.fieldName, false);
        });
        $(".selectfilterstacked").each(function (index, value) {
          SelectFilter.init(value.id, this.dataset.fieldName, true);
        });
      }
    };

    const initPrepopulatedFields = function (row) {
      row.find(".prepopulated_field").each(function () {
        const field = $(this),
          input = field.find("input, select, textarea"),
          dependency_list = input.data("dependency_list") || [],
          dependencies = [];
        $.each(dependency_list, function (i, field_name) {
          // Dependency in a fieldset.
          let field_element = row.find(".form-row .field-" + field_name);
          // Dependency without a fieldset.
          if (!field_element.length) {
            field_element = row.find(".form-row.field-" + field_name);
          }
          dependencies.push(
            "#" + field_element.find("input, select, textarea").attr("id")
          );
        });
        if (dependencies.length) {
          input.prepopulate(dependencies, input.attr("maxlength"));
        }
      });
    };

    $rows.formset({
      prefix: options.prefix,
      addText: options.addText,
      formCssClass: "dynamic-" + options.prefix,
      deleteCssClass: "inline-deletelink",
      deleteText: options.deleteText,
      emptyCssClass: "empty-form",
      removed: updateInlineLabel,
      added: function (row) {
        initPrepopulatedFields(row);
        reinitDateTimeShortCuts();
        updateSelectFilter();
        updateInlineLabel(row);
      },
      addButton: options.addButton,
    });

    if (typeof callback === "function") {
      callback();
    }

    return $rows;
  };

  $(window).on("htmx:afterSettle", function (event) {
    if (event.target.classList.contains("js-inline-admin-formset")) {
      initInlines($(event.target), function () {
        if (typeof DateTimeShortcuts !== "undefined") {
          $(".datetimeshortcuts").remove();
          DateTimeShortcuts.init();
        }

        $(event.target).find(".admin-autocomplete").djangoAdminSelect2();
      });
    }
  });

  $(document).ready(function () {
    $(".js-inline-admin-formset").each(function () {
      initInlines(this);
    });
  });

  function initInlines(el, callback = null) {
    const data = $(el).data(),
      inlineOptions = data.inlineFormset;
    let selector;
    switch (data.inlineType) {
      case "stacked":
        selector = inlineOptions.name + "-group .inline-related";
        $(selector).stackedFormset(selector, inlineOptions.options, callback);
        break;
      case "tabular":
        selector =
          inlineOptions.name +
          "-group .tabular.inline-related tbody:last > tr.form-row";
        $(selector).tabularFormset(selector, inlineOptions.options, callback);
        break;
    }
  }
}
