import * as React from 'react'
import {__, _x} from '@wordpress/i18n'
import cx from 'classnames'

import {
	useReducer,
} from '@wordpress/element'

import {
	Button,
	Modal,
	__experimentalHStack as HStack,
} from '@wordpress/components'

import {
	hasBlockSupport,
	getBlockSupport,
	// @ts-expect-error
} from '@wordpress/blocks'

import {
	addFilter,
	applyFilters,
} from '@wordpress/hooks'

import {
	InspectorControls,
	// @ts-expect-error
} from '@wordpress/block-editor'

import {
	fullscreen,
} from '@wordpress/icons'

import {
	useDispatch,
} from '@wordpress/data'

import {
	createHigherOrderComponent,
	useCopyToClipboard,
} from '@wordpress/compose'

import {
	store as noticesStore,
} from '@wordpress/notices'

import {
	CopyIcon,
} from '@ska/components'

import {
	SkaPanelBody,
} from '@ska/plugin'

import {
	ARRAY_UNIQUE,
	htmlAttributeToReactAttribute,
	styleToReactObject,
} from '@ska/utils'

import {
	RecordEditor,
} from '../components'

import {
	useAs,
} from './as'

import {
	useHasPermissions,
} from '../plugins/permissions'

import type {
	tBlockAttributes,
	tBlockEditProps,
	tBlockNameOrType,
	tBlockSaveProps,
	tBlockType,
} from '@ska/shared'

export interface AttributesValue {
	record?: Record<string, string>
}

export interface AttributesAttributes {
	skaBlocksAttributes?: AttributesValue
}

export const ATTRIBUTES_SUPPORT_KEY = 'skaBlocksAttributes'
export const ATTRIBUTES_SUPPORT_DEFAULT_ENABLED = false
export const hasAttributesSupport = (block: tBlockNameOrType) => hasBlockSupport(block, ATTRIBUTES_SUPPORT_KEY, ATTRIBUTES_SUPPORT_DEFAULT_ENABLED)

/**
 * Default value can be provided in block `supports` attribute.
 *
 * @example
 * ```json
 * 	"supports": {
 * 		"skaBlocksAttributes": {
 * 			"aria-label": "Text"
 * 		}
 * 	}
 * ```
 */
const getDefaultValue = (block: tBlockType) => {

	const support = getBlockSupport(block, ATTRIBUTES_SUPPORT_KEY, ATTRIBUTES_SUPPORT_DEFAULT_ENABLED)

	return {
		...(typeof support === 'object' && {
			record: {
				...support,
			},
		}),
	}
}

const withCustomAttributes = (settings: tBlockType) => {

	if(!hasAttributesSupport(settings)) {
		return settings
	}

	if(!settings.attributes.skaBlocksAttributes) {

		const defaultValue = getDefaultValue(settings)

		Object.assign(settings.attributes, {
			skaBlocksAttributes: {
				type: 'object',
				...(Object.keys(defaultValue).length > 0 && {
					default: defaultValue,
				}),
			},
		})
	}

	return settings
}

export const CLIPBOARD_PREFIX = 'ska-blocks-attributes:'

const AttributesInspectorControls: React.FC<tBlockEditProps<AttributesAttributes>> = (props) => {

	const [isFullscreen, toggleFullscreen] = useReducer((isActive) => !isActive, false)

	const {
		attributes,
		setAttributes,
	} = props

	const {
		skaBlocksAttributes = {},
	} = attributes

	const {
		record = {},
	} = skaBlocksAttributes

	const {
		createSuccessNotice,
		createErrorNotice,
	} = useDispatch(noticesStore)

	const copyRef = useCopyToClipboard(
		() => `${CLIPBOARD_PREFIX}${JSON.stringify(skaBlocksAttributes)}`,
		() => createSuccessNotice(__('Copied attributes to clipboard.', 'ska-blocks'), {type: 'snackbar'})
	)

	const applyPastedAttributes = (text: string) => {

		const invalidError = () => createErrorNotice(__('Invalid attributes.', 'ska-blocks'), {type: 'snackbar'})

		if(text.indexOf(CLIPBOARD_PREFIX) !== 0) {
			invalidError()
			return
		}

		let attributes: AttributesValue = {}
		try {
			attributes = JSON.parse(text.replace(CLIPBOARD_PREFIX, ''))
		} catch(e) {
			invalidError()
			return
		}

		const {
			record = {},
			...rest
		} = attributes

		if(!Object.keys(record).length) {
			invalidError()
			return
		}

		setAttributes({
			skaBlocksAttributes: {
				record: {
					...skaBlocksAttributes.record,
					...record,
				},
				...rest,
			},
		})

		createSuccessNotice(__('Pasted attributes.', 'ska-blocks'), {type: 'snackbar'})
	}

	const onPaste = (e: React.ClipboardEvent<HTMLInputElement>) => {

		const text = e?.clipboardData?.getData('Text')

		if(!text || text.indexOf(CLIPBOARD_PREFIX) !== 0) {
			return
		}

		e.preventDefault()

		applyPastedAttributes(text)
	}

	if(!useHasPermissions()) {
		return null
	}

	return <>
		<InspectorControls>
			<SkaPanelBody
				title={__('Attributes', 'ska-blocks')}
				initialOpen={Object.keys(record).length > 0}
				skaIcon
			>
				<RecordEditor
					className='ska-blocks--use-code-inputs'
					footer={(
						<HStack alignment='start'>
							<p style={{lineHeight: 1, margin: 0}}>{__('Custom HTML attributes for the block root element.', 'ska-blocks')}</p>
							<Button
								size='small'
								icon={<CopyIcon style={{transform: 'scale(0.65)', opacity: '0.5'}} />}
								aria-label={__('Copy attributes', 'ska-blocks')}
								ref={copyRef}
							/>
							<Button
								size='small'
								icon={fullscreen}
								aria-label={__('Edit attributes in a modal', 'ska-blocks')}
								onClick={toggleFullscreen}
							/>
						</HStack>
					)}
					value={record}
					onChange={record => setAttributes({skaBlocksAttributes: {...skaBlocksAttributes, record}})}
					onPaste={onPaste}
				/>
			</SkaPanelBody>
		</InspectorControls>
		{isFullscreen && (
			<Modal
				title={__('Attributes', 'ska-blocks')}
				onRequestClose={toggleFullscreen}
				style={{width: 1280, maxWidth: '96vw'}}
			>
				<RecordEditor
					className='ska-blocks--use-code-inputs ska-blocks--record-editor--wide'
					help={__('Custom HTML attributes for the block root element.', 'ska-blocks')}
					value={record}
					onChange={record => setAttributes({skaBlocksAttributes: {...skaBlocksAttributes, record}})}
				/>
			</Modal>
		)}
	</>
}

const withAttributesControls = createHigherOrderComponent(
	(BlockEdit: any) => (props: tBlockEditProps) => {
		if(hasAttributesSupport(props.name)) {
			return <>
				<AttributesInspectorControls {...props as any as tBlockEditProps<AttributesAttributes>} />
				<BlockEdit {...props} />
			</>
		}
		return <BlockEdit {...props} />
	},
	'withSkaBlocksAttributes'
)

const EVENT_PREVENT_DEFAULT = (e: React.SyntheticEvent) => e?.preventDefault()

export const useAttributes = (props: tBlockEditProps<AttributesAttributes>): Record<string, string | object | boolean> => {

	const {
		attributes = {},
	} = props

	const {
		skaBlocksAttributes = {},
	} = attributes

	const {
		record = {},
	} = skaBlocksAttributes

	const [Element] = useAs(props) // Element is `div` when the current block doesn't use `as`.

	/** Modify attributes for block editor compatibility. */
	return Object.keys(record).reduce((acc, cur) => {
		/** React-compatible attribute name. */
		const key = htmlAttributeToReactAttribute(cur)
		switch(cur) {
			case '': break
			case 'style':
				/** React-compatible styles. */
				acc[key] = styleToReactObject(record[cur])
			break
			case 'defaultChecked':
			case 'checked':
				acc[key] = record[cur] ? record[cur] : true
			break
			default:
				acc[key] = record[cur]
		}
		return acc
	}, {
		/** Prevent submitting forms in editor. */
		...(Element === 'form' && {onSubmit: EVENT_PREVENT_DEFAULT}),
	} as Record<string, string | object | boolean>)
}

useAttributes.save = (props: tBlockSaveProps<AttributesAttributes>): Record<string, string> => {

	const {
		attributes = {},
	} = props

	const {
		skaBlocksAttributes = {},
	} = attributes

	const {
		record = {},
	} = skaBlocksAttributes

	return Object.keys(record).reduce((acc, cur) => {
		const attributeName = cur.trim()
		/** Don't add attribute with no name. */
		if(attributeName !== '') {
			acc[attributeName] = record[cur]
		}
		return acc
	}, {} as Record<string, string>)
}

/**
 * Get class names used in custom attributes.
 * These are compiled and included in CSS but not applied to the block.
 * Mostly detects AlpineJS classes but can be extended.
 */
export const getAttributesClassNames = (attributes: AttributesValue): string => {

	const {
		record = {},
	} = attributes

	const keys = Object.keys(record)
	if(!keys.length) {
		return ''
	}

	return keys.reduce((acc, key) => {

		const value = record[key]
		if(!value) {
			return acc
		}

		if(key.indexOf('x-transition:') === 0) {
			return acc.concat(value)
		}

		return applyFilters('ska.blocks.getAttributesClassNames', acc, {key, value}, attributes) as string[]
	}, [] as string[]).join(' ').split(' ').map(v => v.trim()).filter(v => v).filter(ARRAY_UNIQUE).join(' ')
}

const withAttributesTailwindClassNames = (classNames: string, attributes: tBlockAttributes) => {

	if(attributes?.skaBlocksAttributes) {
		const attributeClassNames = getAttributesClassNames(attributes.skaBlocksAttributes)
		if(attributeClassNames) {
			return cx(classNames, attributeClassNames)
		}
	}

	return classNames
}

export default () => {
	addFilter('blocks.registerBlockType', 'ska-blocks/with-attributes-attribute', withCustomAttributes)
	addFilter('editor.BlockEdit', 'ska-blocks/with-attributes-controls', withAttributesControls, 500)
	addFilter('ska.blocks.additionalTailwindClassNames', `ska-blocks/with-background-tailwind-classnames`, withAttributesTailwindClassNames)
}
