/*
    Парус 8 - Панели мониторинга - Редактор панелей
    Компоненты: Хуки компонентов
*/

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

import { useState, useEffect, useContext, useCallback, useLayoutEffect } from "react"; //Классы React
import { BackEndCtx } from "../../../context/backend"; //Контекст взаимодействия с сервером
import { MessagingCtx } from "../../../context/messaging"; //Контекст сообщений
import { object2Base64XML, genUID, xml2JSON } from "../../../core/utils"; //Вспомогательные функции
import { exportXMLFile } from "../layouts"; //Дополнительная разметка и вёрстка клиентских элементов
import { P8P_COMPONENT_SETTINGS_PATHS } from "../../../components/editors/p8p_component_settings"; //Дополнительные настройки источников
import { getActionsVariables } from "../../../components/editors/p8p_component_action/util"; //Вспомогательный функционал действий компонентов
import { P8P_CA_TYPE } from "../../../components/editors/p8p_component_action/common"; //Общие ресурсы действий

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

//Начальное состояние размера макета
const INITIAL_BREAKPOINT = "lg";

//Начальное состояние макета
const INITIAL_LAYOUTS = {
    [INITIAL_BREAKPOINT]: []
};

//---------
//Константы расчета высоты строки ResponsiveGridLayout
//---------

//Структура параметров для расчета высоты строки ResponsiveGridLayout
const GRID_LAYOUT_PARAMS = {
    //Высота главного меню (px)
    APP_BAR_HEIGHT: 64,
    //Дополнительный отступ (px)
    SAFE_OFFSET: 20,
    //Максимальное количество строк (число)
    MAX_ROWS: 68,
    //Высота отступа (px)
    MARGIN: 10
};

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

//Проверка на массив загружаемых данных панели
const isArrayPanelDesc = (name, jPath) =>
    ["items", "arguments", "conditions", "actions", "dependencies", "inputParams"].includes(name) || /(.*)XLAYOUTS\.[^.]*$/.test(jPath);

//Обработка значений тэгов загружаемых данных панели
const tagValueProcessorPanelDesc = (name, val, jPath) =>
    ["condValue", "resValue", "description"].includes(name)
        ? undefined
        : /(.*)dataSource.arguments.value$/.test(jPath) || /(.*)XVALUE_PROVIDERS(.*)$/.test(jPath)
          ? undefined
          : val;

//Конвертация серверного описания компонентов в данные для редактора панелей
const convertServerData2Components = components => {
    //Корректировка информации о действия (Для типа "setVariable" значение ключа "params" - обязательно массив, в иных случаях - объект)
    const correctionActions = actions => {
        return actions.reduce(
            (prevActions, action) => [
                ...prevActions,
                {
                    ...(action.type === P8P_CA_TYPE.setVariable.code && !Array.isArray(action.params)
                        ? { ...action, params: [{ ...action.params }] }
                        : { ...action })
                }
            ],
            []
        );
    };

    //Форматируем устанавливая пустой объект для dataSource, если он пуст
    return Object.keys(components).reduce(
        (prev, cur) => ({
            ...prev,
            [cur]: {
                ...components[cur],
                settings: {
                    ...components[cur].settings,
                    //dataSource - обязательно объект
                    ...(components[cur].settings?.dataSource ? { dataSource: components[cur].settings.dataSource || {} } : {}),
                    //actions - требуют корректировки
                    ...(components[cur].settings?.actions
                        ? {
                              actions: correctionActions(components[cur].settings.actions)
                          }
                        : {})
                }
            }
        }),
        {}
    );
};

//Конвертация серверного описания проводников значений в данные для редактора панелей
const convertServerData2ValueProviders = valueProviders => {
    //Форматируем инициализируя dependencies, если требуется
    return Object.keys(valueProviders).reduce(
        (prev, cur) => ({ ...prev, [cur]: { ...valueProviders[cur], dependencies: valueProviders[cur].dependencies || [] } }),
        {}
    );
};

//Конвертация серверного описания панели в данные для редактора панелей
const serverPanelData2PanelDesc = (components, valueProviders, layouts, breakpoint) => {
    //Возвращаем информацию о панеле с учетом конвертаций
    return {
        components: convertServerData2Components(components),
        valueProviders: convertServerData2ValueProviders(valueProviders),
        layouts: layouts[breakpoint] ? { [breakpoint]: [...layouts[breakpoint]] } : INITIAL_LAYOUTS,
        breakpoint: breakpoint
    };
};

//Считывание общего списка зависимостей от проводника значений
const getValueProvidersLinks = (componentPath, settings) => {
    //Если это индикатор/график/таблица
    if ([P8P_COMPONENT_SETTINGS_PATHS.INDICATOR, P8P_COMPONENT_SETTINGS_PATHS.CHART, P8P_COMPONENT_SETTINGS_PATHS.TABLE].includes(componentPath)) {
        //Собираем зависимости из настройки источника
        let argumentsSources = Array.isArray(settings?.dataSource?.arguments)
            ? settings.dataSource.arguments.reduce((prev, cur) => (cur.valueSource ? [...prev, cur.valueSource] : [...prev]), [])
            : [];
        //Собираем зависимости из параметров действий
        let actionsSources = Array.isArray(settings?.actions) ? getActionsVariables(settings.actions) : [];
        //Возвращаем зависимости компонента
        return [...new Set([...argumentsSources, ...actionsSources])];
    }
    //Если это форма
    if (P8P_COMPONENT_SETTINGS_PATHS.FORM === componentPath) {
        //Собираем зависимости из элементов формы
        let items = Array.isArray(settings?.items) ? settings.items.map(item => item.name) : [];
        //Возвращаем зависимости компонента
        return [...new Set(items)];
    }
};

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

//Отложенная загрузка модуля компонента (как альтернативу можно применять React.lazy)
const useComponentModule = ({ path = null, module = "view" } = {}) => {
    //Собственное состояние - импортированный модуль компонента
    const [componentModule, setComponentModule] = useState(null);

    //Собственное состояние - флаг готовности
    const [init, setInit] = useState(false);

    //При подмонтировании к странице
    useEffect(() => {
        //Динамическая загрузка модуля компонента из библиотеки
        const importComponentModule = async () => {
            setInit(false);
            const moduleContent = await import(`./${path}/${module}`);
            setComponentModule(moduleContent);
            setInit(true);
        };
        if (path) importComponentModule();
    }, [path, module]);

    //Возвращаем интерфейс хука
    return [componentModule, init];
};

//Работа с панелью
const usePanel = () => {
    //Собственное состояние - рабочая панель
    const [panel, setPanel] = useState();
    //Собственное состояние - имя рабочей панели
    const [panelName, setPanelName] = useState(null);
    //Собственное состояние - наличие изменений рабочей панели
    const [isPanelChanged, setIsPanelChanged] = useState(false);
    //Собственное состояние - возможность редактирования
    const [isEditAvaliable, setIsEditAvaliable] = useState(true);
    //Собственное состояние - режим редактирования
    const [editMode, setEditMode] = useState(true);

    //Подключение к контексту взаимодействия с сервером
    const { executeStored } = useContext(BackEndCtx);

    //При необходимости загрузки информации о панели
    const loadPanel = async panel => {
        //Считываем информацию с сервера
        const res = await executeStored({
            stored: "PKG_P8PANELS_PE.PANEL_ATTRS_GET_BY_CODE",
            args: {
                SCODE: panel
            },
            respArg: "COUT",
            loader: true
        });
        //Устанавливаем рабочую панель
        setPanel(res.XPANEL_ATTRS.rn);
        //Устанавливаем наименование рабочей панели
        setPanelName(res.XPANEL_ATTRS.name);
        //Сбрасываем наличие изменений рабочей панели
        setIsPanelChanged(false);
        //Загружаемую панель запрещено редактировать
        setIsEditAvaliable(false);
        setEditMode(false);
    };

    //При выборе панели
    const selectPanel = (panel, panelName, isPanelEditAvaliable) => {
        //Обновляем информацию о панели
        setPanel(panel);
        setPanelName(panelName);
        setIsEditAvaliable(isPanelEditAvaliable);
        setEditMode(isPanelEditAvaliable);
        setIsPanelChanged(false);
    };

    //При закрытии панели
    const closePanel = () => {
        setPanel(null);
        setPanelName(null);
        setIsPanelChanged(false);
    };

    //При установке признака изменений панели
    const setPanelChanged = isChanged => setIsPanelChanged(isChanged);

    //При установке признака режима редактирования
    const changeEditMode = isEditMode => setEditMode(isEditMode);

    //Возвращаем интерфейс хука
    return [panel, panelName, editMode, isEditAvaliable, isPanelChanged, loadPanel, selectPanel, closePanel, changeEditMode, setPanelChanged];
};

//Работа с менеджером панелей
const usePanelManager = () => {
    //Собственное состояние - флаг необходимости обновления
    const [refresh, setRefresh] = useState(true);

    //Собственное состояние - данные
    const [data, setData] = useState(null);

    //Подключение к контексту взаимодействия с сервером
    const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndCtx);

    //Добавление панели
    const insertPanel = useCallback(
        async (code, name) => {
            await executeStored({ stored: "PKG_P8PANELS_PE.PANEL_INSERT", args: { SCODE: code, SNAME: name }, loader: false });
            setRefresh(true);
        },
        [executeStored]
    );

    //Изменение панели
    const updatePanel = useCallback(
        async (query, code, name) => {
            await executeStored({ stored: "PKG_P8PANELS_PE.PANEL_UPDATE", args: { NRN: query, SCODE: code, SNAME: name }, loader: false });
            setRefresh(true);
        },
        [executeStored]
    );

    //Удаление панели
    const deletePanel = useCallback(
        async query => {
            await executeStored({ stored: "PKG_P8PANELS_PE.PANEL_DELETE", args: { NRN: query }, loader: false });
            setRefresh(true);
        },
        [executeStored]
    );

    //Установка флага готовности панели
    const setPanelReady = useCallback(
        async (query, ready) => {
            await executeStored({ stored: "PKG_P8PANELS_PE.PANEL_READY_SET", args: { NRN: query, NREADY: ready }, loader: false });
            setRefresh(true);
        },
        [executeStored]
    );

    //Установка флага публичности панели
    const setPanelPbl = useCallback(
        async (query, pbl) => {
            await executeStored({ stored: "PKG_P8PANELS_PE.PANEL_PBL_SET", args: { NRN: query, NPBL: pbl }, loader: false });
            setRefresh(true);
        },
        [executeStored]
    );

    //Импорт новой панели
    const importPanel = useCallback(
        async fileData => {
            await executeStored({
                stored: "PKG_P8PANELS_PE.PANEL_IMPORT",
                args: {
                    CPANEL: {
                        //Форматируем данные документа в base64
                        VALUE: btoa(unescape(encodeURIComponent(fileData))),
                        SDATA_TYPE: SERV_DATA_TYPE_CLOB
                    }
                },
                loader: true
            });
            setRefresh(true);
        },
        [SERV_DATA_TYPE_CLOB, executeStored]
    );

    //При необходимости получить/обновить данные
    useEffect(() => {
        //Загрузка данных с сервера
        const loadData = async () => {
            try {
                const data = await executeStored({
                    stored: "PKG_P8PANELS_PE.PANEL_LIST",
                    respArg: "COUT",
                    isArray: name => ["XPANEL"].includes(name),
                    attributeValueProcessor: (name, val) => (["code", "name"].includes(name) ? undefined : val),
                    loader: true
                });
                setData(data?.XPANELS?.XPANEL || []);
            } finally {
                setRefresh(false);
            }
        };
        //Если надо обновить
        if (refresh)
            //Получим данные
            loadData();
    }, [refresh, executeStored]);

    //Возвращаем интерфейс хука
    return [data, insertPanel, updatePanel, deletePanel, setPanelReady, setPanelPbl, importPanel];
};

//Работа с содержимым панели
const usePanelDesc = panel => {
    //Собственное состояние - флаг инициализированности
    const [isInit, setInit] = useState(false);

    //Собственное состояние - флаг необходимости обновления
    const [refresh, setRefresh] = useState(true);

    //Собственное состояние - данные
    const [data, setData] = useState({
        components: {},
        valueProviders: {},
        layouts: INITIAL_LAYOUTS,
        breakpoint: INITIAL_BREAKPOINT
    });

    //Подключение к контексту взаимодействия с сервером
    const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndCtx);

    //Подключение к контексту сообщений
    const { showMsgErr } = useContext(MessagingCtx);

    //Считывание базовой информации о панели
    const getPanelInfo = useCallback(async () => {
        //Считываем информацию с сервера
        const res = await executeStored({
            stored: "PKG_P8PANELS_PE.PANEL_ATTRS_GET",
            args: {
                NRN: panel
            },
            respArg: "COUT",
            loader: true
        });
        //Забираем все без RN
        // eslint-disable-next-line no-unused-vars
        const { rn, ...panelAttrs } = res.XPANEL_ATTRS;
        //Возвращаем результат
        return panelAttrs;
    }, [executeStored, panel]);

    //Формирования данных панели для выгрузки
    const makeXMLPanelDesc = useCallback(
        async (includePanelInfo = false, isBase64 = true) => {
            //Считываем информацию о выгружаемой панели
            const panelInfo = includePanelInfo ? await getPanelInfo() : {};
            //Сформируем данные в формат XML
            const xmlData = object2Base64XML(
                {
                    XPANEL: {
                        ...(includePanelInfo ? { XPANEL_INFO: panelInfo } : {}),
                        XCOMPONENTS: data.components,
                        XVALUE_PROVIDERS: data.valueProviders,
                        XLAYOUTS: data.layouts,
                        XOPTIONS: {
                            breakpoint: data.breakpoint
                        }
                    }
                },
                { suppressEmptyNode: false }
            );
            //Возвращаем данные в формате XML (формат исходит от признака)
            return isBase64 ? xmlData : decodeURIComponent(escape(atob(xmlData)));
        },
        [data.breakpoint, data.components, data.layouts, data.valueProviders, getPanelInfo]
    );

    //Добавление компонента в макет
    const addComponent = component => {
        //Генерируем ID
        const id = genUID(component.path);
        //Добавляем компонент и его макет
        setData(pv => ({
            ...pv,
            components: { ...pv.components, [id]: { ...component, settings: { ...component.settings, id: id } } },
            layouts: { ...pv.layouts, [data.breakpoint]: [...pv.layouts[data.breakpoint], { i: id, x: 0, y: 0, w: 4, h: 10 }] }
        }));
    };

    //Удаление компонента из макета
    const deleteComponent = id => {
        //Удаляем все старые зависимости от компонента
        const newValueProviders = Object.keys(data.valueProviders).reduce(
            (prev, cur) => ({
                ...prev,
                [cur]: { ...data.valueProviders[cur], dependencies: data.valueProviders[cur].dependencies.filter(el => el !== id) }
            }),
            {}
        );
        //Обновляем данные
        setData(pv => ({
            ...pv,
            layouts: { ...pv.layouts, [data.breakpoint]: data.layouts[data.breakpoint].filter(item => item.i !== id) },
            components: { ...pv.components, [id]: { ...pv.components[id], deleted: true } },
            valueProviders: { ...newValueProviders }
        }));
    };

    //Изменение размера холста
    const breakpointChange = breakpoint => setData(pv => ({ ...pv, breakpoint: breakpoint }));

    //Изменение состояния макета
    const layoutsChange = layouts => setData(pv => ({ ...pv, layouts: layouts }));

    //Изменение значений в компоненте
    const changeValueProviders = values => {
        //Считываем проводники
        const newValueProviders = { ...data.valueProviders };
        //Переносим новые значения в проводники
        Object.keys(values).map(el => (newValueProviders[el].value = values[el]));
        //Обновляем проводники
        setData(pv => ({ ...pv, valueProviders: { ...newValueProviders } }));
    };

    //Изменение настроек компонента
    const changeComponentSettings = (id = null, settings = {}, onChanged = null) => {
        //Считываем новые зависимости компонента
        const providedValues = getValueProvidersLinks(data.components[id].path, settings);
        //Удаляем все старые зависимости от компонента
        const newValueProviders = Object.keys(data.valueProviders).reduce(
            (prev, cur) => ({
                ...prev,
                [cur]: { ...data.valueProviders[cur], dependencies: data.valueProviders[cur].dependencies.filter(el => el !== id) }
            }),
            {}
        );
        //Добавляем новые зависимости
        providedValues.map(providerName => newValueProviders[providerName].dependencies.push(id));
        //Обновляем данные
        setData(pv => ({
            ...pv,
            components: { ...pv.components, [id]: { ...pv.components[id], settings: { ...settings } } },
            valueProviders: { ...newValueProviders }
        }));
        //Выполняем действия после изменения
        onChanged && onChanged();
    };

    //Изменение настроек панели
    const changePanelSettings = valueProviders => {
        //Обновляем данные
        setData(pv => ({
            ...pv,
            valueProviders: { ...valueProviders }
        }));
    };

    //Сохранение описания панели
    const savePanelDesc = useCallback(
        async (callBack = null) => {
            try {
                await executeStored({
                    stored: "PKG_P8PANELS_PE.PANEL_DESC_SET",
                    args: {
                        NRN: panel,
                        CPANEL: {
                            VALUE: await makeXMLPanelDesc(),
                            SDATA_TYPE: SERV_DATA_TYPE_CLOB
                        }
                    },
                    loader: true
                });
                callBack && callBack(false);
            } catch (e) {
                callBack && callBack(true);
            }
        },
        [SERV_DATA_TYPE_CLOB, executeStored, makeXMLPanelDesc, panel]
    );

    //Загрузка описания панели из файла
    const importPanelDesc = async (fileData, callBack = null) => {
        //Формируем данные панели
        const panelData = await xml2JSON({
            xmlDoc: fileData,
            isArray: isArrayPanelDesc,
            tagValueProcessor: tagValueProcessorPanelDesc
        });
        //Если файл содержит тэг XPANEL
        if (panelData.XPANEL) {
            setData(
                serverPanelData2PanelDesc(
                    panelData.XPANEL?.XCOMPONENTS || {},
                    panelData.XPANEL?.XVALUE_PROVIDERS || {},
                    panelData.XPANEL?.XLAYOUTS || {},
                    panelData.XPANEL?.XOPTIONS?.breakpoint || INITIAL_BREAKPOINT
                )
            );
            callBack && callBack(true);
        } else {
            showMsgErr("Загружаемые данные не соответствуют формату настройки панели.");
            callBack && callBack(false);
        }
    };

    //Выгрузка панели в файл
    const exportPanelDesc = async panelName => {
        //Формируем XML-представление панели
        const xmlPanelDesc = await makeXMLPanelDesc(true, false);
        //Выгружаем в файл
        exportXMLFile(xmlPanelDesc, panelName);
    };

    //При необходимости получить/обновить данные
    useEffect(() => {
        //Загрузка данных с сервера
        const loadData = async () => {
            try {
                const data = await executeStored({
                    stored: "PKG_P8PANELS_PE.PANEL_DESC_GET",
                    args: { NRN: panel },
                    respArg: "COUT",
                    isArray: isArrayPanelDesc,
                    tagValueProcessor: tagValueProcessorPanelDesc,
                    loader: true
                });
                setData(
                    serverPanelData2PanelDesc(
                        data?.XCOMPONENTS || {},
                        data?.XVALUE_PROVIDERS || {},
                        data?.XLAYOUTS || {},
                        data?.XOPTIONS?.breakpoint || INITIAL_BREAKPOINT
                    )
                );
                setInit(true);
            } finally {
                setRefresh(false);
            }
        };
        //Если надо обновить
        if (refresh)
            if (panel)
                //Если есть для чего получать данные
                loadData();
            //Нет идентификатора запроса - нет данных
            else
                setData({
                    components: {},
                    valueProviders: {},
                    layouts: INITIAL_LAYOUTS,
                    breakpoint: INITIAL_BREAKPOINT
                });
    }, [refresh, panel, executeStored]);

    //При изменении входных свойств - поднимаем флаг обновления
    useEffect(() => {
        setInit(false);
        setRefresh(true);
    }, [panel]);

    //Возвращаем интерфейс хука
    return [
        data,
        isInit,
        addComponent,
        deleteComponent,
        breakpointChange,
        layoutsChange,
        changeValueProviders,
        changeComponentSettings,
        changePanelSettings,
        savePanelDesc,
        importPanelDesc,
        exportPanelDesc
    ];
};

//Работа с соотношением размеров
const useWindowResize = () => {
    //Состояние высоты строки ResponsiveGridLayout
    const [rowHeight, setRowHeight] = useState(1);
    //При изменении размера
    useLayoutEffect(() => {
        //Расчет высоты строки
        const updateRowHeight = () => {
            //Определяем доступную область
            const availableHeight = window.innerHeight - GRID_LAYOUT_PARAMS.APP_BAR_HEIGHT - GRID_LAYOUT_PARAMS.SAFE_OFFSET;
            //Промежутки между рядами
            const totalMargins = (GRID_LAYOUT_PARAMS.MAX_ROWS - 1) * GRID_LAYOUT_PARAMS.MARGIN;
            //Рассчитываем высоту строки
            const result = (availableHeight - totalMargins) / GRID_LAYOUT_PARAMS.MAX_ROWS;
            //Устанавливаем
            setRowHeight(result);
        };
        //Запускаем при открытии панели
        updateRowHeight();
        //Устанавливаем обработчик на событие
        window.addEventListener("resize", updateRowHeight);
        //Удаляем обработчик на событие
        return () => window.removeEventListener("resize", updateRowHeight);
    }, []);

    //Возвращаем интерфейс хука
    return [rowHeight];
};

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

export { useComponentModule, usePanel, usePanelManager, usePanelDesc, useWindowResize };
