/**
 * This program has been developed by students from the bachelor Computer Science at
 * Utrecht University within the Software Project course.
 * © Copyright Utrecht University (Department of Information and Computing Sciences)
 */
import { GraphType, LinkType, NodeType } from '../types';
import { Edge, Node, GraphQueryResult } from '../../../../data-access/store';
import { ML } from '../../../../data-access/store/mlSlice';
import { processML } from './NLMachineLearning';
/** ResultNodeLinkParserUseCase implements methods to parse and translate websocket messages from the backend into a GraphType. */

/**
 * This program has been developed by students from the bachelor Computer Science at
 * Utrecht University within the Software Project course.
 * © Copyright Utrecht University (Department of Information and Computing Sciences)
 */
/** A node link data-type for a query result object from the backend. */
// export type NodeLinkResultType = { DEPRECATED USE GraphQueryResult
//   nodes: Node[];
//   edges: Link[];
//   mlEdges?: Link[];
// };

/** Typing for nodes and links in the node-link result. Nodes and links should always have an id and attributes. */
// export interface AxisType {
//   id: string;
//   attributes: Record<string, any>;
//   mldata?: Record<string, string[]> | number; // This is shortest path data . This name is needs to be changed together with backend TODO: Change this.
// }

/** Typing for a node in the node-link result */
// export type Node = AxisType;

/** Typing for a link in the node-link result */
// export interface Link extends AxisType {
//   from: string;
//   to: string;
// }

export type AxisType = Node | Edge;

/** Gets the group to which the node/edge belongs */
export function getGroupName(axisType: AxisType): string {
  // FIXME: only works in arangodb
  return axisType.label;
}

/** Returns true if the given id belongs to the target group. */
export function isNotInGroup(nodeOrEdge: AxisType, targetGroup: string): boolean {
  return getGroupName(nodeOrEdge) != targetGroup;
}

/** Checks if a query result form the backend contains valid NodeLinkResultType data.
 * @param {any} jsonObject The query result object received from the frontend.
 * @returns True and the jsonObject will be casted, false if the jsonObject did not contain all the data fields.
 */
export function isNodeLinkResult(jsonObject: any): jsonObject is GraphQueryResult {
  if (typeof jsonObject === 'object' && jsonObject !== null && 'nodes' in jsonObject && 'edges' in jsonObject) {
    if (!Array.isArray(jsonObject.nodes) || !Array.isArray(jsonObject.edges)) return false;

    const validNodes = jsonObject.nodes.every((node: any) => 'id' in node && 'attributes' in node);
    const validEdges = jsonObject.edges.every((edge: any) => 'from' in edge && 'to' in edge);

    return validNodes && validEdges;
  } else return false;
}

/** Returns a record with a type of the nodes as key and a number that represents how many times this type is present in the nodeLinkResult as value. */
export function getNodeTypes(nodeLinkResult: GraphQueryResult): Record<string, number> {
  const types: Record<string, number> = {};

  nodeLinkResult.nodes.forEach((node) => {
    const type = getGroupName(node);
    if (types[type] != undefined) types[type]++;
    else types[type] = 0;
  });

  return types;
}

export type UniqueEdge = {
  from: string;
  to: string;
  count: number;
  attributes: Record<string, any>;
};

/**
 * Parse a message (containing query result) edges to unique edges.
 * @param {Link[]} queryResultEdges Edges from a query result.
 * @param {boolean} isLinkPredictionData True if parsing LinkPredictionData, false otherwise.
 * @returns {UniqueEdge[]} Unique edges with a count property added.
 */
export function parseToUniqueEdges(queryResultEdges: Edge[], isLinkPredictionData: boolean): UniqueEdge[] {
  // Edges to be returned
  const edges: UniqueEdge[] = [];

  // Collect the edges in map, to only keep unique edges
  // And count the number of same edges
  const edgesMap = new Map<string, number>();
  const attriMap = new Map<string, Record<string, any>>();
  if (queryResultEdges != null) {
    if (!isLinkPredictionData) {
      for (let j = 0; j < queryResultEdges.length; j++) {
        const newLink = queryResultEdges[j].from + ':' + queryResultEdges[j].to;
        edgesMap.set(newLink, (edgesMap.get(newLink) || 0) + 1);
        attriMap.set(newLink, queryResultEdges[j].attributes);
      }

      edgesMap.forEach((count, key) => {
        const fromTo = key.split(':');
        edges.push({
          from: fromTo[0],
          to: fromTo[1],
          count: count,
          attributes: attriMap.get(key) ?? [],
        });
      });
    } else {
      for (let i = 0; i < queryResultEdges.length; i++) {
        edges.push({
          from: queryResultEdges[i].from,
          to: queryResultEdges[i].to,
          count: queryResultEdges[i].attributes.jaccard_coefficient as number,
          attributes: queryResultEdges[i].attributes,
        });
      }
    }
  }
  return edges;
}

type OptionsI = {
  defaultX?: number;
  defaultY?: number;
  defaultRadius?: number;
};

/**
 * Parse a websocket message containing a query result into a node link GraphType.
 * @param {any} queryResult An incoming query result from the websocket.
 * @returns {GraphType} A node-link graph containing the nodes and links for the diagram.
 */
export function parseQueryResult(queryResult: GraphQueryResult, ml: ML, options: OptionsI = {}): GraphType {
  let ret: GraphType = {
    nodes: {},
    links: {},
  };

  const typeDict: { [key: string]: number } = {};
  // Counter for the types
  let counter = 1;
  // Entry to keep track of the number of machine learning clusters
  let numberOfMlClusters = 0; // TODO

  let communityDetectionInResult = false;
  let shortestPathInResult = false;
  let linkPredictionInResult = false;
  for (let i = 0; i < queryResult.nodes.length; i++) {
    // Assigns a group to every entity type for color coding
    const nodeId = queryResult.nodes[i]._id;
    // for datasets without label, label is included in id. eg. "kamerleden/112"
    //const entityType = queryResult.nodes[i].label;
    const node = queryResult.nodes[i];
    const entityType: string = node.label;

    // The preferred text to be shown on top of the node
    let preferredText = nodeId;
    let typeNumber = 1;

    // Check if entity is already seen by the dictionary
    if (entityType in typeDict) typeNumber = typeDict[entityType];
    else {
      typeDict[entityType] = counter;
      typeNumber = counter;
      counter++;
    }

    // TODO: this should be a setting
    // Check to see if node has a "naam" attribute and set prefText to it
    if (queryResult.nodes[i].attributes.name !== undefined) preferredText = queryResult.nodes[i].attributes.name as string;
    if (queryResult.nodes[i].attributes.label !== undefined) preferredText = queryResult.nodes[i].attributes.label as string;
    if (queryResult.nodes[i].attributes.naam !== undefined) preferredText = queryResult.nodes[i].attributes.naam as string;

    //const labelTemplate = queryResult.nodes[i].label == undefined ? queryResult.nodes[i]._id.split('/')[0] : queryResult.nodes[i].label;

    let radius = options.defaultRadius || 5;
    let data: NodeType = {
      _id: queryResult.nodes[i]._id,
      label: entityType,
      attributes: queryResult.nodes[i].attributes,
      type: typeNumber,
      displayInfo: preferredText,
      radius: radius,
      defaultX: (options.defaultX || 0) + Math.random() * radius * 20 - radius * 10,
      defaultY: (options.defaultY || 0) + Math.random() * radius * 20 - radius * 10,
    };

    // let mlExtra = {};
    // if (queryResult.nodes[i].mldata && typeof queryResult.nodes[i].mldata != 'number') { // TODO FIXME: this is somewhere else now
    //   mlExtra = {
    //     shortestPathData: queryResult.nodes[i].mldata as Record<string, string[]>,
    //   };
    //   shortestPathInResult = true;
    // } else if (typeof queryResult.nodes[i].mldata == 'number') {
    //   // mldata + 1 so you dont get 0, which is interpreted as 'undefined'
    //   const numberOfCluster = (queryResult.nodes[i].mldata as number) + 1;
    //   mlExtra = {
    //     cluster: numberOfCluster,
    //     clusterAccoringToMLData: numberOfCluster,
    //   };
    //   communityDetectionInResult = true;
    //   if (numberOfCluster > numberOfMlClusters) {
    //     numberOfMlClusters = numberOfCluster;
    //   }
    // }

    // Add mlExtra to the node if necessary
    // data = { ...data, ...mlExtra };
    ret.nodes[data._id] = data;
  }

  // Filter unique edges and transform to LinkTypes
  // List for all links
  let links: LinkType[] = [];
  let allNodeIds = new Set(Object.keys(ret.nodes));

  // Parse ml edges
  //   if (ml != undefined) {
  //     ml?.linkPrediction?.forEach((link) => {
  //       if (allNodeIds.has(link.from) && allNodeIds.has(link.to)) {
  //         const toAdd: LinkType = {
  //           source: link.from,
  //           target: link.to,
  //           value: link.attributes.jaccard_coefficient as number,
  //           mlEdge: true,
  //           color: 0x000000,
  //         };
  //         links.push(toAdd);
  //       }
  //       linkPredictionInResult = true;
  //     });
  //   }

  // Parse normal edges
  const uniqueEdges = parseToUniqueEdges(queryResult.edges, false);
  for (let i = 0; i < uniqueEdges.length; i++) {
    if (allNodeIds.has(uniqueEdges[i].from) && allNodeIds.has(uniqueEdges[i].to)) {
      const toAdd: LinkType = {
        id: uniqueEdges[i].from + ':' + uniqueEdges[i].to, // TODO: this only supports one link between two nodes
        source: uniqueEdges[i].from,
        target: uniqueEdges[i].to,
        value: uniqueEdges[i].count,
        name: uniqueEdges[i].attributes.Type,
        mlEdge: false,
        color: 0x000000,
        attributes: uniqueEdges[i].attributes,
      };
      ret.links[toAdd.id] = toAdd;
    }
  }

  //TODO: is this in use?
  const maxCount = links.reduce(
    (previousValue, currentValue) => (currentValue.value > previousValue ? currentValue.value : previousValue),
    -1,
  );
  //TODO: is this in use?
  // Scale the value from 0 to 50
  const maxLineWidth = 50;
  if (maxCount > maxLineWidth) {
    links.forEach((link) => {
      link.value = (link.value / maxCount) * maxLineWidth;
      link.value = link.value < 1 ? 1 : link.value;
    });
  }

  // Graph to be returned
  // let toBeReturned: GraphType = {
  //   nodes: nodes,
  //   links: links,
  // linkPrediction: linkPredictionInResult,
  // shortestPath: shortestPathInResult,
  // communityDetection: communityDetectionInResult,
  // };

  // If query with community detection; add number of clusters to the graph
  // const numberOfClusters = {
  //   numberOfMlClusters: numberOfMlClusters,
  // };
  // if (communityDetectionInResult) {
  //   toBeReturned = { ...toBeReturned, ...numberOfClusters };
  // }

  // return toBeReturned;
  return processML(ml, ret);
}