/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
import brewRendererStylesUrl from './brewRenderer.less?url';
import headerNavStylesUrl from './headerNav/headerNav.less?url';
import './brewRenderer.less';
import React, { useState, useRef, useMemo, useEffect } from 'react';
import _ from 'lodash';

import MarkdownLegacy from '@shared/markdownLegacy.js';
import Markdown from '@shared/markdown.js';
import ErrorBar from './errorBar/errorBar.jsx';
import ToolBar  from './toolBar/toolBar.jsx';

//TODO: move to the brew renderer
import RenderWarnings from '@components/renderWarnings/renderWarnings.jsx';
import NotificationPopup from './notificationPopup/notificationPopup.jsx';
import Frame from 'react-frame-component';
import dedent from 'dedent';
import { printCurrentBrew } from '@shared/helpers.js';

import HeaderNav from './headerNav/headerNav.jsx';
import safeHTML from './safeHTML.js';

const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
const PAGEBREAK_REGEX_LEGACY = /\\page(?:break)?/m;
const COLUMNBREAK_REGEX_LEGACY = /\\column(:?break)?/m;
const PAGE_HEIGHT = 1056;

const TOOLBAR_STATE_KEY = 'HB_renderer_toolbarState';

const INITIAL_CONTENT = dedent`
	<!DOCTYPE html><html><head>
	<title>Rendered Brew Content</title>
	<link href='/homebrew/bundle.css' type="text/css" rel='stylesheet' />
	<link href="${brewRendererStylesUrl}" rel="stylesheet" />
	<link href="${headerNavStylesUrl}" rel="stylesheet" />
	<base target="_top">
	</head><body style='overflow: hidden'><div></div></body></html>`;


//v=====----------------------< Brew Page Component >---------------------=====v//
const BrewPage = (props)=>{
	props = {
		contents : '',
		index    : 0,
		hoisted  : false,
		...props
	};
	const pageRef   = useRef(null);
	const cleanText = safeHTML(props.contents);

	useEffect(()=>{
		if(!pageRef.current) return;

		// Observer for tracking pages within the `.pages` div
		const visibleObserver = new IntersectionObserver(
			(entries)=>{
				entries.forEach((entry)=>{
					if(entry.isIntersecting)
						props.onVisibilityChange(props.index + 1, true, false); // add page to array of visible pages.
					else
						props.onVisibilityChange(props.index + 1, false, false);
				});
			},
			{ threshold: .3, rootMargin: '0px 0px 0px 0px'  } // detect when >30% of page is within bounds.
		);

		// Observer for tracking the page at the center of the iframe.
		const centerObserver = new IntersectionObserver(
			(entries)=>{
				entries.forEach((entry)=>{
					if(entry.isIntersecting)
						props.onVisibilityChange(props.index + 1, true, true); // Set this page as the center page
				});
			},
			{ threshold: 0, rootMargin: '-50% 0px -50% 0px' } // Detect when the page is at the center
		);

		// attach observers to each `.page`
		visibleObserver.observe(pageRef.current);
		centerObserver.observe(pageRef.current);
		return ()=>{
			visibleObserver.disconnect();
			centerObserver.disconnect();
		};
	}, []);

	return <div className={props.className} id={`p${props.index + 1}`} data-index={props.index} ref={pageRef} style={props.style} {...props.attributes}>
	         <div className='columnWrapper' dangerouslySetInnerHTML={{ __html: cleanText }} />
	       </div>;
};


//v=====--------------------< Brew Renderer Component >-------------------=====v//
let renderedPages = [];
let pageTemplates = [];
let rawPages      = [];

const BrewRenderer = (props)=>{
	props = {
		text                       : '',
		style                      : '',
		renderer                   : 'legacy',
		theme                      : '5ePHB',
		lang                       : '',
		errors                     : [],
		currentEditorCursorPageNum : 1,
		currentEditorViewPageNum   : 1,
		currentBrewRendererPageNum : 1,
		themeBundle                : {},
		onPageChange               : ()=>{},
		...props
	};

	const [state, setState] = useState({
		isMounted    : false,
		visibility   : 'hidden',
		visiblePages : [],
		centerPage   : 1
	});

	const [displayOptions, setDisplayOptions] = useState({
		zoomLevel    : 100,
		spread       : 'single',
		startOnRight : true,
		pageShadows  : true,
		rowGap       : 5,
		columnGap    : 10,
	});

	//useEffect to store or gather toolbar state from storage
	useEffect(()=>{
		const toolbarState = JSON.parse(window.localStorage.getItem(TOOLBAR_STATE_KEY));
		toolbarState &&	setDisplayOptions(toolbarState);
	}, []);

	const [headerState, setHeaderState] = useState(false);

	const mainRef  = useRef(null);
	const pagesRef = useRef(null);
	const urlRef = useRef('');

	if(props.renderer == 'legacy') {
		rawPages = props.text.split(PAGEBREAK_REGEX_LEGACY);
	} else {
		rawPages = props.text.split(PAGEBREAK_REGEX_V3);
	}

	const handlePageVisibilityChange = (pageNum, isVisible, isCenter)=>{
		setState((prevState)=>{
			const updatedVisiblePages = new Set(prevState.visiblePages);
			if(!isCenter)
				isVisible ? updatedVisiblePages.add(pageNum) : updatedVisiblePages.delete(pageNum);

			return {
				...prevState,
				visiblePages : [...updatedVisiblePages].sort((a, b)=>a - b),
				centerPage   : isCenter ? pageNum : prevState.centerPage
			};
		});

		if(isCenter)
			props.onPageChange(pageNum);
	};

	const isInView = (index)=>{
		if(!state.isMounted)
			return false;

		if(index == props.currentEditorCursorPageNum - 1)	//Already rendered before this step
			return false;

		if(Math.abs(index - props.currentBrewRendererPageNum - 1) <= 3)
			return true;

		return false;
	};

	const renderDummyPage = (index)=>{
		return <div className='phb page' id={`p${index + 1}`} key={index}>
			<i className='fas fa-spinner fa-spin' />
		</div>;
	};

	const renderStyle = ()=>{
		const themeStyles = props.themeBundle?.joinedStyles ?? '<style>@import url("/themes/V3/Blank/style.css");</style>';
		const cleanStyle = safeHTML(`${themeStyles} \n\n <style> ${props.style} </style>`);
		return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: cleanStyle }} />;
	};

	const renderPage = (pageText, index)=>{

		let styles = {
			...(!displayOptions.pageShadows ? { boxShadow: 'none' } : {})
			// Add more conditions as needed
		};
		let classes    = 'page';
		let attributes = {};

		if(props.renderer == 'legacy') {
			pageText.replace(COLUMNBREAK_REGEX_LEGACY, '```\n````\n'); // Allow Legacy brews to use `\column(break)`
			const html = MarkdownLegacy.render(pageText);

			return <BrewPage className='page phb' index={index} key={index} contents={html} style={styles} onVisibilityChange={handlePageVisibilityChange} />;
		} else {
			if(pageText.startsWith('\\page')) {
				const firstLineTokens  = Markdown.marked.lexer(pageText.split('\n', 1)[0])[0].tokens;
				const injectedTags = firstLineTokens?.find((obj)=>obj.injectedTags !== undefined)?.injectedTags;
				if(injectedTags) {
					styles     = { ...styles, ...injectedTags.styles };
					styles     = _.mapKeys(styles, (v, k)=>k.startsWith('--') ? k : _.camelCase(k)); // Convert CSS to camelCase for React
					classes    = [classes, injectedTags.classes].join(' ').trim();
					attributes = injectedTags.attributes;
					if(global.enablev4) {
						if(attributes && Object.hasOwn(attributes, 'hbtemplate')) {
							pageTemplates[index] = attributes['hbtemplate'];
						}
					}
				}
				if(global.enablev4) {
					// If we don't have a template for this page, look backwards until one is found or the first page.
					if(!pageTemplates[index]) {
						for (let i=index;i>=0; i--) {
							// If one is found, add the template attribute
							if(pageTemplates[i]) attributes['hbtemplate'] = pageTemplates[i];
						}
					}
				}
				pageText = pageText.includes('\n') ? pageText.substring(pageText.indexOf('\n') + 1) : ''; // Remove the \page line
			}

			// DO NOT REMOVE!!! REQUIRED FOR BACKWARDS COMPATIBILITY WITH NON-UPGRADABLE VERSIONS OF CHROME.
			pageText += `\n\n&nbsp;\n\\column\n&nbsp;`; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)

			const html = Markdown.render(pageText, index);

			return <BrewPage className={classes} index={index} key={index} contents={html} style={styles} attributes={attributes} onVisibilityChange={handlePageVisibilityChange} />;
		}
	};

	const renderPages = (checkHoists = false)=>{

		if(props.errors && props.errors.length)
			return renderedPages;

		if(rawPages.length != renderedPages.length) { // Re-render all pages when page count changes
			renderedPages.length = 0;
			pageTemplates.length = 0;
		}

		// Render currently-edited page first so cross-page effects (variables, links) can propagate out first
		if(rawPages.length > props.currentEditorCursorPageNum -1)
			renderedPages[props.currentEditorCursorPageNum - 1] = renderPage(rawPages[props.currentEditorCursorPageNum - 1], props.currentEditorCursorPageNum - 1);

		_.forEach(rawPages, (page, index)=>{
			const varsOnPageRegex = /([!$]?)\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]/g; // Find out if there are any vars on the page.
			const forceRender = checkHoists &&
				!props.hoisted &&
				(page.match(varsOnPageRegex));  // forceRender forces pages outside of the PPR range to render if true.
			                                    // This is necessary on the first load to fully populate the variable table.
			if((isInView(index) || !renderedPages[index] || forceRender) && typeof window !== 'undefined'){
				renderedPages[index] = renderPage(page, index); // Render any page not yet rendered, but only re-render those in PPR range
			}
		});
		if(!props.hoisted) { props.hoisted = true; } // Only fully hoist once.
		return renderedPages;
	};

	const handleControlKeys = (e)=>{
		if(!(e.ctrlKey || e.metaKey)) return;
		const P_KEY = 80;
		if(e.keyCode == P_KEY && props.allowPrint) printCurrentBrew();
		if(e.keyCode == P_KEY) {
			e.stopPropagation();
			e.preventDefault();
		}
	};

	const scrollToHash = (hash)=>{
		if(!hash) return;

		const iframeDoc = document.getElementById('BrewRenderer').contentDocument;
		let anchor = iframeDoc.querySelector(hash);

		if(anchor) {
			anchor.scrollIntoView({ behavior: 'smooth' });
		} else {
			// Use MutationObserver to wait for the element if it's not immediately available
			new MutationObserver((mutations, obs)=>{
				anchor = iframeDoc.querySelector(hash);
				if(anchor) {
					anchor.scrollIntoView({ behavior: 'smooth' });
					obs.disconnect();
				}
			}).observe(iframeDoc, { childList: true, subtree: true });
		}
	};

	const frameDidMount = ()=>{	//This triggers when iFrame finishes internal "componentDidMount"
		scrollToHash(window.location.hash);

		window.addEventListener('hashchange', ()=>scrollToHash(window.location.hash));

		setTimeout(()=>{	//We still see a flicker where the style isn't applied yet, so wait 100ms before showing iFrame
			renderPages(true); //Make sure page is renderable before showing
			setState((prevState)=>({
				...prevState,
				isMounted  : true,
				visibility : 'visible'
			}));
		}, 100);
	};

	const emitClick = ()=>{ // Allow clicks inside iFrame to interact with dropdowns, etc. from outside
		if(!window || !document) return;
		document.dispatchEvent(new MouseEvent('click'));
	};

	const handleDisplayOptionsChange = (newDisplayOptions)=>{
		setDisplayOptions(newDisplayOptions);
		localStorage.setItem(TOOLBAR_STATE_KEY, JSON.stringify(newDisplayOptions));
	};

	const pagesStyle = {
		zoom      : `${displayOptions.zoomLevel}%`,
		columnGap : `${displayOptions.columnGap}px`,
		rowGap    : `${displayOptions.rowGap}px`
	};

	const renderedStyle = useMemo(()=>renderStyle(), [props.style, props.themeBundle]);
	renderedPages = useMemo(()=>renderPages(), [props.text, displayOptions]);

	return (
		<>
			{/*render dummy page while iFrame is mounting.*/}
			{!state.isMounted
				? <div className='brewRenderer'>
					<div className='pages'>
						{renderDummyPage(1)}
					</div>
				</div>
				: null}

			<ErrorBar errors={props.errors} />
			<div className='popups' ref={mainRef}>
				<RenderWarnings />
				<NotificationPopup />
			</div>

			<ToolBar displayOptions={displayOptions} onDisplayOptionsChange={handleDisplayOptionsChange} visiblePages={state.visiblePages.length > 0 ? state.visiblePages : [state.centerPage]} totalPages={rawPages.length} headerState={headerState} setHeaderState={setHeaderState}/>

			{/*render in iFrame so broken code doesn't crash the site.*/}
			<Frame id='BrewRenderer'  title="Rendered Brew Content" initialContent={INITIAL_CONTENT}
				style={{ width: '100%', height: '100%', visibility: state.visibility }}
				contentDidMount={frameDidMount}
				onClick={()=>{emitClick();}}
				sandbox="allow-same-origin allow-modals allow-top-navigation"
			>
				<div className='brewRenderer'
					onKeyDown={handleControlKeys}
					tabIndex={-1}
				>

					{/* Apply CSS from Style tab and render pages from Markdown tab */}
					{state.isMounted
						&&
						<>
							{renderedStyle}
							<div className={`pages ${displayOptions.startOnRight ? 'recto' : 'verso'}	${displayOptions.spread}`} lang={`${props.lang || 'en'}`} style={pagesStyle} ref={pagesRef}>
								{renderedPages}
							</div>
						</>
					}
				</div>
				{headerState ? <HeaderNav ref={pagesRef} /> : <></>}
			</Frame>
		</>
	);
};

export default BrewRenderer;
