import {__, _x} from '@wordpress/i18n'
import {upperFirst} from 'lodash'
import cx from 'classnames'

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

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

import {
	buildClassNames,
} from '../classNames'

import {
	flattenSelectors,
} from '../../supports/selectors'

import type {
	SkaBlocks,
	TailwindFeatureAttribute,
} from '../../types'

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

export interface TailwindFeatureOption {
	key: string
	type?: 'default' | 'toggle' | 'color' | 'font-size'
	label?: string
	className: string
}

export interface TailwindFeatureSchemaItem {
	key: string
	label?: string
	options: TailwindFeatureOption[]
}

type CSSProperty = string

/** Configuration for the `TailwindFeature` class. */
export interface TailwindFeatureConfig {
	id: string
	label: string
	groupId?: string
	/** Use `Tailwind4.designSystem.theme.keysInNamespaces(['--key'])` options. */
	themeKeys?: string[]
	/** Static settings. */
	settings?: Record<string, string>
	/** Feature supports. */
	supports?: {
		/** Can the feature use variants like `hover:` (default `true`). */
		variants?: boolean
		/** Can the value be important (default `true`). */
		important?: boolean
		/** Can the value be negative. */
		signed?: boolean
		/** Can the feature use arbitrary values like `m-[123px]` (default `true`). */
		arbitrary?: boolean
		/** Can the feature use variable values like `m-(--var)` (default `true`). */
		variable?: boolean
		/** Should arbitrary value produce a name like `group/name` not `group-[name]`. */
		name?: boolean
		/** Can the feature use bare values like `m-123` (using a CSS property produces an arbitrary rule e.g. `font-style` => `[font-style:${bareValue}]`). */
		bare?: boolean | CSSProperty
	}
	schema: TailwindFeatureSchemaItem[]
	// /** Function to render inspector controls contents. */
	// edit?: React.FC<EditProps>
	/** Custom function to generate feature's classes. */
	buildClassNames?: (value: TailwindFeatureAttribute, feature: TailwindFeature) => string
	/** Can be used to determine which feature it is based on value. */
	dataType?:
		| 'color'
		| 'length'
		| 'percentage'
		| 'ratio'
		| 'number'
		| 'integer'
		| 'url'
		| 'position'
		| 'bg-size'
		| 'line-width'
		| 'image'
		| 'family-name'
		| 'generic-name'
		| 'absolute-size'
		| 'relative-size'
		| 'angle'
		| 'vector',
	/** Can be used to specify which rule to generate. */
	hint?:
		| 'weight'
}

const builtOptions = new Map<string, Map<string, string | Map<string, string>>>()

export default class TailwindFeature {

	readonly skaBlocks
	readonly config
	public id

	constructor(skaBlocks: SkaBlocks, config: TailwindFeatureConfig) {
		this.skaBlocks = skaBlocks
		this.config = config
		this.id = `skaBlocks${upperFirst(this.featureId)}`
		this.register()
	}

	get featureId(): string {
		return this.config.id
	}

	get label(): string {
		return this.config.label
	}

	/** Settings from theme keys. */
	get themeKeys() {
		return this.config.themeKeys || []
	}

	/** Static settings. */
	get settings(): Record<string, string> {
		return this.config.settings || {}
	}

	get supports() {
		return {
			variants: true,
			important: true,
			signed: false,
			arbitrary: true,
			variable: true,
			name: false,
			bare: false,
			...this.config.supports,
		}
	}

	get schema(): TailwindFeatureConfig['schema'] {
		return this.config.schema
	}

	get dataType(): TailwindFeatureConfig['dataType'] {
		return this.config.dataType
	}

	get hint(): TailwindFeatureConfig['hint'] {
		return this.config.hint
	}

	hasSupport(block: tBlockNameOrType): boolean {
		return hasBlockSupport(block, 'customClassName', true)
	}

	/** Do the given block attributes contain settings for this feature. */
	hasValue = (attributes: tBlockAttributes = {}, includingSelectors = true): boolean => {

		const hasValue = typeof attributes[this.id] !== 'undefined'

		if(hasValue) {
			return true
		}

		if(!includingSelectors) {
			return false
		}

		const hasSelectors = !!attributes['skaBlocksSelectors']

		if(!hasSelectors) {
			return false
		}

		const flatSelectors = flattenSelectors(attributes['skaBlocksSelectors'])

		const selectorsHaveValue = Object.keys(flatSelectors).some(key => typeof flatSelectors[key][this.id] !== 'undefined')

		return selectorsHaveValue
	}

	getDefaultOptions = (resolveValues: boolean = true) => {

		const id = `${this.id}-${this.skaBlocks.compiler.t}-${this.skaBlocks.compiler.themeKey}-${resolveValues ? '1' : '0'}`
		if(builtOptions.has(id)) {
			return builtOptions.get(id)!
		}

		const map = new Map<string, string | Map<string, string>>()

		const ds = this.skaBlocks.compiler.designSystem
		if(!ds) {
			// Failed loading design system, don't crash.
			return map
		}

		/** Add static settings from feature configuration. */
		Object.entries(this.settings).forEach(([key, value]) => {

			/**
			 * Resolve the "DEFAULT" value further, e.g. when `--radius: var(--radius-sm);` is
			 * in the Tailwind config then Border radius "DEFAULT" value is `var(--radius)` but
			 * this block resolves it to `var(--radius-sm)` which is more informative.
			 */
			if(resolveValues && key === 'DEFAULT' && value.indexOf('var(--') === 0) {
				const themeKey = this.themeKeys.find(themeKey => value === `var(${themeKey})`)
				if(themeKey) {
					const themeValue = ds.resolveThemeValue(themeKey)
					if(themeValue) {
						map.set(key, themeValue)
						return
					}
				}
			}

			map.set(key, value)
		})

		/** Add options from theme keys. */
		this.themeKeys.forEach(themeKey => this.skaBlocks.compiler.getThemeKey(themeKey, resolveValues).forEach((v, k) => map.set(k, v)))

		builtOptions.set(id, map)

		return map
	}

	getOptions = (arbitraryValues: string[] = [], resolveValues: boolean = true) => {

		if(arbitraryValues.length > 0) {

			const map = new Map(this.getDefaultOptions(resolveValues))

			/** Append arbitrary values. */
			arbitraryValues.forEach(value => {
				let cleanValue = value
				if(cleanValue.indexOf('[') === 0) {
					cleanValue = cleanValue.substring(1)
				}
				if(cleanValue.lastIndexOf(']') === cleanValue.length - 1) {
					cleanValue = cleanValue.substring(0, cleanValue.length - 1)
				}
				map.set(value, cleanValue)
			})

			return map
		}

		return this.getDefaultOptions(resolveValues)
	}

	/** Adds this feature's class names. */
	getTailwindClasses = (className: string = '', attributes: tBlockAttributes = {}): string => {

		if(!this.hasValue(attributes)) {
			return className
		}

		const buildClassesFn = this.config.buildClassNames || buildClassNames

		return cx(
			/** Existing class names. */
			className,
			/** Computed class names of this feature. */
			buildClassesFn(attributes[this.id], this),
			/** Additional computed class names (used by selectors). */
			applyFilters('ska.blocks.getTailwindClasses', '', this, attributes, buildClassesFn) as string
		)
	}

	/** Adds this feature's class names excluding selectors class names. */
	getRootTailwindClasses = (className: string = '', attributes: tBlockAttributes = {}): string => {

		if(!this.hasValue(attributes, false)) {
			return className
		}

		const {
			skaBlocksSelectors: _skaBlocksSelectors = {},
			skaBlocksOptions: _skaBlocksOptions = {},
			...rootAttributes
		} = attributes

		return this.getTailwindClasses(className, rootAttributes)
	}

	registerAttributes(blockType: tBlockType): tBlockType {

		if(!this.hasSupport(blockType)) {
			return blockType
		}

		return {
			...blockType,
			attributes: {
				...blockType.attributes,
				[this.id]: {
					type: 'object',
				},
			},
		}
	}

	register() {
		/** Register attributes. */
		addFilter('blocks.registerBlockType', `ska-blocks/${this.id}/attributes`, this.registerAttributes.bind(this))
		/** Apply Tailwind classNames of this feature. */
		addFilter('ska.blocks.tailwindClassNames', `ska-blocks/${this.id}/tailwindClassNames`, this.getTailwindClasses)
	}
}
