/**
 * The finsemble console. It displays the filtered logs
 */

import React, { CSSProperties } from "react";
import AutoSizer from "react-virtualized/dist/commonjs/AutoSizer";
import { CellMeasurer, CellMeasurerCache, List } from "react-virtualized";
import LoggerStore from "../../../../stores/LoggerStore";
import ObjectInspector from "../../../objectInspector/object-inspector/ObjectInspector";
import inspectorTheme from "../objectInspectorTheme";
import { IconMap } from "./chromeStyleIcons";
import Highlighter from "../../../highlighter/Highlighter";
import { LoggerClient } from "../../../../stores/LoggerStore";
import "./consoleView.css";
import { LogLevels } from "../../../../../types";
import { FormattedLogMessage } from "../../../../../FormattedLogMessage";

const LOG_OUT_OF_ORDER_TOOLTIP = "This log happened after the next log in the list.";
/**
 * React component to render next to the log's icon
 * in the event that it is out of order relative
 * to the next log in the list.
 * There's a pernicious, transient bug that sometimes
 * causes logs to render out of order. We cannot reliably reproduce
 * it, so instead, we just warn the developer that something is wrong.
 */
const OutOfOrder = () => {
	const Icon = IconMap["Error"];
	return <Icon title={LOG_OUT_OF_ORDER_TOOLTIP} />;
};

const formatYYYYMMDDHHMMSS = (dateValue: Date) => {
	// Pad number with leading zeroes so that there are 2 numbers
	const pad2 = (n: number): string => (n < 10 ? "0" : "") + n;

	// Pad number with leading zeroes so that there are 3 numbers
	const pad3 = (n: number): string => {
		if (n < 10) {
			return `00${n}`;
		}
		if (n < 100) {
			return `0${n}`;
		}
		return String(n);
	};

	return `${pad2(dateValue.getHours())}:${pad2(dateValue.getMinutes())}:${pad2(dateValue.getSeconds())}:${pad3(
		dateValue.getMilliseconds()
	)}`;
};

const baseLogClass = "fsbl-log-message fsbl-plaintext-message-wrapper console-message-wrapper";
const logTypeClasses = {
	Verbose: `${baseLogClass} fsbl-log-verbose`,
	Log: `${baseLogClass} console-log-level`,
	Info: `${baseLogClass} console-info-level`,
	Debug: `${baseLogClass} console-debug-level`,
	Warn: `${baseLogClass} console-warning-level`,
	Error: `${baseLogClass} console-error-level`,
};

const cache = new CellMeasurerCache({
	defaultHeight: 50,
	fixedWidth: true,
});

let ListRef: any;
const setRef = (ref: any) => {
	ListRef = ref;
	(window as any).ListRef = ListRef;
};

interface IProps {}
interface IState {
	tableWidth: number;
	tableHeight: number;
	logCounts: {
		master: number;
		display: number;
	};
	registeredClients: Record<string, LoggerClient>;
	searchBoxIsVisible: boolean;
	searchBoxText: string;
	filterStrings: string[];
	allowedLogLevels: LogLevels;
	allClientVisibility: Record<string, boolean>;
	firstMessageTime: number;
	activeRowTimestamp: number;
	activeRowIndex: number | null;
	clientListVisible: boolean;
	comparisonRows: number[];
	comparisonRow?: FormattedLogMessage;
}

// Rough cut on component for raw table output.  Currently not used but may need to enhance if "View JSON" is too slow or limited
export default class LogList extends React.Component<IProps, IState> {
	update: boolean = false;

	ctrl: boolean = false;

	state: IState;

	constructor(props: IProps) {
		super(props);
		this.state = {
			tableWidth: 100,
			tableHeight: 100,
			logCounts: LoggerStore.getLogCounts(),
			registeredClients: LoggerStore.getRegisteredClients(),
			searchBoxIsVisible: LoggerStore.getShowSearchBox(),
			searchBoxText: LoggerStore.getSearchBoxString(),
			filterStrings: this.getFilterStrings(false),
			allowedLogLevels: LoggerStore.getLogState(),
			allClientVisibility: LoggerStore.getAllShowClientState(),
			firstMessageTime: LoggerStore.getFirstMessageTime(),
			// These two are used to keep track of your position as you're filtering.
			activeRowTimestamp: 0,
			activeRowIndex: null,
			clientListVisible: LoggerStore.getClientListVisible(),
			comparisonRows: [],
		};
		this.getRegisteredClients = this.getRegisteredClients.bind(this);
		this.updateActiveRowState = this.updateActiveRowState.bind(this);
		this.windowResize = this.windowResize.bind(this);
		this.rowRenderer = this.rowRenderer.bind(this);
		this.onRowClicked = this.onRowClicked.bind(this);
		this.setSearchBoxIsVisible = this.setSearchBoxIsVisible.bind(this);
		this.updateSearchBoxText = this.updateSearchBoxText.bind(this);
		this.getFilterStrings = this.getFilterStrings.bind(this);
		this.setFirstMessageTime = this.setFirstMessageTime.bind(this);
		this.setBottom = this.setBottom.bind(this);
		this.setActiveRowTimestamp = this.setActiveRowTimestamp.bind(this);
		this.scrollToActiveRow = this.scrollToActiveRow.bind(this);
		this.addToRowDiff = this.addToRowDiff.bind(this);
		this.onKeyDown = this.onKeyDown.bind(this);
		this.onKeyUp = this.onKeyUp.bind(this);
		this.onActiveRowChanged = this.onActiveRowChanged.bind(this);
	}

	componentDidMount() {
		ListRef.recomputeRowHeights();
		LoggerStore.addListener("loggerNewMessages", this.updateActiveRowState);
		LoggerStore.addListener("newShowClientState", this.updateActiveRowState);
		LoggerStore.addListener("showSearchBoxChange", this.setSearchBoxIsVisible);
		LoggerStore.addListener("searchBoxTextChange", this.updateSearchBoxText);
		LoggerStore.addListener("activeRowChanged", this.onActiveRowChanged);
		LoggerStore.addListener("NewHighlightValues1", this.getFilterStrings);
		LoggerStore.addListener("NewHighlightValues2", this.getFilterStrings);
		LoggerStore.addListener("NewHighlightValues3", this.getFilterStrings);
		LoggerStore.addListener("NewHighlightValues4", this.getFilterStrings);
		LoggerStore.addListener("rerenderList", this.rerenderList);
		LoggerStore.addListener("recalculateHeight", this.recalculateAllRows);
		LoggerStore.addListener("newFirstMessageTime", this.setFirstMessageTime);
		LoggerStore.addListener("SetTop", this.setTop);
		LoggerStore.addListener("SetBottom", this.setBottom);
		LoggerStore.addListener("scrollToActiveRow", this.scrollToActiveRow);
		LoggerStore.addListener("newShowClientState", this.getRegisteredClients);
		window.addEventListener("keydown", this.onKeyDown);
		window.addEventListener("keyup", this.onKeyUp);
	}

	componentWillUnmount() {
		LoggerStore.removeListener("loggerNewMessages", this.updateActiveRowState);
		LoggerStore.removeListener("newShowClientState", this.updateActiveRowState);
		LoggerStore.removeListener("showSearchBoxChange", this.setSearchBoxIsVisible);
		LoggerStore.removeListener("activeRowChanged", this.onActiveRowChanged);
		LoggerStore.removeListener("searchBoxTextChange", this.updateSearchBoxText);
		LoggerStore.removeListener("NewHighlightValues1", this.getFilterStrings);
		LoggerStore.removeListener("NewHighlightValues2", this.getFilterStrings);
		LoggerStore.removeListener("NewHighlightValues3", this.getFilterStrings);
		LoggerStore.removeListener("NewHighlightValues4", this.getFilterStrings);
		LoggerStore.removeListener("rerenderList", this.rerenderList);
		LoggerStore.removeListener("recalculateHeight", this.recalculateAllRows);
		LoggerStore.removeListener("newFirstMessageTime", this.setFirstMessageTime);
		LoggerStore.removeListener("SetTop", this.setTop);
		LoggerStore.removeListener("SetBottom", this.setBottom);
		LoggerStore.removeListener("scrollToActiveRow", this.scrollToActiveRow);
		LoggerStore.removeListener("newShowClientState", this.getRegisteredClients);
		window.removeEventListener("keydown", this.onKeyDown);
		window.removeEventListener("keyup", this.onKeyUp);
	}

	getRegisteredClients() {
		this.setState({
			registeredClients: LoggerStore.getRegisteredClients(),
		});
	}

	setFirstMessageTime() {
		this.setState({
			firstMessageTime: LoggerStore.getFirstMessageTime(),
		});
	}

	// whether to return or set state...the constructor needs the values. everywhere else is setState
	getFilterStrings(setState: boolean = true): string[] {
		let filterStrings = LoggerStore.getFilterStrings().map((filter) => filter.str);
		if (setState) {
			this.setState({
				filterStrings,
			});
		}
		return filterStrings;
	}

	updateSearchBoxText() {
		this.setState({
			searchBoxText: LoggerStore.getSearchBoxString(),
		});
	}

	setSearchBoxIsVisible() {
		this.setState({
			searchBoxIsVisible: LoggerStore.getShowSearchBox(),
		});
	}

	setActiveRowTimestamp(timestamp) {
		this.setState({
			activeRowTimestamp: timestamp,
			activeRowIndex: this.getActiveRowIndex(timestamp),
		});
	}

	windowResize() {
		const newTableWidth = LoggerStore.getWindowWidth();
		const windowHeight = LoggerStore.getWindowHeight();
		const logHeader = document.querySelector("#message-list");
		const offsetTop = logHeader ? logHeader.getBoundingClientRect().top : 600;
		const newTableHeight = Math.abs(windowHeight - offsetTop - 5);
		this.update = true;
		this.setState({
			tableWidth: newTableWidth,
			tableHeight: newTableHeight,
		});
	}

	updateActiveRowState() {
		this.update = true;
		this.setState({
			activeRowIndex: this.getActiveRowIndex(),
		});
	}

	onActiveRowChanged() {
		const index = LoggerStore.getActiveRowIndex();
		this.setState(
			{
				comparisonRows: [],
				activeRowIndex: index,
			},
			() => {
				this.scrollToActiveRow();
				const filteredLog = LoggerStore.getFilteredLog();
				if (filteredLog[index]) {
					this.setActiveRowTimestamp(filteredLog[index].logTimestamp);
				}
				this.addToRowDiff(index);
			}
		);
	}

	onRowClicked(index: number = 0) {
		cache.clear(index);
		this.setActiveRowTimestamp(LoggerStore.getFilteredLog()[index].logTimestamp);
		this.addToRowDiff(index);
	}

	recalculateAllRows() {
		cache.clearAll();
		ListRef.recomputeRowHeights();
	}

	rerenderList() {
		cache.clearAll();
		ListRef.forceUpdate();
	}

	scrollToActiveRow() {
		if (this.state.activeRowIndex === null || typeof this.state.activeRowIndex === "undefined") return;
		ListRef.recomputeRowHeights(this.state.activeRowIndex);
		ListRef.scrollToRow(this.state.activeRowIndex);
	}

	setTop() {
		ListRef.scrollToRow(0);
	}

	setBottom() {
		const filteredLog = LoggerStore.getFilteredLog();
		// don't continue if empty list -- the code doesn't handle it and crashes (probably because of length-1 calculation)
		if (filteredLog.length > 0) {
			ListRef.recomputeRowHeights(filteredLog.length - 1);
			ListRef.scrollToRow(filteredLog.length - 1);
		}
	}

	openClient(windowName: string, viewId: number | null = null) {
		LoggerStore.maybeOpenClient({ windowName, viewId });
	}

	addToRowDiff(index: number) {
		const filteredLog = LoggerStore.getFilteredLog();
		let targetRow = filteredLog[index];
		let rowIndexes = this.state.comparisonRows.filter((row) => row !== undefined);
		let rowDiff: number | null = null;
		if (this.state.comparisonRow) {
			if (this.ctrl) {
				if (rowIndexes.length == 1) {
					rowIndexes.push(index);
				} else {
					rowIndexes = [index];
				}
				targetRow = this.state.comparisonRow;
			} else {
				rowIndexes = [index];
			}
		} else {
			rowIndexes = [index];
		}

		if (rowIndexes.length === 2) {
			rowDiff = Math.abs(
				filteredLog[rowIndexes[0]].timeElapsedFromStartup - filteredLog[rowIndexes[1]].timeElapsedFromStartup
			);
		}

		LoggerStore.setRowTimeDiff(rowDiff);
		this.update = true;
		this.setState({
			comparisonRow: targetRow,
			comparisonRows: rowIndexes,
		});
	}

	getLogContent(strToHighlight: string, ind: number) {
		let messageClasses = "highlighted-message";
		if (typeof strToHighlight === "string" && strToHighlight.includes("Stack")) {
			messageClasses = `${messageClasses} message-log-stack`;
		}
		return (
			<span key={ind} className={messageClasses}>
				<Highlighter
					ind={ind}
					className={messageClasses}
					str={strToHighlight}
					filterStrings={this.state.filterStrings}
					searchBoxText={this.state.searchBoxText}
				/>
				&nbsp;
			</span>
		);
	}

	rowRenderer({ index, key, parent, style }: { index: number; key: string; parent: any; style: CSSProperties }) {
		const filteredLog = LoggerStore.getFilteredLog();
		const log = filteredLog[index];

		let row = this.state.registeredClients[log.logClientName];
		let viewId = row?.viewId ? row.viewId : null;

		/**
		 * Occasionally we have had internal reports of log messages being out of order
		 * We could not replicate it. In order to prevent devs from
		 * going down the wrong path when debugging a problem, we check to see
		 * if this log happened after the log next in the list.
		 *
		 * If it is, we will render a small error icon that tells the dev
		 * "Hey, something is awry here".
		 */
		const nextLog = filteredLog[index + 1];
		let isOutOfOrder = false;
		if (nextLog && log.logTimestamp > nextLog.logTimestamp) {
			isOutOfOrder = true;
		}

		if (!log) {
			return null;
		}
		log.previousRowTimeDelta = 0;
		log.timeElapsedFromStartup = Math.round(log.logTimestamp - this.state.firstMessageTime);
		if (index > 0) {
			const previousLog = filteredLog[index - 1];
			if (previousLog) {
				log.previousRowTimeDelta = log.timeElapsedFromStartup - previousLog.timeElapsedFromStartup;
			}
		}

		// "style" is a react prop. It is const, so it cannot be modified. Since we want to change
		// the style here we'll clone it and use the clone instead.
		const styleClone: CSSProperties = {
			...style,
			wordBreak: "break-all",
			display: "flex",
			alignItems: "center",
			justifyContent: "space-between",
			padding: "3px",
			width: "calc(100% - 8px)",
		};
		// log.logMessageWithoutStack = log.allLogArgs.replace(/Log Stack.*/g, "");
		const LogIcon = IconMap[log.logType];
		let wrapperClasses = logTypeClasses[log.logType];
		if (index === this.state.activeRowIndex) {
			wrapperClasses = `${wrapperClasses} active-console-row`;
		}
		if (this.state.comparisonRows.includes(index)) {
			wrapperClasses = `${wrapperClasses} fsbl-logger-row-selected`;
		}
		const timestampClasses = isOutOfOrder ? "time-elapsed out-of-order" : "time-elapsed";
		const content = (
			<div
				onContextMenu={() => {
					cache.clear(index);
					ListRef.recomputeRowHeights(index);
				}}
				// to handle expansion of js objects.
				onClick={() => {
					this.onRowClicked(index);
				}}
				style={styleClone}
				className={wrapperClasses}
				key={key}
			>
				<LogIcon />
				{isOutOfOrder && <OutOfOrder />}
				<div title={isOutOfOrder ? LOG_OUT_OF_ORDER_TOOLTIP : ""} className={timestampClasses}>
					{formatYYYYMMDDHHMMSS(new Date(log.logTimestamp))}
				</div>
				<div className="console-message-text fsbl-plaintext-log-message" style={{ width: "100%" }}>
					{log.parsedLogArgs.map((arg: any, i: number) => {
						if (typeof arg === "object") {
							return (
								<span
									key={i}
									style={{
										display: "inline-block !important",
										marginRight: "10px",
									}}
								>
									<ObjectInspector
										filterStrings={this.state.filterStrings || []}
										searchBoxText={this.state.searchBoxText}
										theme={inspectorTheme}
										data={arg}
									/>
								</span>
							);
						}
						return this.getLogContent(arg, i);
					})}
				</div>
				<div
					className="fsbl-plaintext-extra-info"
					onClick={() => {
						this.openClient(row?.name, viewId);
					}}
				>
					{log.logClientName}
				</div>
			</div>
		);
		return (
			<CellMeasurer cache={cache} columnIndex={0} key={key} parent={parent} rowIndex={index}>
				{content}
			</CellMeasurer>
		);
	}

	shouldComponentUpdate() {
		let update = false;
		if (LoggerStore.getWindowVisibility() === "visible") {
			update = true;
		}
		return update;
	}

	getActiveRowIndex(timestamp: number = this.state.activeRowTimestamp) {
		const filteredLog = LoggerStore.getFilteredLog();
		for (let i = 0; i < filteredLog.length; i++) {
			const log = filteredLog[i];
			if (log.logTimestamp === timestamp) {
				return i;
			}
		}
		return null;
	}

	onKeyDown(e: KeyboardEvent) {
		this.ctrl = e.ctrlKey;
	}

	onKeyUp() {
		this.ctrl = false;
	}

	render() {
		cache.clearAll();
		let heightOffset = document.getElementById("log-level-filters")?.offsetHeight || 63;
		if (this.state.searchBoxIsVisible) {
			heightOffset = heightOffset + 20;
		}

		return (
			<AutoSizer>
				{({ width, height }) => (
					<div
						style={{
							width: `${width}px`,
						}}
					>
						<List
							id="log-console"
							ref={setRef}
							height={height - heightOffset}
							width={width}
							overscanRowCount={0}
							noRowsRenderer={() => null}
							rowCount={LoggerStore.getFilteredLog().length}
							deferredMeasurementCache={cache}
							rowHeight={cache.rowHeight || 20}
							rowRenderer={this.rowRenderer}
						/>
					</div>
				)}
			</AutoSizer>
		);
	}
}
