import {DropDown, DropDownMenu, Tooltip} from 'argo-ui';
import * as classNames from 'classnames';
import * as dagre from 'dagre';
import * as React from 'react';
import Moment from 'react-moment';

import * as models from '../../../shared/models';

import {EmptyState} from '../../../shared/components';
import {AppContext, Consumer} from '../../../shared/context';
import {ApplicationURLs} from '../application-urls';
import {ResourceIcon} from '../resource-icon';
import {ResourceLabel} from '../resource-label';
import {
    BASE_COLORS,
    ComparisonStatusIcon,
    deletePodAction,
    getAppOverridesCount,
    getExternalUrls,
    HealthStatusIcon,
    isAppNode,
    isYoungerThanXMinutes,
    NodeId,
    nodeKey,
    PodHealthIcon
} from '../utils';
import {NodeUpdateAnimation} from './node-update-animation';
import {PodGroup} from '../application-pod-view/pod-view';
import {ArrowConnector} from './arrow-connector';
import './application-resource-tree.scss';

function treeNodeKey(node: NodeId & {uid?: string}) {
    return node.uid || nodeKey(node);
}

const color = require('color');

export interface ResourceTreeNode extends models.ResourceNode {
    status?: models.SyncStatusCode;
    health?: models.HealthStatus;
    hook?: boolean;
    root?: ResourceTreeNode;
    requiresPruning?: boolean;
    orphaned?: boolean;
    podGroup?: PodGroup;
    isExpanded?: boolean;
}

export interface ApplicationResourceTreeProps {
    app: models.Application;
    tree: models.ApplicationTree;
    useNetworkingHierarchy: boolean;
    nodeFilter: (node: ResourceTreeNode) => boolean;
    selectedNodeFullName?: string;
    onNodeClick?: (fullName: string) => any;
    onGroupdNodeClick?: (groupedNodeIds: string[]) => any;
    nodeMenu?: (node: models.ResourceNode) => React.ReactNode;
    onClearFilter: () => any;
    appContext?: AppContext;
    showOrphanedResources: boolean;
    showCompactNodes: boolean;
    zoom: number;
    filters?: string[];
    setTreeFilterGraph?: (filterGraph: any[]) => void;
    nameDirection: boolean;
    setNodeExpansion: (node: string, isExpanded: boolean) => any;
    getNodeExpansion: (node: string) => boolean;
}

interface Line {
    x1: number;
    y1: number;
    x2: number;
    y2: number;
}

const NODE_WIDTH = 282;
const NODE_HEIGHT = 52;
const POD_NODE_HEIGHT = 136;
const FILTERED_INDICATOR_NODE = '__filtered_indicator__';
const EXTERNAL_TRAFFIC_NODE = '__external_traffic__';
const INTERNAL_TRAFFIC_NODE = '__internal_traffic__';
const NODE_TYPES = {
    filteredIndicator: 'filtered_indicator',
    externalTraffic: 'external_traffic',
    externalLoadBalancer: 'external_load_balancer',
    internalTraffic: 'internal_traffic',
    groupedNodes: 'grouped_nodes',
    podGroup: 'pod_group'
};

// generate lots of colors with different darkness
const TRAFFIC_COLORS = [0, 0.25, 0.4, 0.6]
    .map(darken =>
        BASE_COLORS.map(item =>
            color(item)
                .darken(darken)
                .hex()
        )
    )
    .reduce((first, second) => first.concat(second), []);

function getGraphSize(nodes: dagre.Node[]): {width: number; height: number} {
    let width = 0;
    let height = 0;
    nodes.forEach(node => {
        width = Math.max(node.x + node.width, width);
        height = Math.max(node.y + node.height, height);
    });
    return {width, height};
}

function groupNodes(nodes: any[], graph: dagre.graphlib.Graph) {
    function getNodeGroupingInfo(nodeId: string) {
        const node = graph.node(nodeId);
        return {
            nodeId,
            kind: node.kind,
            parentIds: graph.predecessors(nodeId),
            childIds: graph.successors(nodeId)
        };
    }

    function filterNoChildNode(nodeInfo: {childIds: dagre.Node[]}) {
        return nodeInfo.childIds.length === 0;
    }

    // create nodes array with parent/child nodeId
    const nodesInfoArr = graph.nodes().map(getNodeGroupingInfo);

    // group sibling nodes into a 2d array
    const siblingNodesArr = nodesInfoArr
        .reduce((acc, curr) => {
            if (curr.childIds.length > 1) {
                acc.push(curr.childIds.map(nodeId => getNodeGroupingInfo(nodeId.toString())));
            }
            return acc;
        }, [])
        .map(nodeArr => nodeArr.filter(filterNoChildNode));

    // group sibling nodes with same kind
    const groupedNodesArr = siblingNodesArr
        .map(eachLevel => {
            return eachLevel.reduce(
                (groupedNodesInfo: {kind: string; nodeIds?: string[]; parentIds?: dagre.Node[]}[], currentNodeInfo: {kind: string; nodeId: string; parentIds: dagre.Node[]}) => {
                    const index = groupedNodesInfo.findIndex((nodeInfo: {kind: string}) => currentNodeInfo.kind === nodeInfo.kind);
                    if (index > -1) {
                        groupedNodesInfo[index].nodeIds.push(currentNodeInfo.nodeId);
                    }

                    if (groupedNodesInfo.length === 0 || index < 0) {
                        const nodeIdArr = [];
                        nodeIdArr.push(currentNodeInfo.nodeId);
                        const groupedNodesInfoObj = {
                            kind: currentNodeInfo.kind,
                            nodeIds: nodeIdArr,
                            parentIds: currentNodeInfo.parentIds
                        };
                        groupedNodesInfo.push(groupedNodesInfoObj);
                    }

                    return groupedNodesInfo;
                },
                []
            );
        })
        .reduce((flattedNodesGroup, groupedNodes) => {
            return flattedNodesGroup.concat(groupedNodes);
        }, [])
        .filter((eachArr: {nodeIds: string[]}) => eachArr.nodeIds.length > 1);

    // update graph
    if (groupedNodesArr.length > 0) {
        groupedNodesArr.forEach((obj: {kind: string; nodeIds: string[]; parentIds: dagre.Node[]}) => {
            const {nodeIds, kind, parentIds} = obj;
            const groupedNodeIds: string[] = [];
            nodeIds.forEach((nodeId: string) => {
                const index = nodes.findIndex(node => nodeId === node.uid || nodeId === nodeKey(node));
                if (index > -1) {
                    groupedNodeIds.push(nodeId);
                }
                graph.removeNode(nodeId);
            });
            graph.setNode(`${parentIds[0].toString()}/child/${kind}`, {
                kind,
                groupedNodeIds,
                height: NODE_HEIGHT,
                width: NODE_WIDTH,
                count: nodeIds.length,
                type: NODE_TYPES.groupedNodes
            });
            graph.setEdge(parentIds[0].toString(), `${parentIds[0].toString()}/child/${kind}`);
        });
    }
}

export function compareNodes(first: ResourceTreeNode, second: ResourceTreeNode) {
    function orphanedToInt(orphaned?: boolean) {
        return (orphaned && 1) || 0;
    }
    function compareRevision(a: string, b: string) {
        const numberA = Number(a);
        const numberB = Number(b);
        if (isNaN(numberA) || isNaN(numberB)) {
            return a.localeCompare(b);
        }
        return Math.sign(numberA - numberB);
    }
    function getRevision(a: ResourceTreeNode) {
        const filtered = (a.info || []).filter(b => b.name === 'Revision' && b)[0];
        if (filtered == null) {
            return '';
        }
        const value = filtered.value;
        if (value == null) {
            return '';
        }
        return value.replace(/^Rev:/, '');
    }
    return (
        orphanedToInt(first.orphaned) - orphanedToInt(second.orphaned) ||
        nodeKey(first).localeCompare(nodeKey(second)) ||
        compareRevision(getRevision(first), getRevision(second)) ||
        0
    );
}

function appNodeKey(app: models.Application) {
    return nodeKey({group: 'argoproj.io', kind: app.kind, name: app.metadata.name, namespace: app.metadata.namespace});
}

function renderFilteredNode(node: {count: number} & dagre.Node, onClearFilter: () => any) {
    const indicators = new Array<number>();
    let count = Math.min(node.count - 1, 3);
    while (count > 0) {
        indicators.push(count--);
    }
    return (
        <React.Fragment>
            <div className='application-resource-tree__node' style={{left: node.x, top: node.y, width: node.width, height: node.height}}>
                <div className='application-resource-tree__node-kind-icon '>
                    <i className='icon fa fa-filter' />
                </div>
                <div className='application-resource-tree__node-content-wrap-overflow'>
                    <a className='application-resource-tree__node-title' onClick={onClearFilter}>
                        clear filters to show {node.count} additional resource{node.count > 1 && 's'}
                    </a>
                </div>
            </div>
            {indicators.map(i => (
                <div
                    key={i}
                    className='application-resource-tree__node application-resource-tree__filtered-indicator'
                    style={{left: node.x + i * 2, top: node.y + i * 2, width: node.width, height: node.height}}
                />
            ))}
        </React.Fragment>
    );
}

function renderGroupedNodes(props: ApplicationResourceTreeProps, node: {count: number} & dagre.Node & ResourceTreeNode) {
    const indicators = new Array<number>();
    let count = Math.min(node.count - 1, 3);
    while (count > 0) {
        indicators.push(count--);
    }
    return (
        <React.Fragment>
            <div className='application-resource-tree__node' style={{left: node.x, top: node.y, width: node.width, height: node.height}}>
                <div className='application-resource-tree__node-kind-icon'>
                    <ResourceIcon kind={node.kind} />
                    <br />
                    <div className='application-resource-tree__node-kind'>{ResourceLabel({kind: node.kind})}</div>
                </div>
                <div className='application-resource-tree__node-content-wrap-overflow'>
                    <a className='application-resource-tree__node-title' onClick={() => props.onGroupdNodeClick && props.onGroupdNodeClick(node.groupedNodeIds)}>
                        click to show details of {node.count} collapsed {node.kind}
                    </a>
                </div>
            </div>
            {indicators.map(i => (
                <div
                    key={i}
                    className='application-resource-tree__node application-resource-tree__filtered-indicator'
                    style={{left: node.x + i * 2, top: node.y + i * 2, width: node.width, height: node.height}}
                />
            ))}
        </React.Fragment>
    );
}

function renderTrafficNode(node: dagre.Node) {
    return (
        <div style={{position: 'absolute', left: 0, top: node.y, width: node.width, height: node.height}}>
            <div className='application-resource-tree__node-kind-icon' style={{fontSize: '2em'}}>
                <i className='icon fa fa-cloud' />
            </div>
        </div>
    );
}

function renderLoadBalancerNode(node: dagre.Node & {label: string; color: string}) {
    return (
        <div
            className='application-resource-tree__node application-resource-tree__node--load-balancer'
            style={{
                left: node.x,
                top: node.y,
                width: node.width,
                height: node.height
            }}>
            <div className='application-resource-tree__node-kind-icon'>
                <i title={node.kind} className={`icon fa fa-network-wired`} style={{color: node.color}} />
            </div>
            <div className='application-resource-tree__node-content'>
                <span className='application-resource-tree__node-title'>{node.label}</span>
            </div>
        </div>
    );
}

export const describeNode = (node: ResourceTreeNode) => {
    const lines = [`Kind: ${node.kind}`, `Namespace: ${node.namespace || '(global)'}`, `Name: ${node.name}`];
    if (node.images) {
        lines.push('Images:');
        node.images.forEach(i => lines.push(`- ${i}`));
    }
    return lines.join('\n');
};

function processPodGroup(targetPodGroup: ResourceTreeNode, child: ResourceTreeNode, props: ApplicationResourceTreeProps) {
    if (!targetPodGroup.podGroup) {
        const fullName = nodeKey(targetPodGroup);
        if ((targetPodGroup.parentRefs || []).length === 0) {
            targetPodGroup.root = targetPodGroup;
        }
        targetPodGroup.podGroup = {
            pods: [] as models.Pod[],
            fullName,
            ...targetPodGroup.podGroup,
            ...targetPodGroup,
            info: (targetPodGroup.info || []).filter(i => !i.name.includes('Resource.')),
            createdAt: targetPodGroup.createdAt,
            renderMenu: () => props.nodeMenu(targetPodGroup),
            kind: targetPodGroup.kind,
            type: 'parentResource',
            name: targetPodGroup.name
        };
    }
    if (child.kind === 'Pod') {
        const p: models.Pod = {
            ...child,
            fullName: nodeKey(child),
            metadata: {name: child.name},
            spec: {nodeName: 'Unknown'},
            health: child.health ? child.health.status : 'Unknown'
        } as models.Pod;

        // Get node name for Pod
        child.info?.forEach(i => {
            if (i.name === 'Node') {
                p.spec.nodeName = i.value;
            }
        });
        targetPodGroup.podGroup.pods.push(p);
    }
}

function renderPodGroup(props: ApplicationResourceTreeProps, id: string, node: ResourceTreeNode & dagre.Node, childMap: Map<string, ResourceTreeNode[]>) {
    const fullName = nodeKey(node);
    let comparisonStatus: models.SyncStatusCode = null;
    let healthState: models.HealthStatus = null;
    if (node.status || node.health) {
        comparisonStatus = node.status;
        healthState = node.health;
    }
    const appNode = isAppNode(node);
    const rootNode = !node.root;
    let extLinks: string[] = props.app.status.summary.externalURLs;
    if (rootNode) {
        extLinks = getExternalUrls(props.app.metadata.annotations, props.app.status.summary.externalURLs);
    }
    const podGroupChildren = childMap.get(treeNodeKey(node));
    const nonPodChildren = podGroupChildren?.reduce((acc, child) => {
        if (child.kind !== 'Pod') {
            acc.push(child);
        }
        return acc;
    }, []);
    const childCount = nonPodChildren?.length;
    const margin = 8;
    let topExtra = 0;
    const podGroup = node.podGroup;
    if (podGroup) {
        const numberOfRows = Math.ceil(podGroup.pods.length / 8);
        topExtra = margin + (POD_NODE_HEIGHT / 2 + 30 * numberOfRows) / 2;
    }
    return (
        <div
            onClick={() => props.onNodeClick && props.onNodeClick(fullName)}
            className={classNames('application-resource-tree__node', {
                'active': fullName === props.selectedNodeFullName,
                'application-resource-tree__node--orphaned': node.orphaned
            })}
            title={describeNode(node)}
            style={{left: node.x, top: node.y - topExtra, width: node.width, height: node.height}}>
            <NodeUpdateAnimation resourceVersion={node.resourceVersion} />
            <div className={`application-resource-tree__node__top-part`}>
                <div
                    className={classNames('application-resource-tree__node-kind-icon', {
                        'application-resource-tree__node-kind-icon--big': rootNode
                    })}>
                    <ResourceIcon kind={node.kind || 'Unknown'} />
                    <br />
                    {!rootNode && <div className='application-resource-tree__node-kind'>{ResourceLabel({kind: node.kind})}</div>}
                </div>
                <div className='application-resource-tree__node-content'>
                    <span
                        className={classNames('application-resource-tree__node-title', {
                            'application-resource-tree__direction-right': props.nameDirection,
                            'application-resource-tree__direction-left': !props.nameDirection
                        })}>
                        {node.name}
                    </span>
                    <span
                        className={classNames('application-resource-tree__node-status-icon', {
                            'application-resource-tree__node-status-icon--offset': rootNode
                        })}>
                        {node.hook && <i title='Resource lifecycle hook' className='fa fa-anchor' />}
                        {healthState != null && <HealthStatusIcon state={healthState} />}
                        {comparisonStatus != null && <ComparisonStatusIcon status={comparisonStatus} resource={!rootNode && node} />}
                        {appNode && !rootNode && (
                            <Consumer>
                                {ctx => (
                                    <a href={ctx.baseHref + 'applications/' + node.namespace + '/' + node.name} title='Open application'>
                                        <i className='fa fa-external-link-alt' />
                                    </a>
                                )}
                            </Consumer>
                        )}
                        <ApplicationURLs urls={rootNode ? extLinks : node.networkingInfo && node.networkingInfo.externalURLs} />
                    </span>
                    {childCount > 0 && (
                        <>
                            <br />
                            <div
                                style={{top: node.height / 2 - 6}}
                                className='application-resource-tree__node--podgroup--expansion'
                                onClick={event => {
                                    expandCollapse(node, props);
                                    event.stopPropagation();
                                }}>
                                {props.getNodeExpansion(node.uid) ? <div className='fa fa-minus' /> : <div className='fa fa-plus' />}
                            </div>
                        </>
                    )}
                </div>
                <div className='application-resource-tree__node-labels'>
                    {node.createdAt || rootNode ? (
                        <Moment className='application-resource-tree__node-label' fromNow={true} ago={true}>
                            {node.createdAt || props.app.metadata.creationTimestamp}
                        </Moment>
                    ) : null}
                    {(node.info || [])
                        .filter(tag => !tag.name.includes('Node'))
                        .slice(0, 4)
                        .map((tag, i) => (
                            <span className='application-resource-tree__node-label' title={`${tag.name}:${tag.value}`} key={i}>
                                {tag.value}
                            </span>
                        ))}
                    {(node.info || []).length > 4 && (
                        <Tooltip
                            content={(node.info || []).map(i => (
                                <div key={i.name}>
                                    {i.name}: {i.value}
                                </div>
                            ))}
                            key={node.uid}>
                            <span className='application-resource-tree__node-label' title='More'>
                                More
                            </span>
                        </Tooltip>
                    )}
                </div>
                {props.nodeMenu && (
                    <div className='application-resource-tree__node-menu'>
                        <DropDown
                            isMenu={true}
                            anchor={() => (
                                <button className='argo-button argo-button--light argo-button--lg argo-button--short'>
                                    <i className='fa fa-ellipsis-v' />
                                </button>
                            )}>
                            {() => props.nodeMenu(node)}
                        </DropDown>
                    </div>
                )}
            </div>
            <div className='application-resource-tree__node--lower-section'>
                {podGroup && (
                    <div className={`application-resource-tree__node--lower-section__pod-group`} key={podGroup.fullName || podGroup.name}>
                        <div className='application-resource-tree__node--lower-section__pod-group__pod-container'>
                            <div className='application-resource-tree__node--lower-section__pod-group__pod-container__pods'>
                                {podGroup.pods.map(pod => (
                                    <DropDownMenu
                                        key={pod.uid}
                                        anchor={() => (
                                            <Tooltip
                                                content={
                                                    <div>
                                                        {pod.metadata.name}
                                                        <div>Health: {pod.health}</div>
                                                        {pod.createdAt && (
                                                            <span>
                                                                <span>Created: </span>
                                                                <Moment fromNow={true} ago={true}>
                                                                    {pod.createdAt}
                                                                </Moment>
                                                                <span> ago ({<Moment local={true}>{pod.createdAt}</Moment>})</span>
                                                            </span>
                                                        )}
                                                    </div>
                                                }
                                                popperOptions={{
                                                    modifiers: {
                                                        preventOverflow: {
                                                            enabled: true
                                                        },
                                                        hide: {
                                                            enabled: false
                                                        },
                                                        flip: {
                                                            enabled: false
                                                        }
                                                    }
                                                }}
                                                key={pod.metadata.name}>
                                                <div style={{position: 'relative'}}>
                                                    {isYoungerThanXMinutes(pod, 30) && (
                                                        <i className='fas fa-star application-resource-tree__node--lower-section__pod-group__pod application-resource-tree__node--lower-section__pod-group__pod__star-icon' />
                                                    )}
                                                    <div
                                                        className={`application-resource-tree__node--lower-section__pod-group__pod application-resource-tree__node--lower-section__pod-group__pod--${pod.health.toLowerCase()}`}>
                                                        <PodHealthIcon state={{status: pod.health, message: ''}} />
                                                    </div>
                                                </div>
                                            </Tooltip>
                                        )}
                                        items={[
                                            {
                                                title: (
                                                    <React.Fragment>
                                                        <i className='fa fa-info-circle' /> Info
                                                    </React.Fragment>
                                                ),
                                                action: () => props.onNodeClick(pod.fullName)
                                            },
                                            {
                                                title: (
                                                    <React.Fragment>
                                                        <i className='fa fa-align-left' /> Logs
                                                    </React.Fragment>
                                                ),
                                                action: () => {
                                                    props.appContext.apis.navigation.goto('.', {node: pod.fullName, tab: 'logs'}, {replace: true});
                                                }
                                            },
                                            {
                                                title: (
                                                    <React.Fragment>
                                                        <i className='fa fa-times-circle' /> Delete
                                                    </React.Fragment>
                                                ),
                                                action: () => {
                                                    deletePodAction(pod, props.appContext, props.app.metadata.name, props.app.metadata.namespace);
                                                }
                                            }
                                        ]}
                                    />
                                ))}
                            </div>
                            <div className='application-resource-tree__node--lower-section__pod-group__label'>PODS</div>
                        </div>
                    </div>
                )}
            </div>
        </div>
    );
}

function expandCollapse(node: ResourceTreeNode, props: ApplicationResourceTreeProps) {
    const isExpanded = !props.getNodeExpansion(node.uid);
    node.isExpanded = isExpanded;
    props.setNodeExpansion(node.uid, isExpanded);
}

function renderResourceNode(props: ApplicationResourceTreeProps, id: string, node: ResourceTreeNode & dagre.Node, nodesHavingChildren: Map<string, number>) {
    const fullName = nodeKey(node);
    let comparisonStatus: models.SyncStatusCode = null;
    let healthState: models.HealthStatus = null;
    if (node.status || node.health) {
        comparisonStatus = node.status;
        healthState = node.health;
    }
    const appNode = isAppNode(node);
    const rootNode = !node.root;
    let extLinks: string[] = props.app.status.summary.externalURLs;
    const childCount = nodesHavingChildren.get(node.uid);
    if (rootNode) {
        extLinks = getExternalUrls(props.app.metadata.annotations, props.app.status.summary.externalURLs);
    }
    return (
        <div
            onClick={() => props.onNodeClick && props.onNodeClick(fullName)}
            className={classNames('application-resource-tree__node', 'application-resource-tree__node--' + node.kind.toLowerCase(), {
                'active': fullName === props.selectedNodeFullName,
                'application-resource-tree__node--orphaned': node.orphaned
            })}
            title={describeNode(node)}
            style={{
                left: node.x,
                top: node.y,
                width: node.width,
                height: node.height
            }}>
            {!appNode && <NodeUpdateAnimation resourceVersion={node.resourceVersion} />}
            <div
                className={classNames('application-resource-tree__node-kind-icon', {
                    'application-resource-tree__node-kind-icon--big': rootNode
                })}>
                <ResourceIcon kind={node.kind} />
                <br />
                {!rootNode && <div className='application-resource-tree__node-kind'>{ResourceLabel({kind: node.kind})}</div>}
            </div>
            <div className='application-resource-tree__node-content'>
                <div
                    className={classNames('application-resource-tree__node-title', {
                        'application-resource-tree__direction-right': props.nameDirection,
                        'application-resource-tree__direction-left': !props.nameDirection
                    })}>
                    {node.name}
                </div>
                <div
                    className={classNames('application-resource-tree__node-status-icon', {
                        'application-resource-tree__node-status-icon--offset': rootNode
                    })}>
                    {node.hook && <i title='Resource lifecycle hook' className='fa fa-anchor' />}
                    {healthState != null && <HealthStatusIcon state={healthState} />}
                    {comparisonStatus != null && <ComparisonStatusIcon status={comparisonStatus} resource={!rootNode && node} />}
                    {appNode && !rootNode && (
                        <Consumer>
                            {ctx => (
                                <a href={ctx.baseHref + 'applications/' + node.namespace + '/' + node.name} title='Open application'>
                                    <i className='fa fa-external-link-alt' />
                                </a>
                            )}
                        </Consumer>
                    )}
                    <ApplicationURLs urls={rootNode ? extLinks : node.networkingInfo && node.networkingInfo.externalURLs} />
                </div>
                {childCount > 0 && (
                    <div
                        className='application-resource-tree__node--expansion'
                        onClick={event => {
                            expandCollapse(node, props);
                            event.stopPropagation();
                        }}>
                        {props.getNodeExpansion(node.uid) ? <div className='fa fa-minus' /> : <div className='fa fa-plus' />}
                    </div>
                )}
            </div>
            <div className='application-resource-tree__node-labels'>
                {node.createdAt || rootNode ? (
                    <Moment className='application-resource-tree__node-label' fromNow={true} ago={true}>
                        {node.createdAt || props.app.metadata.creationTimestamp}
                    </Moment>
                ) : null}
                {(node.info || [])
                    .filter(tag => !tag.name.includes('Node'))
                    .slice(0, 4)
                    .map((tag, i) => (
                        <span className='application-resource-tree__node-label' title={`${tag.name}:${tag.value}`} key={i}>
                            {tag.value}
                        </span>
                    ))}
                {(node.info || []).length > 4 && (
                    <Tooltip
                        content={(node.info || []).map(i => (
                            <div key={i.name}>
                                {i.name}: {i.value}
                            </div>
                        ))}
                        key={node.uid}>
                        <span className='application-resource-tree__node-label' title='More'>
                            More
                        </span>
                    </Tooltip>
                )}
            </div>
            {props.nodeMenu && (
                <div className='application-resource-tree__node-menu'>
                    <DropDown
                        isMenu={true}
                        anchor={() => (
                            <button className='argo-button argo-button--light argo-button--lg argo-button--short'>
                                <i className='fa fa-ellipsis-v' />
                            </button>
                        )}>
                        {() => props.nodeMenu(node)}
                    </DropDown>
                </div>
            )}
        </div>
    );
}

function findNetworkTargets(nodes: ResourceTreeNode[], networkingInfo: models.ResourceNetworkingInfo): ResourceTreeNode[] {
    let result = new Array<ResourceTreeNode>();
    const refs = new Set((networkingInfo.targetRefs || []).map(nodeKey));
    result = result.concat(nodes.filter(target => refs.has(nodeKey(target))));
    if (networkingInfo.targetLabels) {
        result = result.concat(
            nodes.filter(target => {
                if (target.networkingInfo && target.networkingInfo.labels) {
                    return Object.keys(networkingInfo.targetLabels).every(key => networkingInfo.targetLabels[key] === target.networkingInfo.labels[key]);
                }
                return false;
            })
        );
    }
    return result;
}
export const ApplicationResourceTree = (props: ApplicationResourceTreeProps) => {
    const graph = new dagre.graphlib.Graph();
    graph.setGraph({nodesep: 25, rankdir: 'LR', marginy: 45, marginx: -100, ranksep: 80});
    graph.setDefaultEdgeLabel(() => ({}));
    const overridesCount = getAppOverridesCount(props.app);
    const appNode = {
        kind: props.app.kind,
        name: props.app.metadata.name,
        namespace: props.app.metadata.namespace,
        resourceVersion: props.app.metadata.resourceVersion,
        group: 'argoproj.io',
        version: '',
        children: Array(),
        status: props.app.status.sync.status,
        health: props.app.status.health,
        uid: props.app.kind + '-' + props.app.metadata.namespace + '-' + props.app.metadata.name,
        info:
            overridesCount > 0
                ? [
                      {
                          name: 'Parameter overrides',
                          value: `${overridesCount} parameter override(s)`
                      }
                  ]
                : []
    };

    const statusByKey = new Map<string, models.ResourceStatus>();
    props.app.status.resources.forEach(res => statusByKey.set(nodeKey(res), res));
    const nodeByKey = new Map<string, ResourceTreeNode>();
    props.tree.nodes
        .map(node => ({...node, orphaned: false}))
        .concat(((props.showOrphanedResources && props.tree.orphanedNodes) || []).map(node => ({...node, orphaned: true})))
        .forEach(node => {
            const status = statusByKey.get(nodeKey(node));
            const resourceNode: ResourceTreeNode = {...node};
            if (status) {
                resourceNode.health = status.health;
                resourceNode.status = status.status;
                resourceNode.hook = status.hook;
                resourceNode.requiresPruning = status.requiresPruning;
            }
            nodeByKey.set(treeNodeKey(node), resourceNode);
        });
    const nodes = Array.from(nodeByKey.values());
    let roots: ResourceTreeNode[] = [];
    const childrenByParentKey = new Map<string, ResourceTreeNode[]>();
    const nodesHavingChildren = new Map<string, number>();
    const childrenMap = new Map<string, ResourceTreeNode[]>();
    const [filters, setFilters] = React.useState(props.filters);
    const [filteredGraph, setFilteredGraph] = React.useState([]);
    const filteredNodes: any[] = [];
    React.useEffect(() => {
        if (props.filters !== filters) {
            setFilters(props.filters);
            setFilteredGraph(filteredNodes);
            props.setTreeFilterGraph(filteredGraph);
        }
    }, [props.filters]);

    function filterGraph(app: models.Application, filteredIndicatorParent: string, graphNodesFilter: dagre.graphlib.Graph, predicate: (node: ResourceTreeNode) => boolean) {
        const appKey = appNodeKey(app);
        let filtered = 0;
        graphNodesFilter.nodes().forEach(nodeId => {
            const node: ResourceTreeNode = graphNodesFilter.node(nodeId) as any;
            const parentIds = graphNodesFilter.predecessors(nodeId);
            if (node.root != null && !predicate(node) && appKey !== nodeId) {
                const childIds = graphNodesFilter.successors(nodeId);
                graphNodesFilter.removeNode(nodeId);
                filtered++;
                childIds.forEach((childId: any) => {
                    parentIds.forEach((parentId: any) => {
                        graphNodesFilter.setEdge(parentId, childId);
                    });
                });
            } else {
                if (node.root != null) filteredNodes.push(node);
            }
        });
        if (filtered) {
            graphNodesFilter.setNode(FILTERED_INDICATOR_NODE, {height: NODE_HEIGHT, width: NODE_WIDTH, count: filtered, type: NODE_TYPES.filteredIndicator});
            graphNodesFilter.setEdge(filteredIndicatorParent, FILTERED_INDICATOR_NODE);
        }
    }

    if (props.useNetworkingHierarchy) {
        // Network view
        const hasParents = new Set<string>();
        const networkNodes = nodes.filter(node => node.networkingInfo);
        const hiddenNodes: ResourceTreeNode[] = [];
        networkNodes.forEach(parent => {
            findNetworkTargets(networkNodes, parent.networkingInfo).forEach(child => {
                const children = childrenByParentKey.get(treeNodeKey(parent)) || [];
                hasParents.add(treeNodeKey(child));
                const parentId = parent.uid;
                if (nodesHavingChildren.has(parentId)) {
                    nodesHavingChildren.set(parentId, nodesHavingChildren.get(parentId) + children.length);
                } else {
                    nodesHavingChildren.set(parentId, 1);
                }
                if (child.kind !== 'Pod' || !props.showCompactNodes) {
                    if (props.getNodeExpansion(parentId)) {
                        hasParents.add(treeNodeKey(child));
                        children.push(child);
                        childrenByParentKey.set(treeNodeKey(parent), children);
                    } else {
                        hiddenNodes.push(child);
                    }
                } else {
                    processPodGroup(parent, child, props);
                }
            });
        });
        roots = networkNodes.filter(node => !hasParents.has(treeNodeKey(node)));
        roots = roots.reduce((acc, curr) => {
            if (hiddenNodes.indexOf(curr) < 0) {
                acc.push(curr);
            }
            return acc;
        }, []);
        const externalRoots = roots.filter(root => (root.networkingInfo.ingress || []).length > 0).sort(compareNodes);
        const internalRoots = roots.filter(root => (root.networkingInfo.ingress || []).length === 0).sort(compareNodes);
        const colorsBySource = new Map<string, string>();
        // sources are root internal services and external ingress/service IPs
        const sources = Array.from(
            new Set(
                internalRoots
                    .map(root => treeNodeKey(root))
                    .concat(
                        externalRoots.map(root => root.networkingInfo.ingress.map(ingress => ingress.hostname || ingress.ip)).reduce((first, second) => first.concat(second), [])
                    )
            )
        );
        // assign unique color to each traffic source
        sources.forEach((key, i) => colorsBySource.set(key, TRAFFIC_COLORS[i % TRAFFIC_COLORS.length]));

        if (externalRoots.length > 0) {
            graph.setNode(EXTERNAL_TRAFFIC_NODE, {height: NODE_HEIGHT, width: 30, type: NODE_TYPES.externalTraffic});
            externalRoots.sort(compareNodes).forEach(root => {
                const loadBalancers = root.networkingInfo.ingress.map(ingress => ingress.hostname || ingress.ip);
                const colorByService = new Map<string, string>();
                (childrenByParentKey.get(treeNodeKey(root)) || []).forEach((child, i) => colorByService.set(treeNodeKey(child), TRAFFIC_COLORS[i % TRAFFIC_COLORS.length]));
                (childrenByParentKey.get(treeNodeKey(root)) || []).sort(compareNodes).forEach((child, i) => {
                    processNode(child, root, [colorByService.get(treeNodeKey(child))]);
                });
                if (root.podGroup && props.showCompactNodes) {
                    setPodGroupNode(root, root);
                } else {
                    graph.setNode(treeNodeKey(root), {...root, width: NODE_WIDTH, height: NODE_HEIGHT, root});
                }
                (childrenByParentKey.get(treeNodeKey(root)) || []).forEach(child => {
                    if (root.namespace === child.namespace) {
                        graph.setEdge(treeNodeKey(root), treeNodeKey(child), {colors: [colorByService.get(treeNodeKey(child))]});
                    }
                });
                loadBalancers.forEach(key => {
                    const loadBalancerNodeKey = `${EXTERNAL_TRAFFIC_NODE}:${key}`;
                    graph.setNode(loadBalancerNodeKey, {
                        height: NODE_HEIGHT,
                        width: NODE_WIDTH,
                        type: NODE_TYPES.externalLoadBalancer,
                        label: key,
                        color: colorsBySource.get(key)
                    });
                    graph.setEdge(loadBalancerNodeKey, treeNodeKey(root), {colors: [colorsBySource.get(key)]});
                    graph.setEdge(EXTERNAL_TRAFFIC_NODE, loadBalancerNodeKey, {colors: [colorsBySource.get(key)]});
                });
            });
        }

        if (internalRoots.length > 0) {
            graph.setNode(INTERNAL_TRAFFIC_NODE, {height: NODE_HEIGHT, width: 30, type: NODE_TYPES.internalTraffic});
            internalRoots.forEach(root => {
                processNode(root, root, [colorsBySource.get(treeNodeKey(root))]);
                graph.setEdge(INTERNAL_TRAFFIC_NODE, treeNodeKey(root));
            });
        }
        if (props.nodeFilter) {
            // show filtered indicator next to external traffic node is app has it otherwise next to internal traffic node
            filterGraph(props.app, externalRoots.length > 0 ? EXTERNAL_TRAFFIC_NODE : INTERNAL_TRAFFIC_NODE, graph, props.nodeFilter);
        }
    } else {
        // Tree view
        const managedKeys = new Set(props.app.status.resources.map(nodeKey));
        const orphanedKeys = new Set(props.tree.orphanedNodes?.map(nodeKey));
        const orphans: ResourceTreeNode[] = [];
        let allChildNodes: ResourceTreeNode[] = [];
        nodesHavingChildren.set(appNode.uid, 1);
        if (props.getNodeExpansion(appNode.uid)) {
            nodes.forEach(node => {
                allChildNodes = [];
                if ((node.parentRefs || []).length === 0 || managedKeys.has(nodeKey(node))) {
                    roots.push(node);
                } else {
                    if (orphanedKeys.has(nodeKey(node))) {
                        orphans.push(node);
                    }
                    node.parentRefs.forEach(parent => {
                        const parentId = treeNodeKey(parent);
                        const children = childrenByParentKey.get(parentId) || [];
                        if (nodesHavingChildren.has(parentId)) {
                            nodesHavingChildren.set(parentId, nodesHavingChildren.get(parentId) + children.length);
                        } else {
                            nodesHavingChildren.set(parentId, 1);
                        }
                        allChildNodes.push(node);
                        if (node.kind !== 'Pod' || !props.showCompactNodes) {
                            if (props.getNodeExpansion(parentId)) {
                                children.push(node);
                                childrenByParentKey.set(parentId, children);
                            }
                        } else {
                            const parentTreeNode = nodeByKey.get(parentId);
                            processPodGroup(parentTreeNode, node, props);
                        }
                        if (props.showCompactNodes) {
                            if (childrenMap.has(parentId)) {
                                childrenMap.set(parentId, childrenMap.get(parentId).concat(allChildNodes));
                            } else {
                                childrenMap.set(parentId, allChildNodes);
                            }
                        }
                    });
                }
            });
        }
        roots.sort(compareNodes).forEach(node => {
            processNode(node, node);
            graph.setEdge(appNodeKey(props.app), treeNodeKey(node));
        });
        orphans.sort(compareNodes).forEach(node => {
            processNode(node, node);
        });
        graph.setNode(appNodeKey(props.app), {...appNode, width: NODE_WIDTH, height: NODE_HEIGHT});
        if (props.nodeFilter) {
            filterGraph(props.app, appNodeKey(props.app), graph, props.nodeFilter);
        }
        if (props.showCompactNodes) {
            groupNodes(nodes, graph);
        }
    }

    function setPodGroupNode(node: ResourceTreeNode, root: ResourceTreeNode) {
        const numberOfRows = Math.ceil(node.podGroup.pods.length / 8);
        graph.setNode(treeNodeKey(node), {...node, type: NODE_TYPES.podGroup, width: NODE_WIDTH, height: POD_NODE_HEIGHT + 30 * numberOfRows, root});
    }

    function processNode(node: ResourceTreeNode, root: ResourceTreeNode, colors?: string[]) {
        if (props.showCompactNodes && node.podGroup) {
            setPodGroupNode(node, root);
        } else {
            graph.setNode(treeNodeKey(node), {...node, width: NODE_WIDTH, height: NODE_HEIGHT, root});
        }
        (childrenByParentKey.get(treeNodeKey(node)) || []).sort(compareNodes).forEach(child => {
            if (treeNodeKey(child) === treeNodeKey(root)) {
                return;
            }
            if (node.namespace === child.namespace) {
                graph.setEdge(treeNodeKey(node), treeNodeKey(child), {colors});
            }
            processNode(child, root, colors);
        });
    }
    dagre.layout(graph);

    const edges: {from: string; to: string; lines: Line[]; backgroundImage?: string; color?: string; colors?: string | {[key: string]: any}}[] = [];
    const nodeOffset = new Map<string, number>();
    const reverseEdge = new Map<string, number>();
    graph.edges().forEach(edgeInfo => {
        const edge = graph.edge(edgeInfo);
        if (edge.points.length > 1) {
            if (!reverseEdge.has(edgeInfo.w)) {
                reverseEdge.set(edgeInfo.w, 1);
            } else {
                reverseEdge.set(edgeInfo.w, reverseEdge.get(edgeInfo.w) + 1);
            }
            if (!nodeOffset.has(edgeInfo.v)) {
                nodeOffset.set(edgeInfo.v, reverseEdge.get(edgeInfo.w) - 1);
            }
        }
    });
    graph.edges().forEach(edgeInfo => {
        const edge = graph.edge(edgeInfo);
        const colors = (edge.colors as string[]) || [];
        let backgroundImage: string;
        if (colors.length > 0) {
            const step = 100 / colors.length;
            const gradient = colors.map((lineColor, i) => {
                return `${lineColor} ${step * i}%, ${lineColor} ${step * i + step / 2}%, transparent ${step * i + step / 2}%, transparent ${step * (i + 1)}%`;
            });
            backgroundImage = `linear-gradient(90deg, ${gradient})`;
        }

        const lines: Line[] = [];
        // don't render connections from hidden node representing internal traffic
        if (edgeInfo.v === INTERNAL_TRAFFIC_NODE || edgeInfo.w === INTERNAL_TRAFFIC_NODE) {
            return;
        }
        if (edge.points.length > 1) {
            const startNode = graph.node(edgeInfo.v);
            const endNode = graph.node(edgeInfo.w);
            const offset = nodeOffset.get(edgeInfo.v);
            let startNodeRight = props.useNetworkingHierarchy ? 162 : 142;
            const endNodeLeft = 140;
            let spaceForExpansionIcon = 0;
            if (edgeInfo.v.startsWith(EXTERNAL_TRAFFIC_NODE) && !edgeInfo.v.startsWith(EXTERNAL_TRAFFIC_NODE + ':')) {
                lines.push({x1: startNode.x + 10, y1: startNode.y, x2: endNode.x - endNodeLeft, y2: endNode.y});
            } else {
                if (edgeInfo.v.startsWith(EXTERNAL_TRAFFIC_NODE + ':')) {
                    startNodeRight = 152;
                    spaceForExpansionIcon = 5;
                }
                const len = reverseEdge.get(edgeInfo.w) + 1;
                const yEnd = endNode.y - endNode.height / 2 + (endNode.height / len + (endNode.height / len) * offset);
                const firstBend =
                    spaceForExpansionIcon +
                    startNode.x +
                    startNodeRight +
                    (endNode.x - startNode.x - startNodeRight - endNodeLeft) / len +
                    ((endNode.x - startNode.x - startNodeRight - endNodeLeft) / len) * offset;
                lines.push({x1: startNode.x + startNodeRight, y1: startNode.y, x2: firstBend, y2: startNode.y});
                if (startNode.y - yEnd >= 1 || yEnd - startNode.y >= 1) {
                    lines.push({x1: firstBend, y1: startNode.y, x2: firstBend, y2: yEnd});
                }
                lines.push({x1: firstBend, y1: yEnd, x2: endNode.x - endNodeLeft, y2: yEnd});
            }
        }
        edges.push({from: edgeInfo.v, to: edgeInfo.w, lines, backgroundImage, colors: [{colors}]});
    });
    const graphNodes = graph.nodes();
    const size = getGraphSize(graphNodes.map(id => graph.node(id)));
    return (
        (graphNodes.length === 0 && (
            <EmptyState icon=' fa fa-network-wired'>
                <h4>Your application has no network resources</h4>
                <h5>Try switching to tree or list view</h5>
            </EmptyState>
        )) || (
            <div
                className={classNames('application-resource-tree', {'application-resource-tree--network': props.useNetworkingHierarchy})}
                style={{width: size.width + 150, height: size.height + 250, transformOrigin: '0% 0%', transform: `scale(${props.zoom})`}}>
                {graphNodes.map(key => {
                    const node = graph.node(key);
                    const nodeType = node.type;
                    switch (nodeType) {
                        case NODE_TYPES.filteredIndicator:
                            return <React.Fragment key={key}>{renderFilteredNode(node as any, props.onClearFilter)}</React.Fragment>;
                        case NODE_TYPES.externalTraffic:
                            return <React.Fragment key={key}>{renderTrafficNode(node)}</React.Fragment>;
                        case NODE_TYPES.internalTraffic:
                            return null;
                        case NODE_TYPES.externalLoadBalancer:
                            return <React.Fragment key={key}>{renderLoadBalancerNode(node as any)}</React.Fragment>;
                        case NODE_TYPES.groupedNodes:
                            return <React.Fragment key={key}>{renderGroupedNodes(props, node as any)}</React.Fragment>;
                        case NODE_TYPES.podGroup:
                            return <React.Fragment key={key}>{renderPodGroup(props, key, node as ResourceTreeNode & dagre.Node, childrenMap)}</React.Fragment>;
                        default:
                            return <React.Fragment key={key}>{renderResourceNode(props, key, node as ResourceTreeNode & dagre.Node, nodesHavingChildren)}</React.Fragment>;
                    }
                })}
                {edges.map(edge => (
                    <div key={`${edge.from}-${edge.to}`} className='application-resource-tree__edge'>
                        {edge.lines.map((line, i) => {
                            const distance = Math.sqrt(Math.pow(line.x1 - line.x2, 2) + Math.pow(line.y1 - line.y2, 2));
                            const xMid = (line.x1 + line.x2) / 2;
                            const yMid = (line.y1 + line.y2) / 2;
                            const angle = (Math.atan2(line.y1 - line.y2, line.x1 - line.x2) * 180) / Math.PI;
                            const lastLine = i === edge.lines.length - 1 ? line : null;
                            let arrowColor = null;
                            if (edge.colors) {
                                if (Array.isArray(edge.colors)) {
                                    const firstColor = edge.colors[0];
                                    if (firstColor.colors) {
                                        arrowColor = firstColor.colors;
                                    }
                                }
                            }
                            return (
                                <div
                                    className='application-resource-tree__line'
                                    key={i}
                                    style={{
                                        width: distance,
                                        left: xMid - distance / 2,
                                        top: yMid,
                                        backgroundImage: edge.backgroundImage,
                                        transform: props.useNetworkingHierarchy ? `translate(140px, 35px) rotate(${angle}deg)` : `translate(150px, 35px) rotate(${angle}deg)`
                                    }}>
                                    {lastLine && props.useNetworkingHierarchy && <ArrowConnector color={arrowColor} left={xMid + distance / 2} top={yMid} angle={angle} />}
                                </div>
                            );
                        })}
                    </div>
                ))}
            </div>
        )
    );
};
