import { Prop, toNative } from 'vue-facing-decorator';
import { globalState } from '../../app/global-state';
import PowerduckState from '../../app/powerduck-state';
import TsxComponent, { Component } from '../../app/vuetsx';
import ScrollUtils from '../../common/scroll-utils';
import Tabs, { TabsRenderMode } from '../tabs/tabs';
import { ModalSectionMode, ModalSectionPillScrollMode } from './../../common/enums/modal';
import './css/modal-section-wrapper.css';

interface ModalSectionWrapperArgs {
	wrapInCard?: boolean;
}

@Component
class ModalSectionWrapperComponent extends TsxComponent<ModalSectionWrapperArgs> implements ModalSectionWrapperArgs {
	@Prop() wrapInCard!: boolean;

	// Scroll offset (per side-pill section index) remembered while the pill scroll
	// mode is `rememberPosition`, so returning to a section restores where we were.
	private _sectionScrollPositions: { [index: number]: number } = {};
	// Index of the side-pill section currently displayed. Tracked so we can save
	// the scroll offset of the section we are leaving before it is swapped out.
	private _activeSectionIndex: number = 0;

	resetValidationErrors() {
		const root = this.$el as HTMLElement;
		root.querySelectorAll('.modalsection-has-error').forEach(el => el.classList.remove('modalsection-has-error'));
		root.querySelectorAll('.fieldset-contains-error').forEach(el => el.classList.remove('fieldset-contains-error'));
	}

	displayValidationErrors() {
		this.resetValidationErrors();
		this.$nextTick(() => {
			this.$nextTick(() => {
				ScrollUtils.scrollToFirstPossibleError(this.$el as Element);
			});
		});
	}

	/**
	 * Switch the inner side-pill Tabs to the first sub-section that contains a
	 * validation error. Useful right after the consumer runs vuelidate's
	 * `$validate()` so the user lands on the sub-tab with the highlighted field
	 * instead of staying on whichever pill was active when they pressed Save.
	 *
	 * Strategy: query the DOM for the first invalid field across all currently
	 * mounted tab panes (in `navPills` mode all panes render, so this finds the
	 * truly-first invalid field anywhere in the form), walk up to its `.tab-pane`,
	 * compute the pane index among its siblings, and call `setActivePageIndex(idx)`
	 * on the internal Tabs instance.
	 *
	 * No-op in `fieldSet` mode (single flat page — nothing to navigate) and
	 * returns false when no invalid field is in the DOM.
	 */
	navigateToFirstError(): boolean {
		if (PowerduckState.getModalSectionMode() == ModalSectionMode.fieldSet) {
			return false;
		}

		const root = this.$el as HTMLElement | null;
		if (root == null) {
			return false;
		}

		// Project-style validation indicators (Bootstrap + vuelidate output):
		//   - `.has-danger` on the `.form-group` wrapper of an invalid field.
		//   - `.invalid-feedback` on the message `<div>` that vuelidate renders.
		//   - `.modalsection-has-error` applied to the section root by
		//     `displayValidationErrors()` above (for consumers that opt in).
		// `.is-invalid` / `.has-error` / `.form-group-error` are kept as
		// fallbacks for other consumers using stock Bootstrap or jQuery
		// validation conventions.
		const firstInvalid = root.querySelector('.has-danger, .invalid-feedback, .modalsection-has-error, .is-invalid, .has-error, .form-group-error') as HTMLElement | null;
		if (firstInvalid == null) {
			return false;
		}

		const tabPane = firstInvalid.closest('.tab-pane.inv-tab-wrap');
		if (tabPane == null) {
			return false;
		}

		const siblings = Array.from(tabPane.parentElement?.children ?? []).filter(c => c.classList.contains('tab-pane'));
		const idx = siblings.indexOf(tabPane);
		if (idx === -1) {
			return false;
		}

		const innerTabs = this.$refs.innerTabs as typeof Tabs.prototype | undefined;
		if (innerTabs?.setActivePageIndex == null) {
			return false;
		}

		innerTabs.setActivePageIndex(idx);
		return true;
	}

	/**
	 * Reacts to a side-pill section change by scrolling the section content
	 * according to `PowerduckState.getModalSectionPillScrollMode()`:
	 *   - `disabled`         → leave the scroll position alone.
	 *   - `scrollToTop`      → always jump back to the top.
	 *   - `rememberPosition` → save the offset of the section we are leaving and
	 *                          restore the target section's saved offset, falling
	 *                          back to the top when it has not been visited yet.
	 *
	 * The save has to happen synchronously (the section being left is still in the
	 * DOM here, before Vue re-renders); the restore is deferred until the newly
	 * selected section has rendered.
	 */
	private onSectionPillChange(newIndex: number): void {
		const mode = PowerduckState.getModalSectionPillScrollMode();
		const previousIndex = this._activeSectionIndex;
		this._activeSectionIndex = newIndex;

		if (mode == ModalSectionPillScrollMode.disabled) {
			return;
		}

		if (mode == ModalSectionPillScrollMode.rememberPosition) {
			this._sectionScrollPositions[previousIndex] = this.readScrollTop(this.getScrollContainer());
		}

		this.$nextTick(() => {
			const apply = () => this.applySectionScroll(newIndex, mode);
			if (typeof requestAnimationFrame === 'function') {
				requestAnimationFrame(apply);
			} else {
				apply();
			}
		});
	}

	private applySectionScroll(index: number, mode: ModalSectionPillScrollMode): void {
		const remembered = this._sectionScrollPositions[index];
		const target = (mode == ModalSectionPillScrollMode.rememberPosition && remembered != null) ? remembered : 0;
		ScrollUtils.scrollToPos(
			target,
			this.getScrollContainer(),
			false,
			true,
		);
	}

	/**
	 * Nearest scrollable ancestor of the section content. Resolves to the `.modal`
	 * element on desktop and to `.modal-body` in the mobile bottom-sheet layout;
	 * returns null when nothing scrolls (page-level usage → window).
	 */
	private getScrollContainer(): HTMLElement | null {
		let parent = (this.$el as HTMLElement | null)?.parentElement ?? null;
		while (parent != null) {
			const overflowY = getComputedStyle(parent).overflowY;
			if (overflowY === 'auto' || overflowY === 'scroll') {
				return parent;
			}

			parent = parent.parentElement;
		}

		return null;
	}

	private readScrollTop(container: HTMLElement | null): number {
		if (container != null) {
			return container.scrollTop;
		}

		return globalState.scrollY ?? globalState.pageYOffset ?? 0;
	}

	render(h) {
		if (PowerduckState.getModalSectionMode() == ModalSectionMode.fieldSet) {
			return <div>{this.$slots.default?.()}</div>;
		} else {
			return (
				<Tabs
					ref="innerTabs"
					wrapInCard={this.wrapInCard}
					renderMode={TabsRenderMode.SidePillsExtended}
					renderHiddenTabs={PowerduckState.getModalSectionMode() == ModalSectionMode.navPills}
					onTabChange={(index: number) => this.onSectionPillChange(index)}
				>
					{this.$slots.default?.()}
				</Tabs>
			);
		}
	}
}

const ModalSectionWrapper = toNative(ModalSectionWrapperComponent);
export default ModalSectionWrapper;
