/**
 * WordPress dependencies
 */
import { useDispatch, useSelect } from '@safe-wordpress/data';
import { useCallback, useEffect } from '@safe-wordpress/element';
import { _x } from '@safe-wordpress/i18n';

/**
 * External dependencies
 */
import clsx from 'clsx';

import { CodeEditor } from '@nab/components';
import { store as NAB_DATA } from '@nab/data';
import { store as NAB_EDITOR } from '@nab/editor';
import {
	getValue,
	omitUndefineds,
	setValue as setValueInLocalStore,
} from '@nab/utils';
import type { AlternativeId, CssEditorState } from '@nab/types';

/**
 * Internal dependencies
 */
import { VisualEditor } from '../visual-editor';
import { ContentEditor } from '../content-editor';
import type { AlternativeAttributes } from '../../../../../../../../../packages/experiment-library/css/types';

export type BodyProps = {
	readonly alternativeId: AlternativeId;
};

export function Body( { alternativeId }: BodyProps ): JSX.Element {
	const activeSelector = useActiveSelector();
	const [ css, setCss ] = useCssValue( alternativeId );
	const [ editor, setEditor ] = useEditor(
		alternativeId,
		getPreferredEditor()
	);
	const markAsActive = useCssEditorActivation();
	const highlightSelectedItems = useSelectorHighlighter();

	return (
		<TabPanel
			className="nab-css-editor-sidebar__body"
			activeClass="nab-css-editor-sidebar__tab--active"
			orientation="horizontal"
			tabs={ TABS }
			activeTab={ editor }
			onSelect={ setEditor }
		>
			{ ( tab: Tab ): JSX.Element => {
				switch ( tab.name ) {
					case 'css-editor':
						return (
							<CodeEditor
								language="css"
								className="nab-css-editor-sidebar__css-editor"
								value={ css }
								focusedSelector={ activeSelector }
								onChange={ setCss }
								onChangeSelectedRule={ highlightSelectedItems }
								onFocus={ () => markAsActive( true ) }
								onBlur={ () => markAsActive( false ) }
							/>
						);

					case 'visual-editor':
						return <VisualEditor />;

					case 'content-editor':
						return <ContentEditor />;
				}
			} }
		</TabPanel>
	);
}

// ============
// HELPER VIEWS
// ============

type TabPanelProps = {
	readonly className: string;
	readonly activeClass: string;
	readonly orientation: 'horizontal';
	readonly onSelect: ( tab: EditorMode ) => void;
	readonly children: ( tab: Tab ) => JSX.Element;
	readonly activeTab: EditorMode;
	readonly tabs: Readonly< [ Tab, ...Tab[] ] >;
};

// NOTICE. This is a workaround until “TabPanel” can be controlled/uncontrolled.
const TabPanel = ( {
	className,
	activeClass,
	orientation,
	onSelect,
	children,
	activeTab,
	tabs,
}: TabPanelProps ) => (
	<div className={ className }>
		<div
			role="tablist"
			aria-orientation={ orientation }
			className="components-tab-panel__tabs"
		>
			{ tabs.map( ( tab ) => (
				<button
					key={ tab.name }
					type="button"
					role="tab"
					tabIndex={ tab.name === activeTab ? -1 : undefined }
					aria-selected="false"
					id={ `tab-panel-1-${ tab.name }` }
					aria-controls={ `tab-panel-1-${ tab.name }-view` }
					className={ clsx( {
						'components-button components-tab-panel__tabs-item':
							true,
						[ tab.className ]: true,
						[ activeClass ]: tab.name === activeTab,
					} ) }
					onClick={ () => onSelect( tab.name ) }
				>
					{ tab.title }
				</button>
			) ) }
		</div>
		<div
			aria-labelledby={ `tab-panel-1-${ activeTab || tabs[ 0 ].name }` }
			role="tabpanel"
			id={ `tab-panel-1-${ activeTab || tabs[ 0 ].name }-view` }
			className="components-tab-panel__tab-content"
		>
			{ children(
				tabs.find( ( t ) => t.name === activeTab ) || tabs[ 0 ]
			) }
		</div>
	</div>
);

// =====
// HOOKS
// =====

const useCssValue = ( alternativeId: AlternativeId ) => {
	const alternative = useSelect(
		( select ) =>
			select( NAB_EDITOR ).getAlternative< AlternativeAttributes >(
				alternativeId
			),
		[ alternativeId ]
	);
	const value = alternative?.attributes?.css || '';

	const { setAlternative } = useDispatch( NAB_EDITOR );
	const setValue = ( css: string ) => {
		if ( ! alternative ) {
			return;
		}
		void setAlternative( alternative.id, {
			...alternative,
			attributes: omitUndefineds( {
				...alternative.attributes,
				css,
			} ),
		} );
	};

	return [ value, setValue ] as const;
};

const useActiveSelector = () =>
	useSelect(
		( select ) =>
			select( NAB_DATA ).getPageAttribute( 'css-editor/cssEditorState' )
				?.activeSelector,
		[]
	);

function getPreferredEditor(): EditorMode {
	const value = getValue( 'cssPreferredEditor' );
	const isValid = ( v: unknown | EditorMode ): v is EditorMode =>
		TABS.some( ( t ) => t.name === v );
	return isValid( value ) ? value : 'visual-editor';
}

function useEditor(
	alternativeId: AlternativeId,
	defaultEditorMode: EditorMode
): [ EditorMode, ( editor: EditorMode ) => void ] {
	const editor = useSelect(
		( select ) =>
			select( NAB_DATA ).getPageAttribute( 'css-editor/cssEditorState' ),
		[]
	);

	const { setPageAttribute } = useDispatch( NAB_DATA );

	const setEditor = useCallback(
		( mode: EditorMode ): void => {
			setValueInLocalStore( 'cssPreferredEditor', mode );
			if ( mode === editor?.mode ) {
				return;
			}
			void setPageAttribute( 'css-editor/cssEditorState', {
				isEditorActive: false,
				areChangesActive: true,
				activeSelector: '',
				activeElement: undefined,
				contentValues: [],
				isPreviewInScope: false,
				...editor,
				alternativeId,
				mode,
				details: undefined,
			} );
		},
		[ alternativeId, editor, setPageAttribute ]
	);

	useEffect( () => {
		if ( editor ) {
			return;
		}
		setEditor( defaultEditorMode );
	}, [ editor, defaultEditorMode, setEditor ] );

	return [ editor?.mode ?? defaultEditorMode, setEditor ];
}

function useCssEditorActivation() {
	const editor = useSelect(
		( select ) =>
			select( NAB_DATA ).getPageAttribute( 'css-editor/cssEditorState' ),
		[]
	);
	const { setPageAttribute } = useDispatch( NAB_DATA );
	return ( isEditorActive: boolean ) =>
		void (
			'css-editor' === editor?.mode &&
			setPageAttribute( 'css-editor/cssEditorState', {
				...editor,
				isEditorActive,
			} )
		);
}

const useSelectorHighlighter = () => {
	const editor = useSelect(
		( select ) =>
			select( NAB_DATA ).getPageAttribute( 'css-editor/cssEditorState' ),
		[]
	);
	const { setPageAttribute } = useDispatch( NAB_DATA );

	if ( ! editor ) {
		return () => void null;
	}

	return ( selectors: ReadonlyArray< string > = [] ) => {
		const activeSelector = selectors.join( ', ' );
		if ( activeSelector === editor.activeSelector ) {
			return;
		}
		void setPageAttribute( 'css-editor/cssEditorState', {
			...editor,
			activeSelector,
			activeElement: undefined,
		} );
	};
};

// ====
// DATA
// ====

type EditorMode = CssEditorState[ 'mode' ];
type Tab = {
	readonly name: EditorMode;
	readonly title: string;
	readonly className: string;
};

const TABS: Readonly< [ Tab, Tab, Tab ] > = [
	{
		name: 'visual-editor',
		title: _x( 'Appearance', 'text', 'nelio-ab-testing' ),
		className: 'nab-css-editor-sidebar__tab',
	},
	{
		name: 'css-editor',
		title: _x( 'CSS', 'text', 'nelio-ab-testing' ),
		className: 'nab-css-editor-sidebar__tab',
	},
	{
		name: 'content-editor',
		title: _x( 'Content', 'text', 'nelio-ab-testing' ),
		className: 'nab-css-editor-sidebar__tab',
	},
];
