/* Pi-hole: A black hole for Internet advertisements
 *  (c) 2023 Pi-hole, LLC (https://pi-hole.net)
 *  Network-wide ad blocking via your own hardware.
 *
 *  This file is copyright under the latest version of the EUPL.
 *  Please see LICENSE file for your rights under this license. */

/* global utils:false, apiFailure: false */

"use strict";

function addAllowedValues(allowed) {
  if (typeof allowed === "object") {
    return (
      "<p>Available options:</p><ul><li>" +
      allowed
        .map(option => "<code>" + option.item + "</code>: " + utils.escapeHtml(option.description))
        .join("</li><li>") +
      "</li></ul>"
    );
  }

  if (typeof allowed === "string") {
    return `<p class="small">Allowed value: ${utils.escapeHtml(allowed)}</p>`;
  }
}

function boxIcons(value) {
  return (
    '<span class="box-icons">' +
    (value.modified ? '<i class="far fa-edit text-info" title="Modified from default"></i>' : "") +
    (value.flags.restart_dnsmasq
      ? '<i class="fas fa-redo text-orange" title="Setting requires FTL restart on change"></i>'
      : "") +
    (value.flags.env_var
      ? '<i class="fas fa-lock text-orange" title="Settings overwritten by an environmental variable are read-only"></i>'
      : "") +
    "</span>"
  );
}

function valueDetails(key, value) {
  // Define default hint text
  let defaultValueHint = "";
  if (value.modified) {
    defaultValueHint = "";
    if (value.default !== null) {
      let defVal = utils.escapeHtml(JSON.stringify(value.default));
      switch (defVal) {
        case "true": {
          defVal = "enabled";

          break;
        }

        case "false": {
          defVal = "disabled";

          break;
        }

        case '""':
        case "[]": {
          defVal = "empty";

          break;
        }
        // No default
      }

      defaultValueHint = "<p>Default Value: " + defVal + "</p>";
    }
  }

  // Define extraAttributes, if needed
  const extraAttributes = value.flags.env_var ? " disabled" : "";

  // Format the output depending on the value type
  let content = "";
  switch (value.type) {
    case "IPv4 address":
    case "IPv6 address":
    case "string": {
      content +=
        '<label class="col-sm-2 control-label">Value <small>(string)</small></label>' +
        '<div class="col-sm-10">' +
        '<input type="text" class="form-control" value="' +
        utils.escapeHtml(value.value) +
        '" data-key="' +
        utils.escapeHtml(key) +
        '"' +
        extraAttributes +
        "> " +
        defaultValueHint +
        addAllowedValues(value.allowed) +
        "</div>";

      break;
    }

    case "boolean": {
      content +=
        '<div class="col-sm-12">' +
        '<div class="form-check"><input class="form-check-input" type="checkbox" ' +
        (value.value ? " checked" : "") +
        ' id="' +
        key +
        '-checkbox" data-key="' +
        key +
        '"' +
        extraAttributes +
        '><label class="form-check-label" for="' +
        key +
        '-checkbox">Enabled ' +
        defaultValueHint +
        "</label></div>" +
        " </div>";

      break;
    }

    case "double": {
      content +=
        '<label class="col-sm-2 control-label">Value</label>' +
        '<div class="col-sm-10">' +
        '<input type="number" class="form-control" value="' +
        utils.escapeHtml(String(value.value)) +
        '" data-key="' +
        utils.escapeHtml(key) +
        '" data-type="float"' +
        extraAttributes +
        "> " +
        defaultValueHint +
        "</div>";

      break;
    }

    case "integer": {
      content +=
        '<label class="col-sm-2 control-label">Value <small>(integer)</small></label>' +
        '<div class="col-sm-10">' +
        '<input type="number" step="1" class="form-control" value="' +
        utils.escapeHtml(String(value.value)) +
        '" data-key="' +
        utils.escapeHtml(key) +
        '" data-type="integer"' +
        extraAttributes +
        "> " +
        defaultValueHint +
        "</div>";

      break;
    }

    case "unsigned integer": {
      content +=
        '<label class="col-sm-4 control-label">Value <small>(unsigned integer)</small></label>' +
        '<div class="col-sm-8">' +
        '<input type="number" step="1" min="0" class="form-control" value="' +
        utils.escapeHtml(String(value.value)) +
        '" data-key="' +
        utils.escapeHtml(key) +
        '" data-type="integer"' +
        extraAttributes +
        "> " +
        defaultValueHint +
        "</div>";

      break;
    }

    case "unsigned integer (16 bit)": {
      content +=
        '<label class="col-sm-4 control-label">Value <small>(unsigned 16bit integer)</small></label>' +
        '<div class="col-sm-8">' +
        '<input type="number" step="1" min="0" max="65535" class="form-control" value="' +
        utils.escapeHtml(String(value.value)) +
        '" data-key="' +
        utils.escapeHtml(key) +
        '" data-type="integer"' +
        extraAttributes +
        "> " +
        defaultValueHint +
        "</div>";

      break;
    }

    case "string array": {
      content +=
        '<label class="col-sm-12 control-label">Values <small>(one item per line)</small></label>' +
        '<div class="col-sm-12">' +
        '<textarea class="form-control field-sizing-content" data-key="' +
        key +
        '"' +
        extraAttributes +
        ">" +
        value.value.join("\n") +
        "</textarea> " +
        defaultValueHint +
        addAllowedValues(value.allowed) +
        "</div>";

      break;
    }

    case "enum (unsigned integer)": // fallthrough
    case "enum (string)": {
      content += '<div class="col-sm-12">';
      for (const [index, option] of value.allowed.entries()) {
        content +=
          '<div class="form-check">' +
          // Radio button
          '<input type="radio" class="form-check-input" ' +
          `value="${option.item}" name="${key}" id="${key}_${index}" data-key="${key}"${extraAttributes}` +
          (option.item === value.value ? " checked" : "") +
          ">" +
          // Label
          `<label class="form-check-label" for="${key}_${index}"><strong>${utils.escapeHtml(option.item)}` +
          (option.item === value.default ? " <em>(default)</em>" : "") +
          "</strong></label>" +
          // Paragraph with description
          `<p class="help-block">${option.description}</p>` +
          "</div>";
      }

      content += "</div>";

      break;
    }

    case "password (write-only string)": {
      content +=
        '<label class="col-sm-2 control-label">Value <small>(string)</small></label>' +
        '<div class="col-sm-10">' +
        '<input type="password" class="form-control" value="' +
        value.value +
        '" data-key="' +
        key +
        '"' +
        extraAttributes +
        "> " +
        defaultValueHint +
        addAllowedValues(value.allowed) +
        "</div>";

      break;
    }

    default: {
      content += "TYPE " + value.type + " NOT DEFINED";
    }
  }

  return '<div class="row">' + content + "</div>";
}

function generateRow(topic, key, value) {
  // If the value is an object, we need to recurse
  if (!("description" in value)) {
    for (const [subkey, subvalue] of Object.entries(value)) {
      generateRow(topic, key + "." + subkey, subvalue);
    }

    return;
  }

  // else: we have a setting we can display
  const box =
    '<div class="card settings-box">' +
    '<div class="card-header">' +
    '<h3 class="card-title" data-key="' +
    key +
    '" data-modified="' +
    (value.modified ? "true" : "false") +
    '">' +
    key +
    boxIcons(value) +
    "</h3>" +
    "</div>" +
    '<div class="card-body">' +
    utils.escapeHtml(value.description).replaceAll("\n", "<br>") +
    "</div>" +
    '<div class="card-footer">' +
    valueDetails(key, value) +
    "</div></div> ";

  const topKey = key.split(".", 1)[0];
  const elem = $("#advanced-content-" + topKey + "-flex");
  elem.append(box);
}

function createDynamicConfigTabs() {
  $.ajax({
    url: document.body.dataset.apiurl + "/config?detailed=true",
  })
    .done(data => {
      // Create the tabs for the advanced dynamic config topics
      for (const topic of Object.values(data.topics)) {
        $("#advanced-settings-tabs").append(`
          <div id="advanced-content-${topic.name}" role="tabpanel" class="tab-pane fade">
            <h3 class="page-header">${topic.description} (<code>${topic.name}</code>)</h3>
            <div class="row" id="advanced-content-${topic.name}-body">
              <div class="col-12 settings-container" id="advanced-content-${topic.name}-flex"></div>
            </div>
          </div>
        `);

        // Dynamically create the settings menu
        $("#advanced-settings-menu ul").append(`
          <li role="presentation">
            <a href="#advanced-content-${topic.name}" class="btn btn-primary" aria-controls="advanced-content-${topic.name}" role="tab" data-bs-toggle="pill">${topic.description.replace(" settings", "")}</a>
          </li>
        `);
      }

      // Dynamically fill the tabs with config topics
      for (const [topic, value] of Object.entries(data.config)) {
        generateRow(topic, topic, value);
      }

      $("#advanced-overlay").hide();

      // Select the first tab and show the content. Bootstrap 5's Tab
      // component tracks the active tab via the "active" class on the link
      // itself (not the surrounding <li>), so that's what needs to be set
      // here for tab switching to correctly deactivate this initial tab.
      $("#advanced-settings-menu ul li:first-child").addClass("active");
      $("#advanced-settings-menu ul li:first-child > a").addClass("active");
      $("#advanced-settings-tabs > div:first-child").addClass("active show");

      applyOnlyChanged();
    })
    .fail(data => {
      apiFailure(data);
    });
}

function initOnlyChanged() {
  const elem = $("#only-changed");

  // Restore settings level from local storage (if available) or default to "false"
  if (localStorage.getItem("only-changed") === null) {
    localStorage.setItem("only-changed", "false");
  }

  elem.prop("checked", localStorage.getItem("only-changed") === "true");

  elem.bootstrapToggle({
    onlabel: "Only modified",
    offlabel: "Show all",
    onstyle: "primary",
    offstyle: "success",
    size: "small",
    width: "180px",
  });

  elem.on("change", function () {
    localStorage.setItem("only-changed", $(this).prop("checked") ? "true" : "false");
    applyOnlyChanged();
  });

  elem.bootstrapToggle(localStorage.getItem("only-changed") === "true" ? "on" : "off");
  elem.trigger("change");
}

function applyOnlyChanged() {
  if (localStorage.getItem("only-changed") === "true") {
    // Show only modified settings (hide tabs menu and empty tabs).

    // Hide the tabs menu
    $("#advanced-settings-menu").hide();

    // Show all tabs, except the ones containing "data-modified='true'" attribute
    // to prevent empty tabs (using the same classes used by Boostrap3)
    $("#advanced-settings-tabs > .tab-pane").addClass("show active");
    $("#advanced-settings-tabs > .tab-pane:not(:has(h3[data-modified='true']))").removeClass(
      "show active"
    );

    // Hide all boxes with data-key attribute, except the ones with "data-modified='true'" attribute
    $(".card-title[data-key]").not("[data-modified='true']").closest(".card").hide();
  } else {
    // Show the tabs menu and activate only the first button (deactivate other buttons)
    $("#advanced-settings-menu").show();
    $("#advanced-settings-menu ul li").removeClass("active");
    $("#advanced-settings-menu ul li > a").removeClass("active");
    $("#advanced-settings-menu ul li:first-child").addClass("active");
    $("#advanced-settings-menu ul li:first-child > a").addClass("active");

    // Hide all tabs, except the first one (removing the classes used by Boostrap3)
    $("#advanced-settings-tabs > .tab-pane:not(:first-child)").removeClass("show active");

    // Show all boxes with data-key attribute
    $(".card-title[data-key]").closest(".card").show();
  }
}

$(() => {
  createDynamicConfigTabs();
  initOnlyChanged();
});
