/**
 * File: UsageStatistics_Page_View.js
 *
 * Usage statistics charts and AJAX refresh driver.
 *
 * @package W3TC
 * @since   2.0.0
 */

jQuery(document).ready(function ($) {
  var lastData;

  /**
   * Loads usage statistics data via AJAX.
   */
  function load() {
    $(".ustats_loading").removeClass("w3tc_hidden");
    $(".ustats_content").addClass("w3tc_hidden");
    $(".ustats_error").addClass("w3tc_none");
    $(".ustats_nodata").addClass("w3tc_none");

    $.getJSON(
      ajaxurl +
        "?action=w3tc_ajax&_wpnonce=" +
        w3tcGetAjaxNonce("ustats_get") +
        "&w3tc_action=ustats_get",
      function (data) {
        lastData = data;

        // Show sections with data.
        for (var p in data) {
          jQuery(".ustats_" + p).css("display", "flex");
        }

        setValues(data, "ustats_");

        if (data.period.seconds) {
          $(".ustats_content").removeClass("w3tc_hidden");
        } else {
          $(".ustats_nodata").removeClass("w3tc_none");
        }

        $(".ustats_loading").addClass("w3tc_hidden");

        setCharts(data);

        setRefresh(data && data.period ? data.period.to_update_secs : 0);

        showMetaboxes();
      },
    ).fail(function () {
      $(".ustats_error").removeClass("w3tc_none");
      $(".ustats_content").addClass("w3tc_hidden");
      $(".ustats_loading").addClass("w3tc_hidden");
    });
  }

  //
  // chart commons
  //
  var chartOptions = {
    //aspectRatio: 4,
    maintainAspectRatio: false,
    animation: false,
    plugins: {
      legend: {
        display: true,
        position: "top",
      },
      tooltip: {
        enabled: true,
      },
    },
    scales: {
      x: {
        display: true,
        title: {
          display: true,
          text: "Time",
        },
      },
      y: {
        display: true,
        title: {
          display: false,
        },
        ticks: {
          beginAtZero: true,
        },
      },
    },
  };

  var chartDateLabels = [];
  var chartGraphValues = {};
  var charts = {};

  /**
   * Sets up and updates all charts with usage statistics data.
   *
   * @param {Object} data Usage statistics data object.
   */
  function setCharts(data) {
    // Collect functors that prepare data for their own chart.
    var processors = [];
    processors.push(setChartsPageCache());
    processors.push(setChartsDb());
    processors.push(setChartsOc());
    processors.push(setChartsPhp());
    processors.push(setChartsCpu());
    processors.push(setChartsWpdb());
    processors.push(setChartsAccessLog());
    processors.push(setChartsMemcached());
    processors.push(setChartsRedis());
    processors.push(setChartsApc());

    // prepare collections
    var columnsToCollect = [];

    for (var i = 0; i < processors.length; i++) {
      for (var id in processors[i].chartDatasets) {
        if (!charts[id]) {
          continue;
        }
        var datasets = [];
        for (var i2 = 0; i2 < processors[i].chartDatasets[id].length; i2++) {
          var datasetTemplate = processors[i].chartDatasets[id][i2];
          var dataColumnString;
          if (Array.isArray(datasetTemplate.dataColumn)) {
            dataColumnString = datasetTemplate.dataColumn.join(".");
          } else {
            dataColumnString = datasetTemplate.dataColumn;
          }

          // Clear existing data array or create new one
          if (!chartGraphValues[dataColumnString]) {
            chartGraphValues[dataColumnString] = [];
          } else {
            chartGraphValues[dataColumnString].length = 0;
          }
          columnsToCollect.push({
            target: dataColumnString,
            column: datasetTemplate.dataColumn,
          });
          datasets.push({
            label: datasetTemplate.label,
            data: chartGraphValues[dataColumnString],
            backgroundColor: datasetTemplate.backgroundColor,
          });
        }

        // Set datasets - Chart.js v4 requires this to be done before data collection
        charts[id].data.datasets = datasets;
      }
    }

    // collect data for charts
    var history = data.history;
    chartDateLabels.length = 0;
    var validDataPoints = [];

    // First pass: collect valid data points with timestamps
    for (var i = 0; i < history.length; i++) {
      var historyItem = history[i];
      var dateFormatted = "";
      var hasValidTimestamp = false;

      if (history[i].timestamp_start) {
        var timestamp = parseInt(history[i].timestamp_start, 10);
        if (!isNaN(timestamp) && timestamp > 0) {
          var d = new Date(timestamp * 1000);
          if (!isNaN(d.getTime())) {
            dateFormatted = dateFormat(d);
            if (dateFormatted && dateFormatted.length > 0) {
              hasValidTimestamp = true;
            }
          }
        }
      }

      // Only include entries with valid timestamps
      if (hasValidTimestamp) {
        validDataPoints.push({
          item: historyItem,
          label: dateFormatted,
        });
      }
    }

    // Second pass: process valid data points
    for (var i = 0; i < validDataPoints.length; i++) {
      var historyItem = validDataPoints[i].item;
      chartDateLabels.push(validDataPoints[i].label);

      // custom preprocess history row
      for (var i2 = 0; i2 < processors.length; i2++) {
        if (processors[i2].preprocess) {
          processors[i2].preprocess(historyItem);
        }
      }

      // collect metrics for graphs
      for (var i2 = 0; i2 < columnsToCollect.length; i2++) {
        var c = columnsToCollect[i2];
        var v;
        if (Array.isArray(c.column)) {
          // Handle nested properties like ['access_log', 'dynamic_count']
          if (
            historyItem[c.column[0]] &&
            typeof historyItem[c.column[0]] === "object"
          ) {
            v = historyItem[c.column[0]][c.column[1]];
          } else {
            v = undefined;
          }
        } else {
          // Handle direct properties
          v = historyItem[c.column];
        }

        // Convert to number if possible, otherwise use 0
        if (v === null || v === undefined || v === "") {
          v = 0;
        } else if (typeof v === "string") {
          v = parseFloat(v) || 0;
        } else if (typeof v === "number") {
          v = isNaN(v) ? 0 : v;
        } else {
          v = 0;
        }

        chartGraphValues[c.target].push(v);
      }
    }

    // visualize - update labels and data for all charts
    for (var c in charts) {
      if (!charts[c]) {
        continue;
      }
      charts[c].data.labels = chartDateLabels.slice();
      charts[c].update("none");
    }
  }

  $(".w3tcus_chart_check").click(function () {
    setCharts(lastData);
  });

  /**
   * Sets up the Page Cache chart.
   *
   * @return {Object} Chart datasets and preprocessing function.
   */
  function setChartsPageCache() {
    if (!charts["pagecache"]) {
      var canvas = $("#w3tcus_pagecache_chart")[0];
      if (!canvas) {
        return { chartDatasets: {} };
      }
      var ctx = canvas.getContext("2d");
      charts["pagecache"] = new Chart(ctx, {
        type: "bar",
        data: {
          labels: chartDateLabels,
          datasets: [],
        },
        options: chartOptions,
      });
    }

    return {
      chartDatasets: {
        pagecache: [
          {
            label: "Time (ms)",
            dataColumn: "pagecache_requests_time_ms",
            backgroundColor: "#0073aa",
          },
        ],
      },
      preprocess: function (historyItem) {
        var v = 0;
        if (
          historyItem.pagecache_requests_time_10ms &&
          historyItem.php_requests
        ) {
          v = Math.round(
            (historyItem.pagecache_requests_time_10ms * 10) /
              historyItem.php_requests,
          );
        }
        historyItem.pagecache_requests_time_ms = v;
      },
    };
  }

  /**
   * Sets up the Database Cache chart.
   *
   * @return {Object} Chart datasets and preprocessing function.
   */
  function setChartsDb() {
    if (!charts["db"]) {
      var canvas = $("#w3tcus_dbcache_chart")[0];
      if (!canvas) {
        return { chartDatasets: {} };
      }
      var ctx = canvas.getContext("2d");
      charts["db"] = new Chart(ctx, {
        type: "bar",
        data: {
          labels: chartDateLabels,
        },
        options: chartOptions,
      });

      var canvas2 = $("#w3tcus_dbcache_time_chart")[0];
      if (canvas2) {
        var ctx2 = canvas2.getContext("2d");
        charts["db_time"] = new Chart(ctx2, {
          type: "bar",
          data: {
            labels: chartDateLabels,
          },
          options: chartOptions,
        });
      }
    }

    return {
      chartDatasets: {
        db_time: [
          {
            label: "Time (ms)",
            dataColumn: "dbcache_time_ms",
            backgroundColor: "#0073aa",
          },
        ],
        db: [
          {
            label: "Calls",
            dataColumn: "dbcache_calls_total",
            backgroundColor: "#0073aa",
          },
          {
            label: "Hits",
            dataColumn: "dbcache_calls_hits",
            backgroundColor: "green",
          },
        ],
      },
    };
  }

  //
  // OC chart
  //
  /**
   * Sets up the Object Cache chart.
   *
   * @param {Object} data Usage statistics data object.
   * @return {Object} Chart datasets and preprocessing function.
   */
  function setChartsOc(data) {
    if (!charts["oc"]) {
      var canvas = $("#w3tcus_objectcache_chart")[0];
      if (!canvas) {
        return { chartDatasets: {} };
      }
      var ctx = canvas.getContext("2d");
      charts["oc"] = new Chart(ctx, {
        type: "bar",
        data: {
          labels: chartDateLabels,
        },
        options: chartOptions,
      });

      var canvas2 = $("#w3tcus_objectcache_time_chart")[0];
      if (!canvas2) {
        return { chartDatasets: {} };
      }
      var ctx2 = canvas2.getContext("2d");
      charts["oc_time"] = new Chart(ctx2, {
        type: "bar",
        data: {
          labels: chartDateLabels,
        },
        options: chartOptions,
      });
    }

    return {
      chartDatasets: {
        oc_time: [
          {
            label: "Time (ms)",
            dataColumn: "objectcache_time_ms",
            backgroundColor: "#0073aa",
          },
        ],
        oc: [
          {
            label: "Gets",
            dataColumn: "objectcache_get_total",
            backgroundColor: "#0073aa",
          },
          {
            label: "Hits",
            dataColumn: "objectcache_get_hits",
            backgroundColor: "green",
          },
          {
            label: "Sets",
            dataColumn: "objectcache_sets",
            backgroundColor: "red",
          },
        ],
      },
    };
  }

  /**
   * Sets up the PHP Memory and Requests charts.
   *
   * @param {Object} data Usage statistics data object.
   * @return {Object} Chart datasets and preprocessing function.
   */
  function setChartsPhp(data) {
    if (!charts["phpMemory"]) {
      var ctx = $("#w3tcus_php_memory_chart")[0].getContext("2d");
      charts["phpMemory"] = new Chart(ctx, {
        type: "bar",
        data: {
          labels: chartDateLabels,
        },
        options: chartOptions,
      });
    }
    if (!charts["phpRequests"]) {
      var ctx = $("#w3tcus_php_requests_chart")[0].getContext("2d");
      charts["phpRequests"] = new Chart(ctx, {
        type: "bar",
        data: {
          labels: chartDateLabels,
        },
        options: chartOptions,
      });
    }

    var phpRequestsDatasets = [];
    $(".w3tcus_chart_check").each(function () {
      if ($(this).is(":checked")) {
        var dataColumn = $(this).data("column");
        var backgroundColor = $(this).data("background");
        if (!backgroundColor) {
          backgroundColor = "#0073aa";
        }

        if (startsWith(dataColumn, "php_php_requests")) {
          phpRequestsDatasets.push({
            label: $(this).data("name"),
            dataColumn: dataColumn.substr(4),
            backgroundColor: backgroundColor,
          });
        }
      }
    });

    return {
      chartDatasets: {
        phpMemory: [
          {
            label: "MB",
            dataColumn: "php_memory_mb",
            backgroundColor: "#0073aa",
          },
        ],
        phpRequests: phpRequestsDatasets,
      },
      preprocess: function (historyItem) {
        var v = 0;
        if (historyItem.php_requests) {
          v = (
            historyItem.php_memory_100kb /
            100.0 /
            historyItem.php_requests
          ).toFixed(2);
        }
        historyItem.php_memory_mb = v;

        historyItem.php_requests_pagecache_miss =
          historyItem.php_requests - historyItem.php_requests_pagecache_hit;
      },
    };
  }

  /**
   * Sets up the CPU chart.
   *
   * @param {Object} data Usage statistics data object.
   * @return {Object} Chart datasets and preprocessing function.
   */
  function setChartsCpu(data) {
    if (!charts["cpu"]) {
      var ctx = $("#w3tcus_cpu_chart")[0].getContext("2d");
      charts["cpu"] = new Chart(ctx, {
        type: "bar",
        data: {
          labels: chartDateLabels,
        },
        options: chartOptions,
      });
    }

    return {
      chartDatasets: {
        cpu: [
          {
            label: "CPU",
            dataColumn: "cpu",
            backgroundColor: "#0073aa",
          },
        ],
      },
    };
  }

  /**
   * Sets up the WPDB chart.
   *
   * @param {Object} data Usage statistics data object.
   * @return {Object} Chart datasets and preprocessing function.
   */
  function setChartsWpdb(data) {
    if (!charts["wpdb"]) {
      var ctx = $("#w3tcus_wpdb_chart")[0].getContext("2d");
      charts["wpdb"] = new Chart(ctx, {
        type: "bar",
        data: {
          labels: chartDateLabels,
        },
        options: chartOptions,
      });
    }

    return {
      chartDatasets: {
        wpdb: [
          {
            label: "Total",
            dataColumn: "wpdb_calls_total",
            backgroundColor: "#0073aa",
          },
        ],
      },
    };
  }

  /**
   * Sets up the Access Log charts.
   *
   * @param {Object} data Usage statistics data object.
   * @return {Object} Chart datasets and preprocessing function.
   */
  function setChartsAccessLog(data) {
    if (!charts["accessLogRequests"]) {
      var ctx = $("#w3tcus_access_log_chart_requests")[0].getContext("2d");
      charts["accessLogRequests"] = new Chart(ctx, {
        type: "bar",
        data: {
          labels: chartDateLabels,
        },
        options: chartOptions,
      });
    }
    if (!charts["accessLogTiming"]) {
      var ctx = $("#w3tcus_access_log_chart_timing")[0].getContext("2d");
      charts["accessLogTiming"] = new Chart(ctx, {
        type: "bar",
        data: {
          labels: chartDateLabels,
        },
        options: chartOptions,
      });
    }

    return {
      chartDatasets: {
        accessLogRequests: [
          {
            label: "Dynamic",
            dataColumn: ["access_log", "dynamic_count"],
            backgroundColor: "#0073aa",
          },
          {
            label: "Static",
            dataColumn: ["access_log", "static_count"],
            backgroundColor: "#0073aa",
          },
        ],
        accessLogTiming: [
          {
            label: "Dynamic",
            dataColumn: ["access_log", "dynamic_timing"],
            backgroundColor: "#0073aa",
          },
          {
            label: "Static",
            dataColumn: ["access_log", "static_timing"],
            backgroundColor: "#0073aa",
          },
        ],
      },
      preprocess: function (historyItem) {
        var dc = 0,
          sc = 0,
          dt = 0,
          st = 0;
        if (historyItem.access_log) {
          var a = historyItem.access_log;
          dc = a.dynamic_count;
          if (dc) {
            dt = (a.dynamic_timetaken_ms / dc).toFixed(2);
          }

          sc = a.static_count;
          if (sc) {
            st = (a.static_timetaken_ms / dc).toFixed(2);
          }

          historyItem["access_log"]["dynamic_timing"] = dt;
          historyItem["access_log"]["static_timing"] = st;
        }
      },
    };
  }

  /**
   * Sets up the Memcached charts.
   *
   * @param {Object} data Usage statistics data object.
   * @return {Object} Chart datasets and preprocessing function.
   */
  function setChartsMemcached(data) {
    if (!charts["memcachedSize"]) {
      var ctx = $("#w3tcus_memcached_size_chart")[0].getContext("2d");
      charts["memcachedSize"] = new Chart(ctx, {
        type: "bar",
        data: {
          labels: chartDateLabels,
        },
        options: chartOptions,
      });
    }

    if (!charts["memcachedHit"]) {
      var ctx = $("#w3tcus_memcached_hit_chart")[0].getContext("2d");
      charts["memcachedHit"] = new Chart(ctx, {
        type: "bar",
        data: {
          labels: chartDateLabels,
        },
        options: chartOptions,
      });
    }

    var prevCalls = -1;
    var prevHits = -1;

    return {
      chartDatasets: {
        memcachedSize: [
          {
            label: "MB",
            dataColumn: "memcached_size_mb",
            backgroundColor: "#0073aa",
          },
        ],
        memcachedHit: [
          {
            label: "Calls",
            dataColumn: "memcached_requests_total",
            backgroundColor: "#0073aa",
          },
          {
            label: "Hits",
            dataColumn: "memcached_requests_hits",
            backgroundColor: "green",
          },
        ],
      },
      preprocess: function (historyItem) {
        var size = 0;
        var calls = 0;
        var hits = 0;
        if (historyItem.memcached && historyItem.memcached.size_used) {
          size = (historyItem.memcached.size_used / 1024.0 / 1024.0).toFixed(2);
          if (prevCalls >= 0 && historyItem.memcached.get_calls >= prevCalls) {
            calls = historyItem.memcached.get_calls - prevCalls;
            hits = historyItem.memcached.get_hits - prevHits;
          }

          if (calls > 10000) {
            calls = 0;
            hits = 0;
          }
          prevCalls = historyItem.memcached.get_calls;
          prevHits = historyItem.memcached.get_hits;
        }

        historyItem.memcached_size_mb = size;
        historyItem.memcached_requests_total = calls;
        historyItem.memcached_requests_hits = hits;
      },
    };
  }

  /**
   * Sets up the Redis charts.
   *
   * @param {Object} data Usage statistics data object.
   * @return {Object} Chart datasets and preprocessing function.
   */
  function setChartsRedis(data) {
    if (!charts["redisSize"]) {
      var ctx = $("#w3tcus_redis_size_chart")[0].getContext("2d");
      charts["redisSize"] = new Chart(ctx, {
        type: "bar",
        data: {
          labels: chartDateLabels,
        },
        options: chartOptions,
      });
    }

    if (!charts["redisHit"]) {
      var ctx = $("#w3tcus_redis_hit_chart")[0].getContext("2d");
      charts["redisHit"] = new Chart(ctx, {
        type: "bar",
        data: {
          labels: chartDateLabels,
        },
        options: chartOptions,
      });
    }

    var prevCalls = -1;
    var prevHits = -1;

    return {
      chartDatasets: {
        redisSize: [
          {
            label: "MB",
            dataColumn: "redis_size_mb",
            backgroundColor: "#0073aa",
          },
        ],
        redisHit: [
          {
            label: "Calls",
            dataColumn: "redis_requests_total",
            backgroundColor: "#0073aa",
          },
          {
            label: "Hits",
            dataColumn: "redis_requests_hits",
            backgroundColor: "green",
          },
        ],
      },
      preprocess: function (historyItem) {
        var size = 0;
        var calls = 0;
        var hits = 0;
        if (historyItem.redis && historyItem.redis.size_used) {
          size = (historyItem.redis.size_used / 1024.0 / 1024.0).toFixed(2);
          if (prevCalls >= 0 && historyItem.redis.get_calls >= prevCalls) {
            calls = historyItem.redis.get_calls - prevCalls;
            hits = historyItem.redis.get_hits - prevHits;
          }

          if (calls > 10000) {
            calls = 0;
            hits = 0;
          }
          prevCalls = historyItem.redis.get_calls;
          prevHits = historyItem.redis.get_hits;
        }

        historyItem.redis_size_mb = size;
        historyItem.redis_requests_total = calls;
        historyItem.redis_requests_hits = hits;
      },
    };
  }

  /**
   * Sets up the APC charts.
   *
   * @param {Object} data Usage statistics data object.
   * @return {Object} Chart datasets and preprocessing function.
   */
  function setChartsApc(data) {
    if (!charts["apcSize"]) {
      var ctx = $("#w3tcus_apc_size_chart")[0].getContext("2d");
      charts["apcSize"] = new Chart(ctx, {
        type: "bar",
        data: {
          labels: chartDateLabels,
        },
        options: chartOptions,
      });
    }

    if (!charts["apcHit"]) {
      var ctx = $("#w3tcus_apc_hit_chart")[0].getContext("2d");
      charts["apcHit"] = new Chart(ctx, {
        type: "bar",
        data: {
          labels: chartDateLabels,
        },
        options: chartOptions,
      });
    }

    var prevCalls = -1;
    var prevHits = -1;

    return {
      chartDatasets: {
        apcSize: [
          {
            label: "MB",
            dataColumn: "apc_size_mb",
            backgroundColor: "#0073aa",
          },
        ],
        apcHit: [
          {
            label: "Calls",
            dataColumn: "apc_requests_total",
            backgroundColor: "#0073aa",
          },
          {
            label: "Hits",
            dataColumn: "apc_requests_hits",
            backgroundColor: "green",
          },
        ],
      },
      preprocess: function (historyItem) {
        var size = 0;
        var calls = 0;
        var hits = 0;
        if (historyItem.apc && historyItem.apc.size_used) {
          size = (historyItem.apc.size_used / 1024.0 / 1024.0).toFixed(2);
          if (prevCalls >= 0 && historyItem.apc.get_total >= prevCalls) {
            calls = historyItem.apc.get_total - prevCalls;
            hits = historyItem.apc.get_hits - prevHits;
          }

          if (calls > 10000) {
            calls = 0;
            hits = 0;
          }
          prevCalls = historyItem.apc.get_total;
          prevHits = historyItem.apc.get_hits;
        }

        historyItem.apc_size_mb = size;
        historyItem.apc_requests_total = calls;
        historyItem.apc_requests_hits = hits;
      },
    };
  }

  //
  // Utils
  //
  /**
   * Checks if a string starts with a given prefix.
   *
   * @param {string} s String to check.
   * @param {string} prefix Prefix to check for.
   * @return {boolean} True if string starts with prefix.
   */
  function startsWith(s, prefix) {
    return s && s.substr(0, prefix.length) === prefix;
  }

  /**
   * Formats a date object as HH:MM.
   *
   * @param {Date} d Date object to format.
   * @return {string} Formatted time string or empty string if invalid.
   */
  function dateFormat(d) {
    if (!d || isNaN(d.getTime())) {
      return "";
    }
    var hours = d.getUTCHours();
    var minutes = d.getUTCMinutes();
    if (isNaN(hours) || isNaN(minutes)) {
      return "";
    }
    return ("0" + hours).slice(-2) + ":" + ("0" + minutes).slice(-2);
  }

  /**
   * Sets values in the DOM based on data object.
   *
   * @param {Object} data Data object to process.
   * @param {string} css_class_prefix CSS class prefix for selectors.
   */
  function setValues(data, css_class_prefix) {
    for (var p in data) {
      var v = data[p];
      if (typeof v !== "string" && typeof v !== "number") {
        setValues(v, css_class_prefix + p + "_");
      } else {
        jQuery("." + css_class_prefix + p + " span").html(v);
        if (jQuery("." + css_class_prefix + p).hasClass("w3tcus_inline")) {
          jQuery("." + css_class_prefix + p).css("display", "inline");
        } else {
          jQuery("." + css_class_prefix + p).css("display", "block");
        }
      }
    }
  }

  var secondsTimerId;
  /**
   * Sets up auto-refresh timer for statistics.
   *
   * @param {number} newSecondsTillRefresh Seconds until next refresh.
   */
  function setRefresh(newSecondsTillRefresh) {
    clearTimeout(secondsTimerId);
    var secondsTillRefresh = newSecondsTillRefresh;

    secondsTimerId = setInterval(function () {
      secondsTillRefresh--;
      if (secondsTillRefresh <= 0) {
        clearTimeout(secondsTimerId);
        secondsTimerId = null;
        load();
        return;
      }

      jQuery(".ustats_reload").text(
        "Will be recalculated in " +
          secondsTillRefresh +
          " second" +
          (secondsTillRefresh > 1 ? "s" : ""),
      );
    }, 1000);
  }

  /**
   * Shows or hides metaboxes based on visible content.
   */
  function showMetaboxes() {
    jQuery(".metabox-holder").each(function () {
      var visible = false;
      jQuery(this)
        .find(".ustats_block")
        .each(function () {
          visible |= jQuery(this).css("display") !== "none";
        });

      jQuery(this).css("display", visible ? "" : "none");
    });
  }

  //
  // Main entry
  //
  load();

  $(".ustats_reload").click(function (e) {
    e.preventDefault();
    load();
  });
});
