import { decode } from "html-entities";
import type { Token, Tokens } from "marked";
import type { ReactNode } from "react";
import type { ImageStyle, TextStyle, ViewStyle } from "react-native";
import type { MarkedStyles } from "../theme/types";
import { getTableColAlignmentStyle } from "./../utils/table";
import { getValidURL } from "./../utils/url";
import type { ParserOptions, RendererInterface } from "./types";

class Parser {
	private renderer: RendererInterface;
	private styles: MarkedStyles;
	private headingStylesMap: Record<number, TextStyle | undefined>;
	private baseUrl: string;

	constructor(options: ParserOptions) {
		this.styles = { ...options.styles };
		this.baseUrl = options.baseUrl ?? "";
		this.renderer = options.renderer;
		this.headingStylesMap = {
			1: this.styles.h1,
			2: this.styles.h2,
			3: this.styles.h3,
			4: this.styles.h4,
			5: this.styles.h5,
			6: this.styles.h6,
		};
	}

	parse(tokens?: Token[]) {
		return this._parse(tokens);
	}

	private _parse(
		tokens?: Token[],
		styles?: ViewStyle | TextStyle | ImageStyle,
	): ReactNode[] {
		if (!tokens) return [];

		const elements: ReactNode[] = tokens.map((token) => {
			return this._parseToken(token, styles);
		});
		return elements.filter((element) => element !== null);
	}

	private _parseToken(
		token: Token,
		styles?: ViewStyle | TextStyle | ImageStyle,
	): ReactNode {
		switch (token.type) {
			case "paragraph": {
				const children = this.getNormalizedSiblingNodesForBlockAndInlineTokens(
					token.tokens ?? [],
					this.styles.text,
				);

				return this.renderer.paragraph(children, this.styles.paragraph);
			}
			case "blockquote": {
				const children = this.parse(token.tokens);
				return this.renderer.blockquote(children, this.styles.blockquote);
			}
			case "heading": {
				const styles = this.headingStylesMap[token.depth];

				if (this.hasDuplicateTextChildToken(token)) {
					return this.renderer.heading(token.text, styles, token.depth);
				}

				const children = this._parse(token.tokens, styles);
				return this.renderer.heading(children, styles, token.depth);
			}
			case "code": {
				return this.renderer.code(
					token.text,
					token.lang,
					this.styles.code,
					this.styles.em,
				);
			}
			case "hr": {
				return this.renderer.hr(this.styles.hr);
			}
			case "list": {
				let startIndex = Number.parseInt(token.start.toString(), 10);
				if (Number.isNaN(startIndex)) {
					startIndex = 1;
				}
				const li = (token as Tokens.List).items.map((item) => {
					const children = item.tokens.flatMap((cItem) => {
						if (cItem.type === "text") {
							/* getViewNode since tokens could contain a block like elements (i.e. img) */
							const childTokens = (cItem as Tokens.Text).tokens || [];
							const listChildren =
								this.getNormalizedSiblingNodesForBlockAndInlineTokens(
									childTokens,
									this.styles.li,
								);
							// return this.renderer.listItem(listChildren, this.styles.li);
							return listChildren;
						}

						/* Parse the nested token */
						return this._parseToken(cItem);
					});

					return this.renderer.listItem(children, this.styles.li);
				});

				return this.renderer.list(
					token.ordered,
					li,
					this.styles.list,
					this.styles.li,
					startIndex,
				);
			}
			case "escape": {
				return this.renderer.escape(token.text, {
					...this.styles.text,
					...styles,
				});
			}
			case "link": {
				// Don't render anchors without text and children
				if (token.text.trim().length < 1 || !token.tokens) {
					return null;
				}

				// Note: Linking Images (https://www.markdownguide.org/basic-syntax/#linking-images) are wrapped
				// in paragraph token, so will be handled via `getNormalizedSiblingNodesForBlockAndInlineTokens`
				const linkStyle = {
					...this.styles.link,
					...styles,
					// To override color and fontStyle properties
					color: this.styles.link?.color,
					fontStyle: this.styles.link?.fontStyle,
				};
				const href = getValidURL(this.baseUrl, token.href);

				if (this.hasDuplicateTextChildToken(token)) {
					return this.renderer.link(token.text, href, linkStyle, token.title);
				}

				const children = this._parse(token.tokens, linkStyle);
				return this.renderer.link(children, href, linkStyle, token.title);
			}
			case "image": {
				return this.renderer.image(
					token.href,
					token.text,
					this.styles.image,
					token.title,
				);
			}
			case "strong": {
				const boldStyle = {
					...this.styles.strong,
					...styles,
				};
				if (this.hasDuplicateTextChildToken(token)) {
					return this.renderer.strong(token.text, boldStyle);
				}

				const children = this._parse(token.tokens, boldStyle);
				return this.renderer.strong(children, boldStyle);
			}
			case "em": {
				const italicStyle = {
					...this.styles.em,
					...styles,
				};
				if (this.hasDuplicateTextChildToken(token)) {
					return this.renderer.em(token.text, italicStyle);
				}

				const children = this._parse(token.tokens, italicStyle);
				return this.renderer.em(children, italicStyle);
			}
			case "codespan": {
				return this.renderer.codespan(decode(token.text), {
					...this.styles.codespan,
					...styles,
				});
			}
			case "br": {
				return this.renderer.br();
			}
			case "del": {
				const strikethroughStyle = {
					...this.styles.strikethrough,
					...styles,
				};
				if (this.hasDuplicateTextChildToken(token)) {
					return this.renderer.del(token.text, strikethroughStyle);
				}

				const children = this._parse(token.tokens, strikethroughStyle);
				return this.renderer.del(children, strikethroughStyle);
			}
			case "text":
				return this.renderer.text(token.raw, {
					...this.styles.text,
					...styles,
				});
			case "html": {
				return this.renderer.html(token.raw, {
					...this.styles.text,
					...styles,
				});
			}
			case "table": {
				const header = (token as Tokens.Table).header.map((row, i) =>
					this._parse(row.tokens, {
						...getTableColAlignmentStyle(token.align[i]),
					}),
				);

				const rows = (token as Tokens.Table).rows.map((cols) =>
					cols.map((col, i) =>
						this._parse(col.tokens, {
							...getTableColAlignmentStyle(token.align[i]),
						}),
					),
				);

				return this.renderer.table(
					header,
					rows,
					this.styles.table,
					this.styles.tableRow,
					this.styles.tableCell,
				);
			}
			default: {
				return null;
			}
		}
	}

	private getNormalizedSiblingNodesForBlockAndInlineTokens(
		tokens: Token[],
		textStyle?: TextStyle,
	): ReactNode[] {
		let tokenRenderQueue: Token[] = [];
		const siblingNodes: ReactNode[] = [];
		for (const t of tokens) {
			/**
			 * To avoid inlining images
			 * Currently supports images, link images
			 * Note: to be extend for other token types
			 */
			if (
				t.type === "image" ||
				(t.type === "link" &&
					t.tokens &&
					t.tokens[0] &&
					t.tokens[0].type === "image")
			) {
				// Render existing inline tokens in the queue
				const parsed = this._parse(tokenRenderQueue);
				if (parsed.length > 0) {
					siblingNodes.push(this.renderer.text(parsed, textStyle));
				}

				// Render the current block token
				if (t.type === "image") {
					siblingNodes.push(this._parseToken(t));
				} else if (t.type === "link" && t.tokens && t.tokens[0]) {
					const imageToken = t.tokens[0] as Tokens.Image;
					const href = getValidURL(this.baseUrl, t.href);
					siblingNodes.push(
						this.renderer.linkImage(
							href,
							imageToken.href,
							imageToken.text ?? imageToken.title ?? "",
							this.styles.image,
							imageToken.title,
						),
					);
				}

				tokenRenderQueue = [];
				continue;
			}
			tokenRenderQueue = [...tokenRenderQueue, t];
		}

		/* Remaining temp tokens if any */
		if (tokenRenderQueue.length > 0) {
			siblingNodes.push(this.renderer.text(this.parse(tokenRenderQueue), {}));
		}

		return siblingNodes;
	}

	// To avoid duplicate text node nesting when there are no child tokens with text emphasis (i.e., italic)
	// ref: https://github.com/gmsgowtham/react-native-marked/issues/522
	private hasDuplicateTextChildToken(token: Token): boolean {
		if (!("tokens" in token)) {
			return false;
		}

		if (
			token.tokens &&
			token.tokens.length === 1 &&
			token.tokens[0]?.type === "text"
		) {
			return true;
		}

		return false;
	}
}

export default Parser;
