/*
    Парус 8 - Панели мониторинга - Редактор запросов
    Диаграмма запроса
*/

//---------------------
//Подключение библиотек
//---------------------

import React, { useState, useCallback, useEffect } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import ReactFlow, { addEdge, Controls, getOutgoers, applyNodeChanges, applyEdgeChanges } from "reactflow"; //Библиотека редактора диаграмм
import { NODE_TYPE } from "../../common"; //Общие ресурсы и константы редактора
import { Entity, ENTITY_SHAPE } from "../entity/entity"; //Сущность запроса
import { Attribute, ATTRIBUTE_SHAPE } from "../attribute/attribute"; //Атрибут сущности
import "reactflow/dist/style.css"; //Типовые стили библиотеки редактора диаграмм
import "./query_diagram.css"; //Стили компонента

//---------
//Константы
//---------

//Стили
const STYLES = {
    CONNECTION_LINE: {
        strokeWidth: 2,
        stroke: "gray"
    },
    EDGE: {
        strokeWidth: 2
    }
};

//Привязка компонтов диаграммы к типам
const NODE_TYPES_COMPONENTS = {
    [NODE_TYPE.ENTITY]: Entity,
    [NODE_TYPE.ATTRIBUTE]: Attribute
};

//Структура элемента диаграммы
const NODE_SHAPE = PropTypes.shape({
    id: PropTypes.string.isRequired,
    type: PropTypes.oneOf([NODE_TYPE.ENTITY, NODE_TYPE.ATTRIBUTE]).isRequired,
    style: PropTypes.object,
    position: PropTypes.shape({ x: PropTypes.number.isRequired, y: PropTypes.number.isRequired }),
    draggable: PropTypes.bool.isRequired,
    data: PropTypes.oneOfType([ENTITY_SHAPE, ATTRIBUTE_SHAPE])
});

//Структура связи диаграммы
const EDGE_SHAPE = PropTypes.shape({ id: PropTypes.string.isRequired, source: PropTypes.string.isRequired, target: PropTypes.string.isRequired });

//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------

//Проверка зацикленности связи
const hasCycle = (connection, target, nodes, edges, visited = new Set()) => {
    if (visited.has(target.id)) return false;
    visited.add(target.id);
    for (const outgoer of getOutgoers(target, nodes, edges))
        if (outgoer.id === connection.source || hasCycle(connection, outgoer, nodes, edges, visited)) return true;
    return false;
};

//Проверка корректности связи
const isValidConnection = (connection, nodes, edges) => {
    //Должны быть заданы источник и приёмник
    if (!connection.source || !connection.target) return false;
    //Найдем источник и приёмник
    const source = nodes.find(node => node.id === connection.source);
    const target = nodes.find(node => node.id === connection.target);
    //Приёмник и источник должны существовать
    if (!target || !source) return false;
    //Нельзя ссылаться на самого себя
    if (source?.data?.parentEntity == target?.data?.parentEntity) return false;
    //Типы данны источника и приёмника должны совпадать
    if (source?.data?.dataType != target?.data?.dataType) return false;
    //Приёмник должен не должен быть источником
    if (target.id === connection.source) return false;
    //Нельзя зацикливаться
    return !hasCycle(connection, target, nodes, edges);
};

//-----------
//Тело модуля
//-----------

//Диаграмма запроса
const QueryDiagram = ({
    nodes = [],
    edges = [],
    onEntityClick,
    onEntityAttrClick,
    onEntityPositionChange,
    onEntityRemove,
    onRelactionClick,
    onRelationAdd,
    onRelationRemove,
    onPaneClick
}) => {
    //Собственное состояние - элементы
    const [nodesCurrent, setNodesCurrent] = useState(nodes);

    //Собственное состояние - связи
    const [edgesCurrent, setEdgesCurrent] = useState(edges);

    //Собственное состояние - перемещённый элемент
    const [movedNode, setMovedNode] = useState(null);

    //При изменении элементов на диаграмме
    const handleNodesChange = useCallback(
        changes => {
            //При выборе атрибута подсветим всю сущность
            const tmpChanges = changes.reduce((prevChanges, curChanges) => {
                const tmp = { ...curChanges };
                if (tmp.type == "select") {
                    const chEnt = nodes.find(n => n.id === tmp.id);
                    if (chEnt && chEnt?.data?.parentEntity) {
                        prevChanges.push({ ...curChanges, id: chEnt.data.parentEntity });
                        tmp.selected = false;
                    }
                }
                prevChanges.push(tmp);
                return prevChanges;
            }, []);
            //Применим изменения в диаграмме
            setNodesCurrent(nodesSnapshot => applyNodeChanges(tmpChanges, nodesSnapshot));
            //Если двигали сущность - запомним начало движения
            if (changes.length == 1 && changes[0].type == "position" && changes[0].dragging)
                setMovedNode({ id: changes[0].id, position: { ...changes[0].position } });
            //Если закончили двигать сущность и ранее запомнили начало  движения - вызываем колбэки для нажатия и двиения сущности
            if (changes.length == 1 && changes[0].type == "position" && !changes[0].dragging && movedNode) {
                if (onEntityPositionChange) onEntityPositionChange(movedNode.id, movedNode.position);
                if (onEntityClick) onEntityClick(movedNode.id);
                setMovedNode(null);
            }
            //Если удалили сущность - вызываем колбэк для удаления
            if (changes[0].type == "remove" && nodes.find(n => n.id == changes[0].id && n.type == NODE_TYPE.ENTITY) && onEntityRemove)
                onEntityRemove(changes[0].id);
        },
        [movedNode, nodes, onEntityClick, onEntityPositionChange, onEntityRemove]
    );

    //При выборе элемента диаграммы
    const handleNodeClick = useCallback(
        (e, node) =>
            node?.type == NODE_TYPE.ENTITY
                ? onEntityClick && onEntityClick(node?.id)
                : onEntityAttrClick && onEntityAttrClick(node?.parentId, node?.id),
        [onEntityClick, onEntityAttrClick]
    );

    //При связывании элементов на диаграмме
    const handleConnect = connection => {
        setEdgesCurrent(state => addEdge({ ...connection, id: `${connection.source}-${connection.target}` }, state));
        onRelationAdd && onRelationAdd(connection.source, connection.target);
    };

    //При выборе связи диаграммы
    const handleEdgeClick = useCallback((e, edge) => onRelactionClick && onRelactionClick(edge.id), [onRelactionClick]);

    //При изменении связей на диаграмме
    const handleEdgesChange = useCallback(
        changes => {
            setEdgesCurrent(edgesSnapshot => applyEdgeChanges(changes, edgesSnapshot));
            if (changes.length == 1 && changes[0].type == "remove" && onRelationRemove) onRelationRemove(changes[0].id);
        },
        [onRelationRemove]
    );

    //При нажатии на холст диаграммы
    const handlePaneClick = () => onPaneClick && onPaneClick();

    //Валидация связи
    const validateConnection = connection => isValidConnection(connection, nodesCurrent, edgesCurrent);

    //Подсветка выбранной сущности

    //При изменении состава сущностей
    useEffect(() => setNodesCurrent(nodes), [nodes]);

    //При изменении состава связей
    useEffect(() => setEdgesCurrent(edges), [edges]);

    //Генерация содержимого
    return (
        <ReactFlow
            nodes={nodesCurrent}
            nodeTypes={NODE_TYPES_COMPONENTS}
            edges={edgesCurrent}
            onNodeClick={handleNodeClick}
            onNodesChange={handleNodesChange}
            onEdgeClick={handleEdgeClick}
            onEdgesChange={handleEdgesChange}
            onPaneClick={handlePaneClick}
            defaultEdgeOptions={{ style: STYLES.EDGE }}
            connectionLineStyle={STYLES.CONNECTION_LINE}
            onConnect={handleConnect}
            isValidConnection={validateConnection}
            className={"query_diagram"}
        >
            <Controls />
        </ReactFlow>
    );
};

//Контроль свойств компонента - Диаграмма запроса
QueryDiagram.propTypes = {
    nodes: PropTypes.arrayOf(NODE_SHAPE),
    edges: PropTypes.arrayOf(EDGE_SHAPE),
    onEntityClick: PropTypes.func,
    onEntityAttrClick: PropTypes.func,
    onEntityPositionChange: PropTypes.func,
    onEntityRemove: PropTypes.func,
    onRelactionClick: PropTypes.func,
    onRelationAdd: PropTypes.func,
    onRelationRemove: PropTypes.func,
    onPaneClick: PropTypes.func
};

//----------------
//Интерфейс модуля
//----------------

export { QueryDiagram };
