/*
 * 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 React, {JSX, useEffect, useState} from 'react';
import './DslConnections.css';
import {DslPosition, EventBus} from "../utils/EventBus";
import {CamelUi} from "../utils/CamelUi";
import {useConnectionsStore, useDesignerStore, useIntegrationStore} from "../DesignerStore";
import {shallow} from "zustand/shallow";
import {CamelDefinitionApiExt} from "karavan-core/lib/api/CamelDefinitionApiExt";
import {TopologyUtils} from "karavan-core/lib/api/TopologyUtils";
import {CamelElement} from "karavan-core/lib/model/IntegrationDefinition";
import {v4 as uuidv4} from "uuid";
import {Button, Tooltip} from "@patternfly/react-core";
import {InfrastructureAPI} from "../utils/InfrastructureAPI";
import {getIntegrations} from "../../topology/TopologyApi";
import {ComponentApi} from "karavan-core/lib/api/ComponentApi";
import {INTERNAL_COMPONENTS} from "karavan-core/lib/api/ComponentApi";

const overlapGap: number = 40;
const DIAMETER: number = 34;
const RADIUS: number = DIAMETER / 2;

export function DslConnections() {

    const [integration, files] = useIntegrationStore((s) => [s.integration, s.files], shallow)
    const [width, height, top, left] = useDesignerStore((s) =>
        [s.width, s.height, s.top, s.left], shallow)
    const [steps, addStep, deleteStep, clearSteps] =
        useConnectionsStore((s) => [s.steps, s.addStep, s.deleteStep, s.clearSteps], shallow)

    const [svgKey, setSvgKey] = useState<string>('svgKey');
    const [tons, setTons] = useState<Map<string, string[]>>(new Map<string, string[]>());

    useEffect(() => {
        const integrations = getIntegrations(files);
        setTons(prevState => {
            const data = new Map<string, string[]>();
            TopologyUtils.findTopologyRouteOutgoingNodes(integrations).forEach(t => {
                const key = (t.step as any)?.uri + ':' + (t.step as any)?.parameters?.name;
                if (data.has(key)) {
                    const list = data.get(key) || [];
                    list.push(t.routeId);
                    data.set(key, list);
                } else {
                    data.set(key, [t.routeId]);
                }
            });
            return data;
        });
        const sub1 = EventBus.onPosition()?.subscribe((evt: DslPosition) => setPosition(evt));
        return () => {
            sub1?.unsubscribe();
        };
    }, [files]);

    useEffect(() => {
        const toDelete1: string[] = Array.from(steps.keys()).filter(k => CamelDefinitionApiExt.findElementInIntegration(integration, k) === undefined);
        toDelete1.forEach(key => deleteStep(key));
        setSvgKey(uuidv4())
    }, [integration]);

    function setPosition(evt: DslPosition) {
        if (evt.command === "add") {
            addStep(evt.step.uuid, evt);
        } else if (evt.command === "delete") {
            deleteStep(evt.step.uuid);
        } else if (evt.command === "clean") {
            clearSteps();
        }
    }

    function getElementType(element: CamelElement): 'internal' | 'remote' | 'nav' | 'poll' {
        const uri = (element as any).uri;
        if (element.dslName === 'PollDefinition') {
            return 'poll';
        } else if (INTERNAL_COMPONENTS.includes((uri))) {
            return 'nav';
        } else {
            const component = ComponentApi.findByName(uri);
            return (component !== undefined && component.component.remote !== true) ? 'internal' : 'remote';
        }
    }

    function getIncomings(): [string, number, 'internal' | 'remote' | 'nav' | 'poll'][] {
        let outs: [string, number, 'internal' | 'remote' | 'nav' | 'poll'][] = Array.from(steps.values())
            .filter(pos => ["FromDefinition"].includes(pos.step.dslName))
            .filter(pos => !(pos.step.dslName === 'FromDefinition' && (pos.step as any).uri === 'kamelet:source'))
            .sort((pos1: DslPosition, pos2: DslPosition) => {
                const y1 = pos1.headerRect.y + pos1.headerRect.height / 2;
                const y2 = pos2.headerRect.y + pos2.headerRect.height / 2;
                return y1 > y2 ? 1 : -1
            })
            .map(pos => [pos.step.uuid, pos.headerRect.y, getElementType(pos.step)]);
        while (hasOverlap(outs)) {
            outs = addGap(outs);
        }
        return outs;
    }

    function getIncoming(data: [string, number, 'internal' | 'remote' | 'nav' | 'poll']) {
        const pos = steps.get(data[0]);
        if (pos) {
            const fromX = pos.headerRect.x + pos.headerRect.width / 2 - left;
            const fromY = pos.headerRect.y + pos.headerRect.height / 2 - top;
            const r = pos.headerRect.height / 2;

            const incomingX = 20;
            const lineX1 = incomingX + r;
            const lineY1 = fromY;
            const lineX2 = fromX - r * 2 + 7;
            const lineY2 = fromY;
            const isInternal = data[2] === 'internal';
            const isNav = data[2] === 'nav';
            return (!isInternal
                    ? <g key={pos.step.uuid + "-incoming"}>
                        <circle key={pos.step.uuid + "-circle"} cx={incomingX} cy={fromY} r={r} className="circle-incoming"/>
                        <path key={pos.step.uuid + "-path"} d={`M ${lineX1},${lineY1} C ${lineX1},${lineY2} ${lineX2},${lineY1}  ${lineX2},${lineY2}`}
                              className={isNav ? 'path-incoming-nav' : 'path-incoming'}
                              markerEnd="url(#arrowheadRight)"/>
                    </g>
                    : <div key={pos.step.uuid + "-incoming"} style={{display: 'none'}}></div>
            )
        }
    }

    // function getToDirectSteps(name: string) {
    //     return Array.from(steps.values())
    //         .filter(s => s.step.dslName === 'ToDefinition')
    //         .filter(s =>  NAV_COMPONENTS.includes((s.step as any)?.uri))
    //         .filter(s =>  (s.step as any)?.parameters?.name === name)
    // }

    function getIncomingIcons(data: [string, number, 'internal' | 'remote' | 'nav' | 'poll']) {
        const pos = steps.get(data[0]);
        if (pos) {
            const step = (pos.step as any);
            const uri = step?.uri;
            const internalCall: boolean = step && uri && step?.dslName === 'FromDefinition' && INTERNAL_COMPONENTS.includes(uri);
            const name: string = internalCall ? (step?.parameters?.name) : undefined;
            const routes = internalCall ? tons.get(uri + ':' + name) || [] : [];
            const isInternal = data[2] === 'internal';
            const fromY = pos.headerRect.y + pos.headerRect.height / 2 - top;
            const r = pos.headerRect.height / 2;
            const incomingX = 20;
            const imageX = incomingX - r + 6;
            const imageY = fromY - r + 6;
            return (!isInternal
                    ? <div key={pos.step.uuid + "-icon"}
                           style={{display: "block", position: "absolute", top: imageY, left: imageX}}
                    >
                        {CamelUi.getConnectionIcon(pos.step)}
                        {routes.map((routeId, index) =>
                            <Tooltip key={`${routeId}:${index}`} content={`Go to route:${routeId}`} position={"right"}>
                                <Button style={{
                                    position: 'absolute',
                                    left: 27,
                                    top: (index * 16) + (12),
                                    whiteSpace: 'nowrap',
                                    zIndex: 300,
                                    padding: 0
                                }}
                                        variant={'link'}
                                        aria-label="Goto"
                                        onClick={_ => InfrastructureAPI.onInternalConsumerClick(undefined, undefined, routeId)}>
                                    {routeId}
                                </Button>
                            </Tooltip>
                        )}
                    </div>
                    : <div key={pos.step.uuid + "-icon"} style={{display: 'none'}}></div>
            )
        }
    }

    function hasOverlap(data: [string, number, 'internal' | 'remote' | 'nav' | 'poll'][]): boolean {
        let result = false;
        data.forEach((d, i, arr) => {
            if (i > 0 && d[1] - arr[i - 1][1] < overlapGap) result = true;
        })
        return result;
    }

    function addGap(data: [string, number, 'internal' | 'remote' | 'nav' | 'poll'][]): [string, number, 'internal' | 'remote' | 'nav' | 'poll'][] {
        const result: [string, number, 'internal' | 'remote' | 'nav' | 'poll'][] = [];
        data.forEach((d, i, arr) => {
            if (i > 0 && d[1] - arr[i - 1][1] < overlapGap) result.push([d[0], d[1] + overlapGap, d[2]])
            else result.push(d);
        })
        return result;
    }


    function getOutgoings(): [string, number, 'internal' | 'remote' | 'nav' | 'poll'][] {
        const outgoingDefinitions = TopologyUtils.getOutgoingDefinitions();
        let outs: [string, number, 'internal' | 'remote' | 'nav' | 'poll'][] = Array.from(steps.values())
            .filter(pos => outgoingDefinitions.includes(pos.step.dslName))
            .filter(pos => pos.step.dslName !== 'KameletDefinition' || (pos.step.dslName === 'KameletDefinition' && !CamelUi.isActionKamelet(pos.step)))
            .filter(pos => ['ToDefinition', 'PollDefinition'].includes(pos.step.dslName) && !CamelUi.isActionKamelet(pos.step))
            .filter(pos => !CamelUi.isKameletSink(pos.step))
            .sort((pos1: DslPosition, pos2: DslPosition) => {
                const y1 = pos1.headerRect.y + pos1.headerRect.height / 2;
                const y2 = pos2.headerRect.y + pos2.headerRect.height / 2;
                return y1 > y2 ? 1 : -1
            })
            .map(pos => [pos.step.uuid, pos.headerRect.y - top, getElementType(pos.step)]);
        while (hasOverlap(outs)) {
            outs = addGap(outs);
        }
        return outs;
    }

    function getOutgoing(data: [string, number, 'internal' | 'remote' | 'nav' | 'poll']) {
        const pos = steps.get(data[0]);
        const isInternal = data[2] === 'internal';
        const isNav = data[2] === 'nav';
        const isPoll = data[2] === 'poll';
        if (pos) {
            const fromX = pos.headerRect.x + pos.headerRect.width / 2 - left;
            const fromY = pos.headerRect.y + pos.headerRect.height / 2 - top;
            const r = pos.headerRect.height / 2;

            const outgoingX = width - 20;
            const outgoingY = data[1] + RADIUS;

            const lineX1 = fromX + r;
            const lineY1 = fromY;
            const lineX2 = outgoingX - r * 2 + (isPoll ? 14 : 6);
            const lineY2 = outgoingY;

            const lineXi = lineX1 + 40;
            const lineYi = lineY2;

            const className = isNav ? 'path-incoming-nav' : (isPoll ? 'path-poll' : 'path-incoming')

            return (!isInternal
                    ? <g key={pos.step.uuid + "-outgoing"}>
                        <circle cx={outgoingX} cy={outgoingY} r={r} className="circle-outgoing"/>
                        <path
                            d={`M ${lineX1},${lineY1} C ${lineXi - 20}, ${lineY1} ${lineX1 - RADIUS},${lineYi} ${lineXi},${lineYi} L ${lineX2},${lineY2}`}
                            className={className} markerStart={isPoll ? "url(#arrowheadLeft)" : "none"} markerEnd={isPoll ? "none" : "url(#arrowheadRight)"}/>
                    </g>
                    : <div key={pos.step.uuid + "-outgoing"} style={{display: 'none'}}></div>
            )
        }
    }

    function getOutgoingIcons(data: [string, number, 'internal' | 'remote' | 'nav' | 'poll']) {
        const pos = steps.get(data[0]);
        if (pos) {
            const step = (pos.step as any);
            const uri = step?.uri;
            const isInternal = data[2] === 'internal';
            const isNav = data[2] === 'nav';
            const internalCall = step && uri && step?.dslName === 'ToDefinition' && isNav;
            const name = internalCall ? (step?.parameters?.name ? step?.parameters.name : step?.parameters?.address) : '';
            const r = pos.headerRect.height / 2;
            const outgoingX = width - 20;
            const outgoingY = data[1] + RADIUS;
            const imageX = outgoingX - r + 6;
            const imageY = outgoingY - r + 6;
            return (!isInternal
                    ? <div key={pos.step.uuid + "-icon"}
                           style={{display: "block", position: "absolute", top: imageY, left: imageX}}>
                        {CamelUi.getConnectionIcon(pos.step)}
                        {name !== undefined &&
                            <Tooltip content={`Go to ${uri}:${name}`} position={"left"}>
                                <Button style={{
                                    position: 'absolute',
                                    right: 27,
                                    top: -12,
                                    whiteSpace: 'nowrap',
                                    zIndex: 300,
                                    padding: 0
                                }}
                                        variant={'link'}
                                        aria-label="Goto"
                                        onClick={_ => InfrastructureAPI.onInternalConsumerClick(uri, name, undefined)}>
                                    {name}
                                </Button>
                            </Tooltip>
                        }
                    </div>
                    : <div key={pos.step.uuid + "-icon"} style={{display: 'none'}}></div>
            )
        }
    }

    function getCircle(pos: DslPosition) {
        const cx = pos.headerRect.x + pos.headerRect.width / 2 - left;
        const cy = pos.headerRect.y + pos.headerRect.height / 2 - top;
        const r = pos.headerRect.height / 2;
        return (
            <circle cx={cx} cy={cy} r={r} stroke="transparent" strokeWidth="3" fill="transparent"
                    key={pos.step.uuid + "-circle"}/>
        )
    }

    function getNext(pos: DslPosition): CamelElement | undefined {
        if (pos.nextstep) {
            return pos.nextstep;
        } else if (pos.parent) {
            const parent = steps.get(pos.parent.uuid);
            if (parent) return getNext(parent);
        }
    }

    function isSpecial(pos: DslPosition): boolean {
        return ['ChoiceDefinition', 'MulticastDefinition', 'LoadBalanceDefinition', 'TryDefinition', 'RouteConfigurationDefinition'].includes(pos.step.dslName);
    }

    function addArrowToList(list: JSX.Element[], from?: DslPosition, to?: DslPosition, fromHeader?: boolean, toHeader?: boolean): JSX.Element[] {
        const result: JSX.Element[] = [...list];
        if (from && to) {
            const rect1 = fromHeader === true ? from.headerRect : from.rect;
            const rect2 = toHeader === true ? to.headerRect : to.rect;
            const key = from.step.uuid + "->" + to.step.uuid;
            result.push(getComplexArrow(key, rect1, rect2, toHeader === true));
        }
        return result;
    }

    function getParentDsl(uuid?: string): string | undefined {
        return uuid ? steps.get(uuid)?.parent?.dslName : undefined;
    }

    function getArrow(pos: DslPosition): JSX.Element[] {
        const list: JSX.Element[] = [];

        if (pos.parent && pos.parent.dslName === 'TryDefinition' && pos.position === 0) {
            const parent = steps.get(pos.parent.uuid);
            list.push(...addArrowToList(list, parent, pos, true, false))
        } else if (pos.parent && ['RouteConfigurationDefinition', 'MulticastDefinition', 'LoadBalanceDefinition'].includes(pos.parent.dslName)) {
            const parent = steps.get(pos.parent.uuid);
            list.push(...addArrowToList(list, parent, pos, true, false))
            if (parent?.nextstep) {
                const to = steps.get(parent.nextstep.uuid);
                list.push(...addArrowToList(list, pos, to, true, true))
            }
        } else if (pos.parent && pos.parent.dslName === 'ChoiceDefinition') {
            const parent = steps.get(pos.parent.uuid);
            list.push(...addArrowToList(list, parent, pos, true, false))
        } else if (pos.parent && ['WhenDefinition', 'OtherwiseDefinition', 'CatchDefinition', 'FinallyDefinition', 'TryDefinition'].includes(pos.parent.dslName)) {
            if (pos.position === 0) {
                const parent = steps.get(pos.parent.uuid);
                list.push(...addArrowToList(list, parent, pos, true, false))
            }
            if (pos.position === (pos.inStepsLength - 1) && !isSpecial(pos)) {
                const nextElement = getNext(pos);
                const parentDsl1 = getParentDsl(nextElement?.uuid);
                if (parentDsl1 && ['RouteConfigurationDefinition', 'MulticastDefinition', 'LoadBalanceDefinition'].includes(parentDsl1)) {
                    // do nothing
                } else if (nextElement) {
                    const next = steps.get(nextElement.uuid);
                    list.push(...addArrowToList(list, pos, next, true, true))
                }
            }
        } else if (pos.step && !isSpecial(pos)) {
            if (pos.nextstep) {
                const next = steps.get(pos.nextstep.uuid);
                const fromHeader = !pos.step.hasSteps();
                list.push(...addArrowToList(list, pos, next, fromHeader, true))
            }
            if (pos.step.hasSteps() && (pos.step as any).steps.length > 0) {
                const firstStep = (pos.step as any).steps[0];
                const next = steps.get(firstStep.uuid);
                list.push(...addArrowToList(list, pos, next, true, true))
            }
        }

        if (['WhenDefinition', 'OtherwiseDefinition'].includes(pos.step.dslName) && pos.step.hasSteps() && (pos.step as any).steps.length === 0) {
            const parentDsl = getParentDsl(pos?.nextstep?.uuid);
            if (parentDsl && ['RouteConfigurationDefinition', 'MulticastDefinition', 'LoadBalanceDefinition'].includes(parentDsl)) {
                // do nothing
            } else if (pos.nextstep) {
                const to = steps.get(pos.nextstep.uuid);
                list.push(...addArrowToList(list, pos, to, true, true))
            } else {
                const next = getNext(pos);
                const parentDsl1 = getParentDsl(next?.uuid);
                if (parentDsl1 && ['RouteConfigurationDefinition', 'MulticastDefinition', 'LoadBalanceDefinition'].includes(parentDsl1)) {
                    // do nothing
                } else if (next) {
                    const to = steps.get(next.uuid);
                    list.push(...addArrowToList(list, pos, to, true, true))
                }
            }
        }

        if (pos.parent?.dslName === 'TryDefinition' && pos.inSteps && pos.position === (pos.inStepsLength - 1)) {
            const parent = steps.get(pos.parent.uuid);
            if (parent && parent.nextstep) {
                const to = steps.get(parent.nextstep.uuid);
                list.push(...addArrowToList(list, pos, to, true, true))
            }
        }

        if (!isSpecial(pos) && pos.inSteps && pos.nextstep && (pos.parent?.dslName &&
            !['MulticastDefinition', 'LoadBalanceDefinition'].includes(pos.parent?.dslName))) {
            const next = steps.get(pos.nextstep.uuid);
            if (pos.step.hasSteps() && pos.prevStep) {
            } else {
                list.push(...addArrowToList(list, pos, next, true, true))
            }
        }

        if (!isSpecial(pos) && pos.inSteps && pos.nextstep && (pos.parent?.dslName &&
            !['MulticastDefinition', 'LoadBalanceDefinition'].includes(pos.parent?.dslName))) {
            const next = steps.get(pos.nextstep.uuid);
            if (next && !isSpecial(next) && next.inSteps) {
                // console.log(pos)
                // const to = steps.get(parent.nextstep.uuid);
                // list.push(...addArrowToList(list, pos, to, true, true))
            }
        }

        return list;
    }

    function getComplexArrow(key: string, rect1: DOMRect, rect2: DOMRect, toHeader: boolean) {
        const startX = rect1.x + rect1.width / 2 - left;
        const startY = rect1.y + rect1.height - top - 2;
        const endX = rect2.x + rect2.width / 2 - left;
        const endTempY = rect2.y - top - 9;

        const gapX = Math.abs(endX - startX);
        const gapY = Math.abs(endTempY - startY);

        const radX = gapX > DIAMETER ? 20 : gapX / 2;
        const radY = gapY > DIAMETER ? 20 : gapY / 2;
        const endY = rect2.y - top - radY - (toHeader ? 9 : 6);

        const iRadX = startX > endX ? -1 * radX : radX;
        const iRadY = startY > endY ? -1 * radY : radY;

        const LX1 = startX;
        const LY1 = endY - radY;

        const Q1_X1 = startX;
        const Q1_Y1 = LY1 + radY;
        const Q1_X2 = startX + iRadX;
        const Q1_Y2 = LY1 + radY;

        const LX2 = startX + (endX - startX) - iRadX;
        const LY2 = LY1 + radY;

        const Q2_X1 = LX2 + iRadX;
        const Q2_Y1 = endY;
        const Q2_X2 = LX2 + iRadX;
        const Q2_Y2 = endY + radY;

        const path = `M ${startX} ${startY}`
            + ` L ${LX1} ${LY1} `
            + ` Q ${Q1_X1} ${Q1_Y1} ${Q1_X2} ${Q1_Y2}`
            + ` L ${LX2} ${LY2}`
            + ` Q ${Q2_X1} ${Q2_Y1} ${Q2_X2} ${Q2_Y2}`
        return (
            <path key={uuidv4()} name={key} d={path} className="path" markerEnd="url(#arrowheadRight)"/>
        )
    }

    function getSvg() {
        const stepsArray = Array.from(steps.values());
        const arrows = stepsArray.map(pos => getArrow(pos)).flat(1);
        const uniqueArrows = [...new Map(arrows.map(item => [(item as any).key, item])).values()];

        return (
            <svg key={svgKey}
                 style={{width: width, height: height, position: "absolute", left: 0, top: 0}}
                 viewBox={"0 0 " + (width) + " " + (height)}>
                <defs key='defs'>
                    <marker key='maker1' id="arrowheadRight" markerWidth="9" markerHeight="6" refX="0" refY="3" orient="auto"
                            className="arrow">
                        <polygon points="0 0, 9 3, 0 6"/>
                    </marker>
                    <marker key='maker2' id="arrowheadLeft" markerWidth="9" markerHeight="6" refX="0" refY="3" orient="auto"
                            className="arrow">
                        <polygon points="9 0, 0 3, 9 6"/>
                    </marker>
                </defs>
                {stepsArray.map(pos => getCircle(pos))}
                {uniqueArrows}
                {getIncomings().map(p => getIncoming(p))}
                {getOutgoings().map(p => getOutgoing(p))}
            </svg>
        )
    }

    return (
        <div id="connections" className="connections" style={{width: width, height: height}}>
            {getSvg()}
            {getIncomings().map(p => getIncomingIcons(p))}
            {getOutgoings().map(p => getOutgoingIcons(p))}
        </div>
    )
}