/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import * as d3 from "d3";
import type { Node, Call, HierarchyNode } from "@/types/topology";

export function layout(levels: Node[][], calls: Call[], radius: number) {
  // precompute level depth
  levels.forEach((l: Node[], i: number) => l.forEach((n: any) => n && (n.level = i)));

  const nodes: Node[] = levels.reduce((a, x) => a.concat(x), []);
  // layout
  const padding = 30;
  const node_height = 120;
  const node_width = 100;
  const bundle_width = 14;
  const metro_d = 4;
  for (const n of nodes) {
    n.height = 5 * metro_d;
  }

  let x_offset = padding;
  let y_offset = 0;
  for (const level of levels) {
    y_offset = 0;
    x_offset += 5 * bundle_width;
    for (const l of level) {
      const n: any = l;
      for (const call of calls) {
        if (call.source === n.id) {
          call.sourceObj = n;
        }
        if (call.target === n.id) {
          call.targetObj = n;
        }
      }
      n.x = n.level * node_width + x_offset;
      n.y = node_height + y_offset + n.height / 2;
      y_offset += node_height + n.height;
    }
  }
  const layout = {
    width: d3.max(nodes as any, (n: { x: number }) => n.x) || 0 + node_width + 2 * padding,
    height: d3.max(nodes as any, (n: { y: number }) => n.y) || 0 + node_height / 2 + 2 * padding,
  };

  return { nodes, layout, calls: computeCallPos(calls, radius) };
}

export function computeCallPos(calls: Call[], radius: number) {
  for (const [index, call] of calls.entries()) {
    const centrePoints = [
      call.sourceObj?.x || 0,
      call.sourceObj?.y || 0,
      call.targetObj?.x || 0,
      call.targetObj?.y || 0,
    ];
    for (const [idx, link] of calls.entries()) {
      if (
        index < idx &&
        call.id !== link.id &&
        call.sourceObj?.x === link.targetObj?.x &&
        call.sourceObj?.y === link.targetObj?.y &&
        call.targetObj?.x === link.sourceObj?.x &&
        call.targetObj?.y === link.sourceObj?.y
      ) {
        if (call.targetObj?.y === call.sourceObj?.y) {
          centrePoints[1] = centrePoints[1] - 8;
          centrePoints[3] = centrePoints[3] - 8;
        } else if (call.targetObj?.x === call.sourceObj?.x) {
          centrePoints[0] = centrePoints[0] - 8;
          centrePoints[2] = centrePoints[2] - 8;
        } else {
          centrePoints[1] = centrePoints[1] + 6;
          centrePoints[3] = centrePoints[3] + 6;
          centrePoints[0] = centrePoints[0] - 6;
          centrePoints[2] = centrePoints[2] - 6;
        }
      }
    }
    const pos: { x: number; y: number }[] = circleIntersection(
      centrePoints[0],
      centrePoints[1],
      radius,
      centrePoints[2],
      centrePoints[3],
      radius,
    );
    call.sourceX = pos[0].x;
    call.sourceY = pos[0].y;
    call.targetX = pos[1].x;
    call.targetY = pos[1].y;
  }
  return calls;
}

export function circleIntersection(ax: number, ay: number, ar: number, bx: number, by: number, br: number) {
  const dab = Math.sqrt(Math.pow(ax - bx, 2) + Math.pow(ay - by, 2));

  const dfx = (ar * Math.abs(ax - bx)) / dab;
  const dfy = (ar * Math.abs(ay - by)) / dab;
  const fx = bx > ax ? ax + dfx : ax - dfx;
  const fy = ay > by ? ay - dfy : ay + dfy;

  const dgx = (br * Math.abs(ax - bx)) / dab;
  const dgy = (br * Math.abs(ay - by)) / dab;
  const gx = bx > ax ? bx - dgx : bx + dgx;
  const gy = ay > by ? by + dgy : by - dgy;

  return [
    { x: fx, y: fy },
    { x: gx, y: gy },
  ];
}
function findMostFrequent(arr: Call[]) {
  const count: any = {};
  let maxCount = 0;
  let maxItem = null;

  for (let i = 0; i < arr.length; i++) {
    const item = arr[i];
    count[item.sourceObj?.id || ""] = (count[item.sourceObj?.id || ""] || 0) + 1;
    if (count[item.sourceObj?.id || ""] > maxCount) {
      maxCount = count[item.sourceObj?.id || ""];
      maxItem = item.sourceObj;
    }
    count[item.targetObj?.id || ""] = (count[item.targetObj?.id || ""] || 0) + 1;
    if (count[item.targetObj?.id || ""] > maxCount) {
      maxCount = count[item.targetObj?.id || ""];
      maxItem = item.targetObj;
    }
  }

  return maxItem;
}
export function computeLevels(calls: Call[], nodeList: Node[], arr: Node[][]) {
  const levels: Node[][] = [];
  const node = findMostFrequent(calls);
  let nodes = JSON.parse(JSON.stringify(nodeList)).sort((a: Node, b: Node) => {
    if (a.name.toLowerCase() < b.name.toLowerCase()) {
      return -1;
    }
    if (a.name.toLowerCase() > b.name.toLowerCase()) {
      return 1;
    }
    return 0;
  });
  const index = nodes.findIndex((n: Node) => n.type === "USER");
  let key = index;
  if (index < 0) {
    key = nodes.findIndex((n: Node) => n.id === node?.id);
  }
  levels.push([nodes[key]]);
  nodes = nodes.filter((_: unknown, index: number) => index !== key);
  for (const level of levels) {
    const a = [];
    for (const l of level) {
      for (const n of calls) {
        if (n.target === l.id) {
          const i = nodes.findIndex((d: Node) => d.id === n.source);
          if (i > -1 && nodes[i]) {
            a.push(nodes[i]);
            nodes = nodes.filter((_: unknown, index: number) => index !== i);
          }
        }
        if (n.source === l.id) {
          const i = nodes.findIndex((d: Node) => d.id === n.target);
          if (i > -1 && nodes[i]) {
            a.push(nodes[i]);
            nodes = nodes.filter((_: unknown, index: number) => index !== i);
          }
        }
      }
    }
    if (a.length) {
      levels.push(a);
    }
  }
  const list = levels.length > arr.length ? levels : arr;
  const subList = levels.length > arr.length ? arr : levels;
  arr = list.map((subArray: Node[], index: number) => {
    if (subList[index]) {
      return subArray.concat(subList[index]);
    } else {
      return subArray;
    }
  });

  if (nodes.length) {
    const ids = nodes.map((d: Node) => d.id);
    const links = calls.filter((item: Call) => ids.includes(item.source) || ids.includes(item.target));
    arr = computeLevels(links, nodes, arr);
  }
  return arr;
}
export function changeNode(d: { x: number; y: number }, currentNode: Nullable<Node>, layout: any, radius: number) {
  if (!currentNode) {
    return;
  }
  for (const node of layout.nodes) {
    if (node.id === currentNode.id) {
      node.x = d.x;
      node.y = d.y;
    }
  }
  for (const call of layout.calls) {
    if (call.sourceObj.id === currentNode.id) {
      call.sourceObj.x = d.x;
      call.sourceObj.y = d.y;
    }
    if (call.targetObj.id === currentNode.id) {
      call.targetObj.x = d.x;
      call.targetObj.y = d.y;
    }
    if (call.targetObj.id === currentNode.id || call.sourceObj.id === currentNode.id) {
      const pos: any = circleIntersection(
        call.sourceObj.x,
        call.sourceObj.y,
        radius,
        call.targetObj.x,
        call.targetObj.y,
        radius,
      );
      call.sourceX = pos[0].x;
      call.sourceY = pos[0].y;
      call.targetX = pos[1].x;
      call.targetY = pos[1].y;
    }
  }
  return computeCallPos(layout.calls, radius);
}
export function hierarchy(levels: Node[][], calls: Call[], radius: number) {
  // precompute level depth
  levels.forEach((l: Node[], i: number) => l.forEach((n: Node) => n && (n.level = i)));

  const nodes: Node[] = levels.reduce((a, x) => a.concat(x), []);
  // layout
  const padding = 30;
  const node_height = 100;
  const node_width = 180;
  const bundle_width = 10;
  const metro_d = 4;
  for (const n of nodes) {
    n.width = 5 * metro_d;
  }

  let y_offset = padding;
  let x_offset = 0;
  for (const level of levels) {
    x_offset = 0;
    y_offset += 3 * bundle_width;
    for (const l of level) {
      const n: any = l;
      for (const call of calls) {
        if (call.source === n.id) {
          call.sourceObj = n;
        }
        if (call.target === n.id) {
          call.targetObj = n;
        }
      }
      n.x = node_width + x_offset + n.width / 2;
      n.y = n.level * node_height + y_offset;
      x_offset += node_width + n.width;
    }
  }
  const layout = {
    width: d3.max(nodes as any, (n: { x: number }) => n.x) || 0 + node_width + 2 * padding,
    height: d3.max(nodes as any, (n: { y: number }) => n.y) || 0 + node_height / 2 + 2 * padding,
  };

  return { nodes, layout, calls: computeCallPos(calls, radius) };
}
export function computeHierarchyLevels(nodes: (Node | HierarchyNode)[]) {
  const levelsNum: number[] = nodes.map((d: Node | HierarchyNode) => (d as Node).l || 0);
  const list = [...new Set(levelsNum)];
  const sortedArr = list.sort((a, b) => b - a);
  const nodesList = [];

  for (const min of sortedArr) {
    const arr = nodes.filter((d) => (d as Node).l === min);
    nodesList.push(arr);
  }

  return nodesList;
}
