import { Q } from '@nozbe/watermelondb';
import { dequal } from 'dequal';
import moment from 'moment';
import React from 'react';
import { FlatListProps, View, ViewToken, StyleSheet, Platform } from 'react-native';
import { event, Value } from 'react-native-reanimated';
import { Observable, Subscription } from 'rxjs';

import ActivityIndicator from '../../../containers/ActivityIndicator';
import { TAnyMessageModel, TMessageModel, TThreadMessageModel, TThreadModel } from '../../../definitions';
import database from '../../../lib/database';
import { compareServerVersion, debounce } from '../../../lib/methods/helpers';
import { animateNextTransition } from '../../../lib/methods/helpers/layoutAnimation';
import log from '../../../lib/methods/helpers/log';
import EmptyRoom from '../EmptyRoom';
import List, { IListProps, TListRef } from './List';
import NavBottomFAB from './NavBottomFAB';
import { loadMissedMessages, loadThreadMessages } from '../../../lib/methods';
import { Services } from '../../../lib/services';
import RefreshControl from './RefreshControl';

const QUERY_SIZE = 50;

const styles = StyleSheet.create({
	inverted: {
		...Platform.select({
			android: {
				scaleY: -1
			}
		})
	}
});

const onScroll = ({ y }: { y: Value<number> }) =>
	event(
		[
			{
				nativeEvent: {
					contentOffset: { y }
				}
			}
		],
		{ useNativeDriver: true }
	);

export { IListProps };

export interface IListContainerProps {
	renderRow: Function;
	rid: string;
	tmid?: string;
	loading: boolean;
	listRef: TListRef;
	hideSystemMessages?: string[];
	tunread?: string[];
	ignored?: string[];
	navigation: any; // TODO: type me
	showMessageInMainThread: boolean;
	serverVersion: string | null;
}

interface IListContainerState {
	messages: TAnyMessageModel[];
	refreshing: boolean;
	highlightedMessage: string | null;
}

class ListContainer extends React.Component<IListContainerProps, IListContainerState> {
	private count = 0;
	private mounted = false;
	private animated = false;
	private jumping = false;
	private cancelJump = false;
	private y = new Value(0);
	private onScroll = onScroll({ y: this.y });
	private unsubscribeFocus: () => void;
	private viewabilityConfig = {
		itemVisiblePercentThreshold: 10
	};
	private highlightedMessageTimeout: ReturnType<typeof setTimeout> | undefined | false;
	private thread?: TThreadModel;
	private messagesObservable?: Observable<TMessageModel[] | TThreadMessageModel[]>;
	private messagesSubscription?: Subscription;
	private viewableItems?: ViewToken[];

	constructor(props: IListContainerProps) {
		super(props);
		console.time(`${this.constructor.name} init`);
		console.time(`${this.constructor.name} mount`);
		this.state = {
			messages: [],
			refreshing: false,
			highlightedMessage: null
		};
		this.query();
		this.unsubscribeFocus = props.navigation.addListener('focus', () => {
			this.animated = true;
		});
		console.timeEnd(`${this.constructor.name} init`);
	}

	componentDidMount() {
		this.mounted = true;
		console.timeEnd(`${this.constructor.name} mount`);
	}

	shouldComponentUpdate(nextProps: IListContainerProps, nextState: IListContainerState) {
		const { refreshing, highlightedMessage } = this.state;
		const { hideSystemMessages, tunread, ignored, loading } = this.props;
		if (loading !== nextProps.loading) {
			return true;
		}
		if (highlightedMessage !== nextState.highlightedMessage) {
			return true;
		}
		if (refreshing !== nextState.refreshing) {
			return true;
		}
		if (!dequal(hideSystemMessages, nextProps.hideSystemMessages)) {
			return true;
		}
		if (!dequal(tunread, nextProps.tunread)) {
			return true;
		}
		if (!dequal(ignored, nextProps.ignored)) {
			return true;
		}
		return false;
	}

	componentDidUpdate(prevProps: IListContainerProps) {
		const { hideSystemMessages } = this.props;
		if (!dequal(hideSystemMessages, prevProps.hideSystemMessages)) {
			this.reload();
		}
	}

	componentWillUnmount() {
		this.unsubscribeMessages();
		if (this.unsubscribeFocus) {
			this.unsubscribeFocus();
		}
		this.clearHighlightedMessageTimeout();
		console.countReset(`${this.constructor.name}.render calls`);
	}

	// clears previous highlighted message timeout, if exists
	clearHighlightedMessageTimeout = () => {
		if (this.highlightedMessageTimeout) {
			clearTimeout(this.highlightedMessageTimeout);
			this.highlightedMessageTimeout = false;
		}
	};

	query = async () => {
		this.count += QUERY_SIZE;
		const { rid, tmid, showMessageInMainThread, serverVersion } = this.props;
		const db = database.active;

		// handle servers with version < 3.0.0
		let { hideSystemMessages = [] } = this.props;
		if (!Array.isArray(hideSystemMessages)) {
			hideSystemMessages = [];
		}

		if (tmid) {
			try {
				this.thread = await db.get('threads').find(tmid);
			} catch (e) {
				console.log(e);
			}
			this.messagesObservable = db
				.get('thread_messages')
				.query(Q.where('rid', tmid), Q.experimentalSortBy('ts', Q.desc), Q.experimentalSkip(0), Q.experimentalTake(this.count))
				.observe();
		} else if (rid) {
			const whereClause = [
				Q.where('rid', rid),
				Q.experimentalSortBy('ts', Q.desc),
				Q.experimentalSkip(0),
				Q.experimentalTake(this.count)
			] as (Q.WhereDescription | Q.Or)[];
			if (!showMessageInMainThread) {
				whereClause.push(Q.or(Q.where('tmid', null), Q.where('tshow', Q.eq(true))));
			}
			this.messagesObservable = db
				.get('messages')
				.query(...whereClause)
				.observe();
		}

		if (rid) {
			this.unsubscribeMessages();
			this.messagesSubscription = this.messagesObservable?.subscribe(messages => {
				if (tmid && this.thread) {
					messages = [...messages, this.thread];
				}

				/**
				 * Since 3.16.0 server version, the backend don't response with messages if
				 * hide system message is enabled
				 */
				if (compareServerVersion(serverVersion, 'lowerThan', '3.16.0') || hideSystemMessages.length) {
					messages = messages.filter(m => !m.t || !hideSystemMessages?.includes(m.t));
				}

				if (this.mounted) {
					this.setState({ messages }, () => this.update());
				} else {
					// @ts-ignore
					this.state.messages = messages;
				}
				// TODO: move it away from here
				this.readThreads();
			});
		}
	};

	reload = () => {
		this.count = 0;
		this.query();
	};

	readThreads = debounce(async () => {
		const { tmid } = this.props;

		if (tmid) {
			try {
				await Services.readThreads(tmid);
			} catch {
				// Do nothing
			}
		}
	}, 300);

	onEndReached = () => this.query();

	onRefresh = () =>
		this.setState({ refreshing: true }, async () => {
			const { messages } = this.state;
			const { rid, tmid } = this.props;

			if (messages.length) {
				try {
					if (tmid) {
						await loadThreadMessages({ tmid, rid });
					} else {
						await loadMissedMessages({ rid, lastOpen: moment().subtract(7, 'days').toDate() });
					}
				} catch (e) {
					log(e);
				}
			}

			this.setState({ refreshing: false });
		});

	update = () => {
		if (this.animated) {
			animateNextTransition();
		}
		this.forceUpdate();
	};

	unsubscribeMessages = () => {
		if (this.messagesSubscription && this.messagesSubscription.unsubscribe) {
			this.messagesSubscription.unsubscribe();
		}
	};

	getLastMessage = (): TMessageModel | TThreadMessageModel | null => {
		const { messages } = this.state;
		if (messages.length > 0) {
			return messages[0];
		}
		return null;
	};

	handleScrollToIndexFailed: FlatListProps<any>['onScrollToIndexFailed'] = params => {
		const { listRef } = this.props;
		listRef.current?.scrollToIndex({ index: params.highestMeasuredFrameIndex, animated: false });
	};

	jumpToMessage = (messageId: string) =>
		new Promise<void>(async resolve => {
			const { messages } = this.state;
			const { listRef } = this.props;

			// if jump to message was cancelled, reset variables and stop
			if (this.cancelJump) {
				this.resetJumpToMessage();
				return resolve();
			}
			this.jumping = true;

			// look for the message on the state
			const index = messages.findIndex(item => item.id === messageId);

			// if found message, scroll to it
			if (index > -1) {
				listRef.current?.scrollToIndex({ index, viewPosition: 0.5, viewOffset: 100 });

				// wait for scroll animation to finish
				await new Promise(res => setTimeout(res, 300));

				// if message is not visible
				if (!this.viewableItems?.map(vi => vi.key).includes(messageId)) {
					await setTimeout(() => resolve(this.jumpToMessage(messageId)), 300);
					return;
				}
				// if message is visible, highlight it
				this.setState({ highlightedMessage: messageId });
				this.clearHighlightedMessageTimeout();
				// clears highlighted message after some time
				this.highlightedMessageTimeout = setTimeout(() => {
					this.setState({ highlightedMessage: null });
				}, 5000);
				this.resetJumpToMessage();
				resolve();
			} else {
				// if message not found, wait for scroll to top and then jump to message
				listRef.current?.scrollToIndex({ index: messages.length - 1, animated: true });
				await setTimeout(() => resolve(this.jumpToMessage(messageId)), 300);
			}
		});

	resetJumpToMessage = () => {
		this.cancelJump = false;
		this.jumping = false;
	};

	cancelJumpToMessage = () => {
		if (this.jumping) {
			this.cancelJump = true;
			return;
		}
		this.resetJumpToMessage();
	};

	jumpToBottom = () => {
		const { listRef } = this.props;
		listRef.current?.scrollToOffset({ offset: -100 });
	};

	renderFooter = () => {
		const { rid, loading } = this.props;
		if (loading && rid) {
			return <ActivityIndicator />;
		}
		return null;
	};

	renderItem: FlatListProps<any>['renderItem'] = ({ item, index }) => {
		const { messages, highlightedMessage } = this.state;
		const { renderRow } = this.props;
		return <View style={styles.inverted}>{renderRow(item, messages[index + 1], highlightedMessage)}</View>;
	};

	onViewableItemsChanged: FlatListProps<any>['onViewableItemsChanged'] = ({ viewableItems }) => {
		this.viewableItems = viewableItems;
	};

	render() {
		console.count(`${this.constructor.name}.render calls`);
		const { rid, tmid, listRef } = this.props;
		const { messages, refreshing } = this.state;
		return (
			<>
				<EmptyRoom rid={rid} length={messages.length} mounted={this.mounted} />
				<RefreshControl refreshing={refreshing} onRefresh={this.onRefresh}>
					<List
						onScroll={this.onScroll}
						scrollEventThrottle={16}
						listRef={listRef}
						data={messages}
						renderItem={this.renderItem}
						onEndReached={this.onEndReached}
						ListFooterComponent={this.renderFooter}
						onScrollToIndexFailed={this.handleScrollToIndexFailed}
						onViewableItemsChanged={this.onViewableItemsChanged}
						viewabilityConfig={this.viewabilityConfig}
					/>
				</RefreshControl>
				<NavBottomFAB y={this.y} onPress={this.jumpToBottom} isThread={!!tmid} />
			</>
		);
	}
}

export type ListContainerType = ListContainer;

export default ListContainer;