/*!
 * V4Fire Client Core
 * https://github.com/V4Fire/Client
 *
 * Released under the MIT license
 * https://github.com/V4Fire/Client/blob/master/LICENSE
 */

/**
 * [[include:super/i-input-text/README.md]]
 * @packageDocumentation
 */

import iWidth from 'traits/i-width/i-width';
import iSize from 'traits/i-size/i-size';

import iInput, {

	component,
	prop,
	system,
	computed,
	wait,

	ModsDecl,
	UnsafeGetter

} from 'super/i-input/i-input';

//#if runtime has iInputText/mask
import * as mask from 'super/i-input-text/modules/mask';
//#endif

import { $$ } from 'super/i-input-text/const';
import type { CompiledMask, SyncMaskWithTextOptions, UnsafeIInputText } from 'super/i-input-text/interface';

export * from 'super/i-input/i-input';
export * from 'super/i-input-text/const';
export * from 'super/i-input-text/interface';

export * from 'super/i-input-text/modules/validators';
export { default as TextValidators } from 'super/i-input-text/modules/validators';

export { $$ };

/**
 * Superclass to create text inputs
 */
@component()
export default class iInputText extends iInput implements iWidth, iSize {
	/**
	 * Initial text value of the input
	 */
	@prop({type: String, required: false})
	readonly textProp?: string;

	/**
	 * UI type of the input
	 * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input#input_types
	 */
	@prop(String)
	readonly type: string = 'text';

	/**
	 * Autocomplete mode of the input
	 * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input#htmlattrdefautocomplete
	 */
	@prop(String)
	readonly autocomplete: string = 'off';

	/**
	 * Placeholder text of the input
	 * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input#htmlattrdefplaceholder
	 */
	@prop({type: String, required: false})
	readonly placeholder?: string;

	/**
	 * The minimum text value length of the input.
	 * The option will be ignored if provided `mask`.
	 *
	 * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input#htmlattrdefminlength
	 */
	@prop({type: Number, required: false})
	readonly minLength?: number;

	/**
	 * The maximum text value length of the input.
	 * The option will be ignored if provided `mask`.
	 *
	 * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input#htmlattrdefmaxlength
	 */
	@prop({type: Number, required: false})
	readonly maxLength?: number;

	/**
	 * A value of the input's mask.
	 *
	 * The mask is used when you need to "decorate" some input value,
	 * like a phone number or credit card number. The mask can contain terminal and non-terminal symbols.
	 * The terminal symbols will be shown as they are written.
	 * The non-terminal symbols should start with `%` and one more symbol. For instance, `%d` means that it can be
	 * replaced by a numeric character (0-9).
	 *
	 * Supported non-terminal symbols:
	 *
	 * `%d` - is equivalent RegExp' `\d`
	 * `%w` - is equivalent RegExp' `\w`
	 * `%s` - is equivalent RegExp' `\s`
	 *
	 * @example
	 * ```
	 * < b-input :mask = '+%d% (%d%d%d) %d%d%d-%d%d-%d%d'
	 * ```
	 */
	@prop({type: String, required: false})
	readonly mask?: string;

	/**
	 * A value of the mask placeholder.
	 * All non-terminal symbols from the mask without the specified value will have this placeholder.
	 *
	 * @example
	 * ```
	 * /// A user will see an input element with a value:
	 * /// +_ (___) ___-__-__
	 * /// When it starts typing, the value will be automatically changed, like,
	 * /// +7 (49_) ___-__-__
	 * < b-input :mask = '+%d% (%d%d%d) %d%d%d-%d%d-%d%d' | :maskPlaceholder = '_'
	 * ```
	 */
	@prop({
		type: String,
		validator: (val: string) => [...val.letters()].length === 1,
		watch: {
			handler: 'initMask',
			immediate: true,
			provideArgs: false
		}
	})

	readonly maskPlaceholder: string = '_';

	/**
	 * Number of mask repetitions.
	 * This parameter allows you to specify how many times the mask pattern needs to apply to the input value.
	 * The `true` value means that the pattern can be repeated infinitely.
	 *
	 * @example
	 * ```
	 * /// A user will see an input element with a value:
	 * /// _-_
	 * /// When it starts typing, the value will be automatically changed, like,
	 * /// 2-3 1-_
	 * < b-input :mask = '%d-%d' | :maskRepetitions = 2
	 * ```
	 */
	@prop({type: [Number, Boolean], required: false})
	readonly maskRepetitionsProp?: number | boolean;

	/**
	 * A delimiter for a mask value. This parameter is used when you are using the `maskRepetitions` prop.
	 * Every next chunk of the mask will have the delimiter as a prefix.
	 *
	 * @example
	 * ```
	 * /// A user will see an input element with a value:
	 * /// _-_
	 * /// When it starts typing, the value will be automatically changed, like,
	 * /// 2-3@1-_
	 * < b-input :mask = '%d-%d' | :maskRepetitions = 2 | :maskDelimiter = '@'
	 * ```
	 */
	@prop(String)
	readonly maskDelimiter: string = ' ';

	/**
	 * A dictionary with RegExp-s as values.
	 * Keys of the dictionary are interpreted as non-terminal symbols for the component mask, i.e.,
	 * you can add new non-terminal symbols.
	 *
	 * @example
	 * ```
	 * < b-input :mask = '%l%l%l' | :regExps = {l: /[a-z]/i}
	 * ```
	 */
	@prop({type: Object, required: false})
	readonly regExps?: Dictionary<RegExp>;

	override get unsafe(): UnsafeGetter<UnsafeIInputText<this>> {
		return Object.cast(this);
	}

	/**
	 * Text value of the input
	 * @see [[iInputText.textStore]]
	 */
	@computed({cache: false})
	get text(): string {
		const
			v = this.field.get<string>('textStore') ?? '';

		// If the input is empty, don't return the empty mask
		if (this.compiledMask?.placeholder === v) {
			return '';
		}

		return v;
	}

	/**
	 * Sets a new text value of the input
	 * @param value
	 */
	set text(value: string) {
		if (this.mask != null) {
			void this.syncMaskWithText(value);
			return;
		}

		this.updateTextStore(value);
	}

	/**
	 * True, if the mask is repeated infinitely
	 */
	get isMaskInfinite(): boolean {
		return this.maskRepetitionsProp === true;
	}

	static override readonly mods: ModsDecl = {
		...iWidth.mods,
		...iSize.mods,

		empty: [
			'true',
			'false'
		],

		readonly: [
			'true',
			['false']
		]
	};

	/**
	 * Text value store of the input
	 * @see [[iInputText.textProp]]
	 */
	@system((o) => o.sync.link((v) => v ?? ''))
	protected textStore!: string;

	/**
	 * Object of the compiled mask
	 * @see [[iInputText.mask]]
	 */
	@system()
	protected compiledMask?: CompiledMask;

	/**
	 * Number of mask repetitions
	 * @see [[iInputText.maskRepetitionsProp]]
	 */
	@system()
	protected maskRepetitions: number = 1;

	protected override readonly $refs!: {input: HTMLInputElement};

	/**
	 * Selects all content of the input
	 * @emits `selectText()`
	 */
	@wait('ready', {label: $$.selectText})
	selectText(): CanPromise<boolean> {
		const
			{input} = this.$refs;

		if (input.selectionStart !== 0 || input.selectionEnd !== input.value.length) {
			input.select();
			this.emit('selectText');
			return true;
		}

		return false;
	}

	/**
	 * Clears content of the input
	 * @emits `clearText()`
	 */
	@wait('ready', {label: $$.clearText})
	clearText(): CanPromise<boolean> {
		if (this.text === '') {
			return false;
		}

		if (this.mask != null) {
			void this.syncMaskWithText('');

		} else {
			this.text = '';
		}

		this.emit('clearText');
		return true;
	}

	/**
	 * Initializes the component mask
	 */
	@wait('ready', {label: $$.initMask})
	protected initMask(): CanPromise<void> {
		return mask.init(this);
	}

	/**
	 * Compiles the component mask.
	 * The method saves the compiled mask object and other properties within the component.
	 */
	protected compileMask(): CanUndef<CompiledMask> {
		if (this.mask == null) {
			return;
		}

		this.compiledMask = mask.compile(this, this.mask);
		return this.compiledMask;
	}

	/**
	 * Synchronizes the component mask with the specified text value
	 *
	 * @param [text] - text to synchronize or a list of Unicode symbols
	 * @param [opts] - additional options
	 */
	@wait('ready', {label: $$.syncComponentMaskWithText})
	protected syncMaskWithText(
		text: CanArray<string> = this.text,
		opts?: SyncMaskWithTextOptions
	): CanPromise<void> {
		mask.syncWithText(this, text, opts);
	}

	/**
	 * Updates the component text store with the provided value
	 * @param value
	 */
	protected updateTextStore(value: string): void {
		const
			{input} = this.$refs;

		// Force to set a value to the input
		if (Object.isTruly(input)) {
			input.value = value;
		}

		this.field.set('textStore', value);
	}

	protected override normalizeAttrs(attrs: Dictionary = {}): Dictionary {
		attrs = {
			...attrs,
			type: this.type,

			placeholder: this.placeholder,
			autocomplete: this.autocomplete,
			readonly: Object.parse(this.mods.readonly),

			minlength: this.minLength,
			maxlength: this.maxLength
		};

		return attrs;
	}

	protected override initModEvents(): void {
		super.initModEvents();
		this.sync.mod('empty', 'text', (v) => v === '');
	}

	protected mounted(): void {
		this.updateTextStore(this.text);
	}

	/**
	 * Handler: the input with a mask has lost the focus
	 */
	protected onMaskBlur(): boolean {
		return mask.syncInputWithField(this);
	}

	/**
	 * Handler: value of the masked input has been changed and can be saved
	 */
	protected onMaskValueReady(): boolean {
		return mask.saveSnapshot(this);
	}

	/**
	 * Handler: there is occurred an input action on the masked input
	 */
	protected onMaskInput(): Promise<boolean> {
		return mask.syncFieldWithInput(this);
	}

	/**
	 * Handler: there is occurred a keypress action on the masked input
	 * @param e
	 */
	protected onMaskKeyPress(e: KeyboardEvent): boolean {
		return mask.onKeyPress(this, e);
	}

	/**
	 * Handler: removing characters from the mask via `backspace/delete` buttons
	 * @param e
	 */
	protected onMaskDelete(e: KeyboardEvent): boolean {
		return mask.onDelete(this, e);
	}

	/**
	 * Handler: "navigation" over the mask via "arrow" buttons or click events
	 * @param e
	 */
	protected onMaskNavigate(e: KeyboardEvent | MouseEvent): boolean {
		return mask.onNavigate(this, e);
	}
}
