import React from "react";
import PropTypes from "prop-types";
import TreeNode from "./TreeNode";

import { DEFAULT_ROOT_PATH, hasChildNodes, getExpandedPaths } from "./pathUtils";

const reducer = (state, action) => {
	if (action.type === "TOGGLE_EXPAND") {
		let { path } = action;
		if (!Array.isArray(path)) {
			path = [path];
		}
		const ret = { ...state };

		path.forEach((objPath) => {
			const { expandedPaths } = state;
			const expanded = !!expandedPaths[objPath];
			ret.expandedPaths[objPath] = !expanded;
		});
		return ret;
	} else {
		return state;
	}
};

class ConnectedTreeNode extends React.Component<{
	name: string;
	data: any;
	dataIterator: Function;
	depth: number;
	expanded: boolean;
	nodeRenderer: (args: any) => JSX.Element;
	filterStrings: string[];
	searchBoxText: string;
	path: string;
	onMatchFound: Function;
}> {
	state: {
		depth: number;
		expandedPaths: string[];
	};

	constructor(props, context) {
		super(props);
		this.state = context.store.storeState;
		this.state.depth = props.depth;
	}

	shouldComponentUpdate(nextProps, nextState) {
		const filterChanged = this.props.filterStrings.every((str) => nextProps.filterStrings.includes(str));
		const searchBoxTextChanged = nextProps.searchBoxText !== this.props.searchBoxText;
		return (
			filterChanged ||
			searchBoxTextChanged ||
			!!nextState.expandedPaths[nextProps.path] !== !!this.state.expandedPaths[this.props.path] ||
			nextProps.data !== this.props.data ||
			nextProps.name !== this.props.name
		);
	}

	handleRightClick(path: string) {
		this.handleClick(path);
	}

	handleClick(path: string) {
		this.context.store.storeState = reducer(this.context.store.storeState, {
			type: "TOGGLE_EXPAND",
			path,
		});
		this.setState(this.context.store.storeState);
	}

	renderChildNodes(parentData, parentPath) {
		const { dataIterator } = this.props;
		const { depth } = this.props;

		const { onMatchFound, nodeRenderer, searchBoxText, filterStrings, expanded } = this.props;

		const childNodes: any[] = [];
		for (const { name, data } of dataIterator(parentData)) {
			const key = name;
			const path = `${parentPath}.${key}`;
			childNodes.push(
				<ConnectedTreeNode
					name={name}
					data={data}
					depth={depth + 1}
					path={path}
					key={key}
					onMatchFound={onMatchFound}
					dataIterator={dataIterator}
					nodeRenderer={nodeRenderer}
					searchBoxText={searchBoxText}
					filterStrings={filterStrings}
					expanded={expanded}
				/>
			);
		}
		return childNodes;
	}

	render() {
		const { data, dataIterator, path, depth, name } = this.props;

		const nodeHasChildNodes = hasChildNodes(data, dataIterator);
		const { expandedPaths } = this.state;
		const expanded = !!expandedPaths[path];
		const { nodeRenderer } = this.props;

		return (
			<TreeNode
				expanded={expanded}
				onContextMenu={() => {
					this.handleRightClick(path);
				}}
				onClick={
					nodeHasChildNodes
						? () => {
								this.handleClick(path);
						  }
						: () => {}
				}
				// show arrow anyway even if not expanded and not rendering children
				shouldShowArrow={nodeHasChildNodes}
				// show placeholder only for non root nodes
				shouldShowPlaceholder={depth > 0}
				// Render a node from name and data (or possibly other props like isNonenumerable)
				nodeRenderer={nodeRenderer}
				name={name}
				data={data}
			>
				{
					// only render if the node is expanded
					expanded ? this.renderChildNodes(data, path) : undefined
				}
			</TreeNode>
		);
	}
}

// @ts-ignore Convert to modern React context
ConnectedTreeNode.contextTypes = {
	store: PropTypes.any,
};

/**
 * See ObjectInspector for description of props
 */
class TreeView extends React.PureComponent<{
	name: string;
	data: any;
	dataIterator: Function;
	nodeRenderer: (args: any) => JSX.Element;
	filterStrings: string[];
	searchBoxText: string;
	onMatchFound: Function;
	expanded: boolean;
	expandPaths?: string[];
	expandLevel?: number;
}> {
	store: any;

	static defaultProps: {
		expandLevel: number;
		expandPaths: string[];
	} = {
		expandLevel: 0,
		expandPaths: [],
	};

	static childContextTypes = {
		store: PropTypes.any,
	};

	constructor(props) {
		super(props);
		this.state = {
			expandLevel: 0,
		};
		this.store = {
			storeState: {
				expandedPaths: getExpandedPaths(props.data, props.dataIterator, props.expandPaths, props.expandLevel),
			},
		};
	}

	UNSAFE_componentWillReceiveProps(nextProps) {
		this.store = {
			storeState: {
				expandedPaths: getExpandedPaths(
					nextProps.data,
					nextProps.dataIterator,
					nextProps.expandPaths,
					nextProps.expandLevel,
					this.store.storeState.expandedPaths
				),
			},
		};
	}

	getChildContext() {
		return {
			store: this.store,
		};
	}

	render() {
		const { name, data, dataIterator, expanded, nodeRenderer, filterStrings, searchBoxText, onMatchFound } = this.props;
		const rootPath = DEFAULT_ROOT_PATH;
		return (
			<ConnectedTreeNode
				name={name}
				data={data}
				dataIterator={dataIterator}
				depth={0}
				path={rootPath}
				nodeRenderer={nodeRenderer}
				searchBoxText={searchBoxText}
				filterStrings={filterStrings}
				onMatchFound={onMatchFound}
				expanded={expanded}
			/>
		);
	}
}

export default TreeView;
