<template>
  <div
    class="flex justify-between items-start bg-neutral-10 border border-neutral-300 rounded-xl px-4 w-full h-full space-x-4 overflow-y-auto overflow-x-auto scrollbar"
  >
    <!-- Range Selection -->
    <section class="py-2 space-y-2 text-sm min-w-[48%] h-full">
      <p class="h2">Range Selection</p>
      <div class="text-primary h2 flex items-center justify-start">
        <span class="material-symbols-outlined mr-2 !text-base">info</span>
        <p>Select a percentage range.</p>
      </div>
      <Chart
        type="bar"
        :data="chartData"
        :options="chartOptions"
        class="h-2/5"
      />
      <div class="px-2">
        <p class="py-4">
          Select percentage of case
          <span class="float-right">{{ caseTotalPercent }}%</span>
        </p>
        <Slider
          v-model="selectArea"
          :step="1"
          :min="0"
          :max="traceTotal"
          range
          class="mx-2"
        />
      </div>
    </section>
    <!-- Trace List -->
    <section class="h-full min-w-[48%] py-2 space-y-2">
      <p class="h2">Trace List ({{ traceTotal }})</p>
      <p class="text-primary h2 flex items-center justify-start">
        <span class="material-symbols-outlined mr-2 !text-base">info</span>Click
        trace number to see more.
      </p>
      <div
        class="overflow-y-scroll overflow-x-hidden scrollbar mx-[-8px] max-h-[calc(100%_-_96px)]"
      >
        <table class="border-separate border-spacing-x-2 text-sm w-full">
          <caption class="hidden">
            Trace list
          </caption>
          <thead class="sticky top-0 z-10 bg-neutral-10">
            <tr>
              <th class="h2 px-2 border-b border-neutral-500">Trace</th>
              <th
                class="h2 px-2 border-b border-neutral-500 text-start"
                colspan="3"
              >
                Occurrences
              </th>
            </tr>
          </thead>
          <tbody>
            <tr
              v-for="(trace, key) in traceList"
              :key="key"
              class="cursor-pointer hover:text-primary"
              @click="switchCaseData(trace.id, trace.base_count)"
            >
              <td class="p-2 text-center">#{{ trace.id }}</td>
              <td class="p-2 min-w-[96px]">
                <div
                  class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden"
                >
                  <div class="h-full bg-primary" :style="trace.value"></div>
                </div>
              </td>
              <td class="py-2 text-right">{{ trace.count }}</td>
              <td class="p-2 text-right">{{ trace.ratio }}%</td>
            </tr>
          </tbody>
        </table>
      </div>
    </section>
    <!-- Trace item Table -->
    <section v-show="showTraceId" class="pl-4 h-full min-w-full py-2 space-y-2">
      <p class="h2 mb-2">Trace #{{ showTraceId }}</p>
      <div class="h-36 w-full px-2 mb-2 border border-neutral-300 rounded">
        <div class="h-full w-full">
          <div
            id="cyTrace"
            ref="cyTraceRef"
            class="h-full min-w-full relative"
          ></div>
        </div>
      </div>
      <div
        class="overflow-y-auto overflow-x-auto scrollbar h-[calc(100%_-_200px)] infiniteTable"
        @scroll="handleScroll"
      >
        <DataTable
          :value="caseData"
          showGridlines
          tableClass="text-sm"
          breakpoint="0"
        >
          <div v-for="col in columnData" :key="col.field">
            <Column :field="col.field" :header="col.header">
              <template #body="{ data }">
                <div
                  :class="
                    data[col.field]?.length > 18
                      ? 'whitespace-normal'
                      : 'whitespace-nowrap'
                  "
                >
                  {{ data[col.field] }}
                </div>
              </template>
            </Column>
          </div>
        </DataTable>
      </div>
    </section>
  </div>
</template>

<script setup>
// The Lucia project.
// Copyright 2023-2026 DSP, inc.  All rights reserved.
// Authors:
//   chiayin.kuo@dsp.im (chiayin), 2023/1/31
/**
 * @module components/Discover/Map/Filter/Trace
 *   Trace filter with trace selection table and
 *   trace detail display.
 */

import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue";
import { storeToRefs } from "pinia";
import { useAllMapDataStore } from "@/stores/allMapData";
import { useLoadingStore } from "@/stores/loading";
import cytoscapeMapTrace from "@/module/cytoscapeMapTrace.js";
import { cloneDeep } from "lodash-es";

const emit = defineEmits(["filter-trace-selectArea"]);

const allMapDataStore = useAllMapDataStore();
const loadingStore = useLoadingStore();
const {
  infinit404,
  baseInfiniteStart,
  baseTraces,
  baseTraceTaskSeq,
  baseCases,
} = storeToRefs(allMapDataStore);
const { isLoading } = storeToRefs(loadingStore);

const processMap = ref({
  nodes: [],
  edges: [],
});
const showTraceId = ref(null);
const infinitMaxItems = ref(false);
const infiniteData = ref([]);
const infiniteFinish = ref(true); // Whether infinite scroll loading is complete
const chartOptions = ref(null);
const selectArea = ref([0, 1]);
const cyTraceRef = ref(null);
const cyTraceInstance = ref(null);

const traceTotal = computed(() => {
  return baseTraces.value.length;
});

defineExpose({ selectArea, showTraceId, traceTotal });

const traceCountTotal = computed(() => {
  return baseTraces.value
    .map((trace) => trace.count)
    .reduce((acc, cur) => acc + cur, 0);
});

const traceList = computed(() => {
  if (traceCountTotal.value === 0) return [];
  return baseTraces.value
    .map((trace) => {
      return {
        id: trace.id,
        value: progressWidth(
          Number(((trace.count / traceCountTotal.value) * 100).toFixed(1)),
        ),
        count: trace.count.toLocaleString("en-US"),
        base_count: trace.count,
        ratio: getPercentLabel(trace.count / traceCountTotal.value),
      };
    })
    .slice(selectArea.value[0], selectArea.value[1]);
});

const caseTotalPercent = computed(() => {
  if (traceCountTotal.value === 0) return "0%";
  const ratioSum =
    traceList.value
      .map((trace) => trace.base_count)
      .reduce((acc, cur) => acc + cur, 0) / traceCountTotal.value;
  return getPercentLabel(ratioSum);
});

const chartData = computed(() => {
  const start = selectArea.value[0];
  const end = selectArea.value[1] - 1;
  const labels = baseTraces.value.map((trace) => `#${trace.id}`);
  const data = baseTraces.value.map((trace) =>
    getPercentLabel(trace.count / traceCountTotal.value),
  );
  const selectAreaData = baseTraces.value.map((trace, index) =>
    index >= start && index <= end ? "rgba(0,153,255)" : "rgba(203, 213, 225)",
  );

  return {
    // Data to display
    labels,
    datasets: [
      {
        label: "Trace", // Dataset label
        data,
        backgroundColor: selectAreaData,
        categoryPercentage: 1,
        barPercentage: 1,
      },
    ],
  };
});

const caseData = computed(() => {
  const data = cloneDeep(infiniteData.value);
  data.forEach((item) => {
    item.attributes.forEach((attribute, index) => {
      item[`att_${index}`] = attribute.value; // Create a new key-value pair
    });
    delete item.attributes; // Remove the original attributes property
  });
  return data;
});

const columnData = computed(() => {
  const data = cloneDeep(baseCases.value);
  let result = [
    { field: "id", header: "Case Id" },
    { field: "started_at", header: "Start time" },
    { field: "completed_at", header: "End time" },
  ];
  if (data.length !== 0) {
    result = [
      { field: "id", header: "Case Id" },
      { field: "started_at", header: "Start time" },
      { field: "completed_at", header: "End time" },
      ...(data[0]?.attributes ?? []).map((att, index) => ({
        field: `att_${index}`,
        header: att.key,
      })),
    ];
  }
  return result;
});

watch(selectArea, (newValue, oldValue) => {
  const roundValue = Math.round(newValue[1].toFixed());
  if (newValue[1] !== roundValue) selectArea.value[1] = roundValue;
  if (newValue != oldValue) emit("filter-trace-selectArea", newValue); // Determine whether Apply should be disabled
});

watch(infinit404, (newValue) => {
  if (newValue === 404) infinitMaxItems.value = true;
});

watch(showTraceId, (newValue, oldValue) => {
  const isScrollTop = document.querySelector(".infiniteTable");
  if (isScrollTop && isScrollTop.scrollTop !== undefined)
    if (newValue !== oldValue) isScrollTop.scrollTop = 0;
});

/**
 * Set bar chart Options
 */
function barOptions() {
  return {
    maintainAspectRatio: false,
    aspectRatio: 0.8,
    layout: {
      padding: {
        top: 16,
        left: 8,
        right: 8,
      },
    },
    plugins: {
      legend: {
        // Legend
        display: false,
      },
      tooltip: {
        callbacks: {
          label: (tooltipItems) => {
            return `${tooltipItems.dataset.label}: ${tooltipItems.parsed.y}%`;
          },
        },
      },
    },
    animations: false,
    scales: {
      x: {
        display: false,
      },
      y: {
        ticks: {
          // Set tick intervals
          display: false, // Hide values, only show grid lines
          min: 0,
          max: traceList.value[0]?.ratio ?? 1,
          stepSize: (traceList.value[0]?.ratio ?? 1) / 4,
        },
        grid: {
          color: "rgba(100,116,139)",
          z: 1,
        },
        border: {
          display: false, // Hide the extra border line on the left
        },
      },
    },
  };
}

/**
 * Number to percentage
 * @param   {number} val - The raw ratio value.
 * @returns {string} The formatted percentage string.
 */
function getPercentLabel(val) {
  if (Number((val * 100).toFixed(1)) >= 100) return 100;
  else return Number.parseFloat((val * 100).toFixed(1));
}

/**
 * set progress bar width
 * @param   {number} value - The percentage value.
 * @returns {string} The CSS width style string.
 */
function progressWidth(value) {
  return `width:${value}%;`;
}

/**
 * switch case data
 * @param {number} id case id
 * @param {number} count - The total number of cases.
 */
async function switchCaseData(id, count) {
  // Do nothing if clicking the same id
  if (id === showTraceId.value) return;
  isLoading.value = true; // Always show loading screen
  try {
    infinit404.value = null;
    infinitMaxItems.value = false;
    baseInfiniteStart.value = 0;
    allMapDataStore.baseTraceId = id;
    infiniteData.value = await allMapDataStore.getBaseTraceDetail();
    showTraceId.value = id; // Set after getDetail so the case table finishes loading before switching showTraceId
    createCy();
  } catch (error) {
    console.error("Failed to load data:", error);
  } finally {
    isLoading.value = false;
  }
}

/**
 * Assembles the trace element nodes data for Cytoscape rendering.
 */
function setNodesData() {
  // Clear nodes to prevent accumulation on each render
  processMap.value.nodes = [];
  // Populate nodes with data returned from the API call
  baseTraceTaskSeq.value.forEach((node, index) => {
    processMap.value.nodes.push({
      data: {
        id: index,
        label: node,
        backgroundColor: "#CCE5FF",
        bordercolor: "#003366",
        shape: "round-rectangle",
        height: 80,
        width: 100,
      },
    });
  });
}

/**
 * Assembles the trace edge line data for Cytoscape rendering.
 */
function setEdgesData() {
  processMap.value.edges = [];
  baseTraceTaskSeq.value.forEach((edge, index) => {
    processMap.value.edges.push({
      data: {
        source: `${index}`,
        target: `${index + 1}`,
        lineWidth: 1,
        style: "solid",
      },
    });
  });
  // The number of edges is one less than the number of nodes
  processMap.value.edges.pop();
}

/**
 * create trace cytoscape's map
 */
function createCy() {
  const graphId = cyTraceRef.value;

  setNodesData();
  setEdgesData();
  cyTraceInstance.value?.destroy();
  cyTraceInstance.value = cytoscapeMapTrace(processMap.value.nodes, processMap.value.edges, graphId);
}

/**
 * Infinite scroll: listens for scroll reaching the bottom.
 * @param {Event} event - The scroll event.
 */
function handleScroll(event) {
  if (
    infinitMaxItems.value ||
    baseCases.value.length < 20 ||
    infiniteFinish.value === false
  )
    return;

  const container = event.target;
  const overScrollHeight =
    container.scrollTop + container.clientHeight >= container.scrollHeight;

  if (overScrollHeight) fetchData();
}

/**
 * Infinite scroll: loads more data when the bottom is reached.
 */
async function fetchData() {
  try {
    isLoading.value = true;
    infiniteFinish.value = false;
    baseInfiniteStart.value += 20;
    await allMapDataStore.getBaseTraceDetail();
    infiniteData.value = [...infiniteData.value, ...baseCases.value];
    infiniteFinish.value = true;
    isLoading.value = false;
  } catch (error) {
    console.error("Failed to load data:", error);
    infiniteFinish.value = true;
    isLoading.value = false;
  }
}

onMounted(() => {
  isLoading.value = true; // Will be closed after createCy finishes
  setNodesData();
  setEdgesData();
  createCy();
  chartOptions.value = barOptions();
  selectArea.value = [0, traceTotal.value];
  isLoading.value = false;
});

onBeforeUnmount(() => {
  cyTraceInstance.value?.destroy();
});
</script>

<style scoped>
@reference "../../../../assets/tailwind.css";
/* Table set */
:deep(.p-datatable-thead) {
  @apply sticky top-0 left-0 z-10 bg-neutral-10;
}
:deep(.p-datatable .p-datatable-thead > tr > th) {
  @apply !border-y-0 border-neutral-500 bg-neutral-100 after:absolute after:left-0 after:w-full after:h-full after:block after:top-0 after:border-b after:border-t after:border-neutral-500;
  white-space: nowrap;
}
:deep(.p-datatable .p-datatable-tbody > tr > td) {
  @apply border-neutral-500 !border-t-0 text-center;
}
:deep(.p-datatable.p-datatable-gridlines .p-datatable-tbody > tr > td) {
  min-width: 72px;
  max-width: 184px;
  overflow-wrap: break-word;
  word-wrap: break-word;
}
/* Center datatable header */
:deep(.p-column-header-content) {
  @apply justify-center;
}
</style>
