// 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 {
  type NodeProps,
  Position,
  type Node, useNodeConnections
} from '@xyflow/react';
import {useEffect, useRef} from "react";
import {type EditorWarning} from "../components/EditorWarnings.tsx";
import { Toolbar } from '../components/NodeComponents';
import styles from '../../VisProg.module.css';
import {SingleConnectionHandle, MultiConnectionHandle} from "../components/RuleBasedHandle.tsx";
import {allowOnlyConnectionsFromType, noSelfConnections} from "../HandleRules.ts";
import { NodeReduces, NodesInPhase, NodeTypes} from '../NodeRegistry';
import useFlowStore from '../VisProgStores';
import { TextField } from '../../../../components/TextField';

/**
 * The default data dot a phase node
 * @param label: the label of this phase
 * @param droppable: whether this node is droppable from the drop bar (initialized as true)
 * @param children: ID's of children of this node
 * @param hasReduce: whether this node has reducing functionality (true by default)
 * @param nextPhaseId:
 */
export type PhaseNodeData = {
  label: string;
  droppable: boolean;
  children: string[];
  hasReduce: boolean;
  nextPhaseId: string | "end" | null;
  isFirstPhase: boolean;
};

export type PhaseNode = Node<PhaseNodeData>

/**
 * Defines how a phase node should be rendered
 * @param props NodeProps, like id, label, children
 * @returns React.JSX.Element
 */
export default function PhaseNode(props: NodeProps<PhaseNode>) {
  const data = props.data;
  const {updateNodeData, registerWarning, unregisterWarning} = useFlowStore();
  const updateLabel = (value: string) => updateNodeData(props.id, {...data, label: value});
  const label_input_id = `phase_${props.id}_label_input`;

  const connections = useNodeConnections({
    id: props.id,
    handleType: "target",
    handleId: 'data'
  })
  
  const phaseOutCons = useNodeConnections({
    id: props.id,
    handleType: "source",
    handleId: 'source',
  })

  const phaseInCons = useNodeConnections({
    id: props.id,
    handleType: "target",
    handleId: 'target',
  })



  useEffect(() => {
    const noConnectionWarning : EditorWarning = {
      scope: {
        id: props.id,
        handleId: 'data'
      },
      type: 'MISSING_INPUT',
      severity: "WARNING",
      description: "the phaseNode has no incoming goals, norms, and/or triggers"
    }

    if (connections.length === 0) { registerWarning(noConnectionWarning); return; }
    unregisterWarning(props.id, `${noConnectionWarning.type}:data`);
  }, [connections.length, props.id, registerWarning, unregisterWarning]);

  useEffect(() => {
    const notConnectedInfo : EditorWarning = {
      scope: {
        id: props.id,
        handleId: undefined,
      },
      type: 'NOT_CONNECTED_TO_PROGRAM',
      severity: "INFO",
      description: "The PhaseNode is not connected to other nodes"
    };
    const noIncomingPhaseWarning : EditorWarning = {
      scope: {
        id: props.id,
        handleId: 'target'
      },
      type: 'MISSING_INPUT',
      severity: "WARNING",
      description: "the phaseNode has no incoming connection from a phase or the startNode"
    }
    const noOutgoingPhaseWarning : EditorWarning = {
      scope: {
        id: props.id,
        handleId: 'source'
      },
      type: 'MISSING_OUTPUT',
      severity: "WARNING",
      description: "the phaseNode has no outgoing connection to a phase or the endNode"
    }

    // register relevant warning and unregister others
    if (phaseInCons.length === 0 && phaseOutCons.length === 0) {
      registerWarning(notConnectedInfo);
      unregisterWarning(props.id, `${noOutgoingPhaseWarning.type}:${noOutgoingPhaseWarning.scope.handleId}`);
      unregisterWarning(props.id, `${noIncomingPhaseWarning.type}:${noIncomingPhaseWarning.scope.handleId}`);
      return;
    }
    if (phaseOutCons.length === 0) {
      registerWarning(noOutgoingPhaseWarning);
      unregisterWarning(props.id, `${noIncomingPhaseWarning.type}:${noIncomingPhaseWarning.scope.handleId}`);
      unregisterWarning(notConnectedInfo.scope.id, notConnectedInfo.type);
      return;
    }
    if (phaseInCons.length === 0) {
      registerWarning(noIncomingPhaseWarning);
      unregisterWarning(props.id, `${noOutgoingPhaseWarning.type}:${noOutgoingPhaseWarning.scope.handleId}`);
      unregisterWarning(notConnectedInfo.scope.id, notConnectedInfo.type);
      return;
    }
    // unregister all warnings if none should be present
    unregisterWarning(notConnectedInfo.scope.id, notConnectedInfo.type);
    unregisterWarning(props.id, `${noOutgoingPhaseWarning.type}:${noOutgoingPhaseWarning.scope.handleId}`);
    unregisterWarning(props.id, `${noIncomingPhaseWarning.type}:${noIncomingPhaseWarning.scope.handleId}`);
  }, [phaseInCons.length, phaseOutCons.length, props.id, registerWarning, unregisterWarning]);
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (ref.current) {
      const { width, height } = ref.current.getBoundingClientRect();

      console.log('Node width:', width, 'height:', height);
    }
  }, []);
  return (
    <>
      <Toolbar nodeId={props.id} allowDelete={true}/>
      <div className={`${styles.defaultNode} ${styles.nodePhase}`}>
        <div className={"flex-row gap-sm"}>
          <label htmlFor={label_input_id}>Name:</label>
          <TextField
            id={label_input_id}
            value={data.label}
            setValue={updateLabel}
            placeholder={"Phase ..."}
          />
        </div>
        <SingleConnectionHandle type="target" position={Position.Left} id="target" rules={[
          noSelfConnections,
          allowOnlyConnectionsFromType(["phase", "start"]),
        ]} title="Connect to a phase or the startNode"/>
        <MultiConnectionHandle type="target" position={Position.Bottom} id="data" rules={[
          allowOnlyConnectionsFromType(["norm", "goal", "trigger"])
        ]} title="Connect to any number of norm, goal, and TriggerNode(-s)"/>
        <SingleConnectionHandle type="source" position={Position.Right} id="source" rules={[
          noSelfConnections,
          allowOnlyConnectionsFromType(["phase", "end"]),
        ]} title="Connect to a phase or the endNode"/>
      </div>
    </>
  );
};

/**
 * Reduces each phase, including its children down into its relevant data.
 * @param node the node which is being reduced
 * @param nodes all the nodes currently in the flow.
 * @returns A collection of all reduced nodes in this phase, starting with this phases' reduced data.  
 */
export function PhaseReduce(node: Node, nodes: Node[]) {
    const thisNode = node as PhaseNode;
    const data = thisNode.data as PhaseNodeData;

    // node typings that are not in phase
    const nodesNotInPhase: string[] = Object.entries(NodesInPhase)
        .filter(([, f]) => !f())
        .map(([t]) => t);

    // node typings that then are in phase
    const nodesInPhase: string[] = Object.entries(NodeTypes)
        .filter(([t]) => !nodesNotInPhase.includes(t))
        .map(([t]) => t);

    // children nodes - make sure to check for empty arrays
    let childrenNodes: Node[] = [];
    if (data.children) 
      childrenNodes = nodes.filter((node) => data.children.includes(node.id));

    // Build the result object
    const result: Record<string, unknown> = {
        id: thisNode.id,
        name: data.label,
    };

    nodesInPhase.forEach((type) => {
        const typedChildren = childrenNodes.filter((child) => child.type == type);
        const reducer = NodeReduces[type as keyof typeof NodeReduces];
        if (!reducer) {
            console.warn(`No reducer found for node type ${type}`);
            result[type + "s"] = [];
        } else {
            result[type + "s"] = [];
            for (const typedChild of typedChildren) {
              (result[type + "s"] as object[]).push(reducer(typedChild, nodes))
            }
        }
    });

    return result;
}

export const PhaseTooltip = `
  A phase is a single stage of the program, during a phase Pepper will behave
  in accordance with any connected norms, goals and triggers`;

/**
 * This function is called whenever a connection is made with this node type as the target (phase)
 * @param _thisNode the node of this node type which function is called
 * @param _sourceNodeId the source of the received connection
 */
export function PhaseConnectionTarget(_thisNode: Node, _sourceNodeId: string) {
  const data = _thisNode.data as PhaseNodeData

  const nodes = useFlowStore.getState().nodes;
  const sourceNode = nodes.find((node) => node.id === _sourceNodeId)!
  switch (sourceNode.type) {
    case "phase": break;
    case "start": data.isFirstPhase = true; break;
    // we only add none phase or start nodes to the children
    // endNodes cannot be the source of an outgoing connection
    // so we don't need to cover them with a special case
    // before handling the default behavior
    default: data.children.push(_sourceNodeId); break;
  }
}

/**
 * This function is called whenever a connection is made with this node type as the source (phase)
 * @param _thisNode the node of this node type which function is called
 * @param _targetNodeId the target of the created connection
 */
export function PhaseConnectionSource(_thisNode: Node, _targetNodeId: string) {
  const data = _thisNode.data as PhaseNodeData
  const nodes = useFlowStore.getState().nodes;

  const targetNode = nodes.find((node) => node.id === _targetNodeId)
  if (!targetNode) {throw new Error("Source node not found")}

  // we set the nextPhaseId to the next target's id if the target is a phaseNode,
  // or "end" if the target node is the end node
  switch (targetNode.type) {
    case 'phase': data.nextPhaseId = _targetNodeId; break;
    case 'end': data.nextPhaseId = "end"; break;
    default: break;
  }
}

/**
 * This function is called whenever a connection is disconnected with this node type as the target (phase)
 * @param _thisNode the node of this node type which function is called
 * @param _sourceNodeId the source of the disconnected connection
 */
export function PhaseDisconnectionTarget(_thisNode: Node, _sourceNodeId: string) {
  const data = _thisNode.data as PhaseNodeData

  const nodes = useFlowStore.getState().nodes;
  const sourceNode = nodes.find((node) => node.id === _sourceNodeId)
  const sourceType = sourceNode ? sourceNode.type : "deleted";
  switch (sourceType) {
    case "phase": break;
    case "start": data.isFirstPhase = false; break;
    // we only add none phase or start nodes to the children
    // endNodes cannot be the source of an outgoing connection
    // so we don't need to cover them with a special case
    // before handling the default behavior
    default:
      data.children = data.children.filter((child) => { if (child != _sourceNodeId) return child; });
      break;
  }

}

/**
 * This function is called whenever a connection is disconnected with this node type as the source (phase)
 * @param _thisNode the node of this node type which function is called
 * @param _targetNodeId the target of the diconnected connection
 */
export function PhaseDisconnectionSource(_thisNode: Node, _targetNodeId: string) {
  const data = _thisNode.data as PhaseNodeData
  const nodes = useFlowStore.getState().nodes;

  // if the target is a phase or end node set the nextPhaseId to null,
  // as we are no longer connected to a subsequent phaseNode or to the endNode
  if (nodes.some((node) => node.id === _targetNodeId && ['phase', 'end'].includes(node.type!))){
    data.nextPhaseId = null;
  }
}