import * as i0 from '@angular/core';
import { input, output, inject, ElementRef, Renderer2, forwardRef, Directive, NgModule } from '@angular/core';
import { NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';

/**
 * @description Diretiva de máscara de entrada para campos de texto.
 *
 * Aplique `[sInputMask]` em qualquer `<input>` para aplicar uma máscara de formatação,
 * guiando a entrada do usuário com um padrão definido. Implementa ControlValueAccessor
 * e Validator para integração nativa com Angular Forms.
 *
 * **Slots de máscara:**
 * - `9` — dígito (0-9)
 * - `a` — letra (conforme `characterPattern`, padrão `[A-Za-z]`)
 * - `*` — alfanumérico (letra ou dígito)
 * - Outros caracteres são tratados como literais (ex: `.`, `-`, `/`, `(`, `)`)
 * - `?` — tudo após esse ponto na máscara é opcional
 *
 * @example
 * ```html
 * <!-- CPF -->
 * <input type="text" [sInputMask]="'999.999.999-99'" [(ngModel)]="cpf" />
 *
 * <!-- Telefone com unmask -->
 * <input type="text" [sInputMask]="'(99) 99999-9999'" [unmask]="true" [(ngModel)]="telefone" />
 * ```
 * @category Input
 */
class InputMaskDirective {
    /**
     * Padrão da máscara. Slots: `9` (dígito), `a` (letra), `*` (alfanumérico).
     * Demais caracteres são literais. `?` torna o restante da máscara opcional.
     */
    mask = input(undefined, { alias: 'sInputMask' });
    /**
     * Caractere exibido nas posições ainda não preenchidas.
     * @default '_'
     */
    slotChar = input('_');
    /**
     * Quando `true`, o modelo recebe apenas os caracteres digitados pelo usuário,
     * sem os literais da máscara. Quando `false`, o modelo recebe o valor completo
     * incluindo literais.
     * @default false
     */
    unmask = input(false);
    /**
     * Expressão regular usada para validar o slot `a` (letra).
     * @default '[A-Za-z]'
     */
    characterPattern = input('[A-Za-z]');
    /**
     * Quando `true`, limpa o campo ao perder o foco se a máscara estiver incompleta.
     * @default true
     */
    autoClear = input(true);
    /**
     * Emitido quando todos os slots obrigatórios da máscara estão preenchidos.
     * Emite o valor atual (mascarado ou não, conforme `unmask`).
     */
    maskComplete = output();
    el = inject((ElementRef));
    renderer = inject(Renderer2);
    tokens = [];
    buffer = [];
    wasComplete = false;
    lastValue = null;
    onChange = () => { };
    onTouched = () => { };
    ngOnInit() {
        this.buildMask();
    }
    ngOnChanges(changes) {
        if (changes['mask'] || changes['slotChar'] || changes['characterPattern']) {
            this.buildMask();
            if (this.mask()) {
                this.writeValue(this.lastValue);
            }
        }
    }
    buildMask() {
        const maskStr = this.mask();
        if (!maskStr) {
            this.tokens = [];
            this.buffer = [];
            return;
        }
        this.tokens = this.parseMask(maskStr);
        this.resetBuffer();
    }
    parseMask(mask) {
        const tokens = [];
        let optional = false;
        for (let i = 0; i < mask.length; i++) {
            const ch = mask[i];
            if (ch === '?') {
                optional = true;
                continue;
            }
            switch (ch) {
                case '9':
                    tokens.push({ type: 'digit', optional });
                    break;
                case 'a':
                    tokens.push({ type: 'alpha', optional });
                    break;
                case '*':
                    tokens.push({ type: 'alphanumeric', optional });
                    break;
                default:
                    tokens.push({ type: 'literal', char: ch, optional: false });
                    break;
            }
        }
        return tokens;
    }
    resetBuffer() {
        this.buffer = this.tokens.map((t) => (t.type === 'literal' ? t.char : this.slotChar()));
    }
    isValidChar(char, token) {
        switch (token.type) {
            case 'digit':
                return /[0-9]/.test(char);
            case 'alpha':
                return new RegExp(this.characterPattern()).test(char);
            case 'alphanumeric':
                return /[A-Za-z0-9]/.test(char);
            default:
                return false;
        }
    }
    isEditable(token) {
        return token.type !== 'literal';
    }
    nextEditablePos(from) {
        for (let i = from; i < this.tokens.length; i++) {
            if (this.isEditable(this.tokens[i])) {
                return i;
            }
        }
        return this.tokens.length;
    }
    prevEditablePos(from) {
        for (let i = from; i >= 0; i--) {
            if (this.tokens[i] && this.isEditable(this.tokens[i])) {
                return i;
            }
        }
        return -1;
    }
    setCursor(pos) {
        const safePos = Math.min(pos, this.tokens.length);
        this.el.nativeElement.setSelectionRange(safePos, safePos);
    }
    setInputValue(value) {
        this.renderer.setProperty(this.el.nativeElement, 'value', value);
    }
    getDisplayValue() {
        return this.buffer.join('');
    }
    getBlurDisplayValue() {
        let lastFilledIndex = -1;
        for (let i = 0; i < this.buffer.length; i++) {
            if (this.tokens[i] && this.tokens[i].type !== 'literal' && this.buffer[i] !== this.slotChar()) {
                lastFilledIndex = i;
            }
        }
        if (lastFilledIndex === -1)
            return '';
        return this.buffer.slice(0, lastFilledIndex + 1).join('');
    }
    /**
     * Returns the value that should be set on the model.
     * Returns null when the user has not typed anything.
     */
    getModelValue() {
        const hasInput = this.buffer.some((ch, i) => this.tokens[i] && this.tokens[i].type !== 'literal' && ch !== this.slotChar());
        if (!hasInput) {
            return null;
        }
        if (this.unmask()) {
            return (this.buffer
                .filter((ch, i) => this.tokens[i] && this.tokens[i].type !== 'literal' && ch !== this.slotChar())
                .join('') || null);
        }
        return this.buffer.join('');
    }
    clearRange(startPos, endPos) {
        for (let i = startPos; i < endPos; i++) {
            if (this.tokens[i] && this.tokens[i].type !== 'literal') {
                this.buffer[i] = this.slotChar();
            }
        }
        this.setInputValue(this.getDisplayValue());
    }
    isMaskComplete() {
        return this.tokens.every((token, i) => {
            if (token.type === 'literal' || token.optional) {
                return true;
            }
            return this.buffer[i] !== this.slotChar();
        });
    }
    checkAndEmitComplete() {
        const complete = this.isMaskComplete();
        if (complete && !this.wasComplete) {
            this.wasComplete = true;
            this.maskComplete.emit(this.getModelValue() ?? '');
        }
        else if (!complete) {
            this.wasComplete = false;
        }
    }
    dispatchInputEvent() {
        this.el.nativeElement.dispatchEvent(new Event('input', { bubbles: true }));
    }
    onKeyDown(event) {
        if (!this.mask() || this.el.nativeElement.readOnly || this.el.nativeElement.disabled) {
            return;
        }
        const key = event.key;
        // Let the browser handle shortcuts (Ctrl/Cmd), Tab, and function keys
        if (event.ctrlKey || event.metaKey || key === 'Tab' || key.startsWith('F')) {
            return;
        }
        // Let the browser handle arrow navigation and Home/End natively
        if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(key)) {
            return;
        }
        const input = this.el.nativeElement;
        const startPos = input.selectionStart ?? 0;
        const endPos = input.selectionEnd ?? 0;
        if (key === 'Backspace') {
            event.preventDefault();
            if (startPos !== endPos) {
                this.clearRange(startPos, endPos);
                const nextPos = this.nextEditablePos(startPos);
                this.setCursor(nextPos < this.tokens.length ? nextPos : startPos);
            }
            else if (startPos > 0) {
                const pos = this.prevEditablePos(startPos - 1);
                if (pos >= 0) {
                    this.buffer[pos] = this.slotChar();
                    this.setInputValue(this.getDisplayValue());
                    this.setCursor(pos);
                }
            }
            this.onChange(this.getModelValue());
            this.checkAndEmitComplete();
            this.dispatchInputEvent();
            return;
        }
        if (key === 'Delete') {
            event.preventDefault();
            if (startPos !== endPos) {
                this.clearRange(startPos, endPos);
                const nextPos = this.nextEditablePos(startPos);
                this.setCursor(nextPos < this.tokens.length ? nextPos : startPos);
            }
            else {
                const pos = this.nextEditablePos(startPos);
                if (pos < this.tokens.length) {
                    this.buffer[pos] = this.slotChar();
                    this.setInputValue(this.getDisplayValue());
                    this.setCursor(pos);
                }
            }
            this.onChange(this.getModelValue());
            this.checkAndEmitComplete();
            this.dispatchInputEvent();
            return;
        }
        // Single printable character
        if (key.length === 1) {
            event.preventDefault();
            let insertPos = startPos;
            // Clear selection range if any
            if (startPos !== endPos) {
                this.clearRange(startPos, endPos);
                insertPos = startPos;
            }
            const pos = this.nextEditablePos(insertPos);
            if (pos >= this.tokens.length) {
                return;
            }
            const token = this.tokens[pos];
            if (!this.isValidChar(key, token)) {
                return;
            }
            this.buffer[pos] = key;
            this.setInputValue(this.getDisplayValue());
            const next = this.nextEditablePos(pos + 1);
            this.setCursor(next <= this.tokens.length ? next : this.tokens.length);
            this.onChange(this.getModelValue());
            this.checkAndEmitComplete();
            this.dispatchInputEvent();
        }
    }
    onFocus(_event) {
        if (!this.mask()) {
            return;
        }
        // Always restore full mask display on focus (slot chars become visible)
        this.setInputValue(this.getDisplayValue());
        // Move cursor to the first empty editable slot
        const firstEmpty = this.buffer.findIndex((ch, i) => this.tokens[i] && this.tokens[i].type !== 'literal' && ch === this.slotChar());
        this.setCursor(firstEmpty >= 0 ? firstEmpty : this.nextEditablePos(0));
    }
    onBlur(_event) {
        if (!this.mask()) {
            return;
        }
        this.onTouched();
        if (!this.isMaskComplete()) {
            if (this.autoClear()) {
                this.resetBuffer();
                this.onChange(null);
            }
            this.setInputValue(this.getBlurDisplayValue());
        }
    }
    onPaste(event) {
        if (!this.mask() || this.el.nativeElement.readOnly || this.el.nativeElement.disabled) {
            return;
        }
        event.preventDefault();
        const pastedText = event.clipboardData?.getData('text') ?? '';
        const input = this.el.nativeElement;
        const startPos = input.selectionStart ?? 0;
        const endPos = input.selectionEnd ?? 0;
        if (startPos !== endPos) {
            this.clearRange(startPos, endPos);
        }
        let pos = this.nextEditablePos(startPos);
        for (const ch of pastedText) {
            if (pos >= this.tokens.length) {
                break;
            }
            const token = this.tokens[pos];
            if (token.type === 'literal') {
                // Skip matching literal in pasted text; skip the literal slot regardless
                if (ch === token.char) {
                    pos++;
                }
                pos = this.nextEditablePos(pos);
                continue;
            }
            if (this.isValidChar(ch, token)) {
                this.buffer[pos] = ch;
                pos = this.nextEditablePos(pos + 1);
            }
        }
        this.setInputValue(this.getDisplayValue());
        this.setCursor(pos <= this.tokens.length ? pos : this.tokens.length);
        this.onChange(this.getModelValue());
        this.checkAndEmitComplete();
        this.dispatchInputEvent();
    }
    onClick() {
        if (!this.mask() || this.el.nativeElement.readOnly || this.el.nativeElement.disabled) {
            return;
        }
        const input = this.el.nativeElement;
        const pos = input.selectionStart ?? 0;
        // If cursor landed on a literal, nudge it to the nearest editable slot
        if (this.tokens[pos] && this.tokens[pos].type === 'literal') {
            this.setCursor(this.nextEditablePos(pos));
        }
    }
    writeValue(value) {
        this.lastValue = value;
        if (!this.mask()) {
            return;
        }
        this.resetBuffer();
        if (!value) {
            this.setInputValue('');
            return;
        }
        let bufferIdx = 0;
        let valueIdx = 0;
        while (valueIdx < value.length && bufferIdx < this.tokens.length) {
            const token = this.tokens[bufferIdx];
            const char = value[valueIdx];
            if (token.type === 'literal') {
                // Consume the literal character from value if it matches; always advance buffer
                if (char === token.char)
                    valueIdx++;
                bufferIdx++;
            }
            else if (this.isValidChar(char, token)) {
                this.buffer[bufferIdx] = char;
                bufferIdx++;
                valueIdx++;
            }
            else {
                // Skip characters from value that do not match the current slot
                valueIdx++;
            }
        }
        this.setInputValue(this.getDisplayValue());
    }
    registerOnChange(onChange) {
        this.onChange = onChange;
    }
    registerOnTouched(onTouched) {
        this.onTouched = onTouched;
    }
    setDisabledState(isDisabled) {
        this.renderer.setProperty(this.el.nativeElement, 'disabled', isDisabled);
    }
    validate(control) {
        if (!this.mask() || !control.value) {
            return null;
        }
        if (!this.isMaskComplete()) {
            return { incompleteMask: { mask: this.mask() } };
        }
        return null;
    }
    static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: InputMaskDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
    static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "18.2.14", type: InputMaskDirective, isStandalone: true, selector: "input[sInputMask]", inputs: { mask: { classPropertyName: "mask", publicName: "sInputMask", isSignal: true, isRequired: false, transformFunction: null }, slotChar: { classPropertyName: "slotChar", publicName: "slotChar", isSignal: true, isRequired: false, transformFunction: null }, unmask: { classPropertyName: "unmask", publicName: "unmask", isSignal: true, isRequired: false, transformFunction: null }, characterPattern: { classPropertyName: "characterPattern", publicName: "characterPattern", isSignal: true, isRequired: false, transformFunction: null }, autoClear: { classPropertyName: "autoClear", publicName: "autoClear", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { maskComplete: "maskComplete" }, host: { listeners: { "keydown": "onKeyDown($event)", "focus": "onFocus($event)", "blur": "onBlur($event)", "paste": "onPaste($event)", "click": "onClick()" } }, providers: [
            {
                provide: NG_VALUE_ACCESSOR,
                useExisting: forwardRef(() => InputMaskDirective),
                multi: true,
            },
            {
                provide: NG_VALIDATORS,
                useExisting: forwardRef(() => InputMaskDirective),
                multi: true,
            },
        ], usesOnChanges: true, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: InputMaskDirective, decorators: [{
            type: Directive,
            args: [{
                    selector: 'input[sInputMask]',
                    standalone: true,
                    host: {
                        '(keydown)': 'onKeyDown($event)',
                        '(focus)': 'onFocus($event)',
                        '(blur)': 'onBlur($event)',
                        '(paste)': 'onPaste($event)',
                        '(click)': 'onClick()',
                    },
                    providers: [
                        {
                            provide: NG_VALUE_ACCESSOR,
                            useExisting: forwardRef(() => InputMaskDirective),
                            multi: true,
                        },
                        {
                            provide: NG_VALIDATORS,
                            useExisting: forwardRef(() => InputMaskDirective),
                            multi: true,
                        },
                    ],
                }]
        }] });

class InputMaskModule {
    static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: InputMaskModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
    static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.2.14", ngImport: i0, type: InputMaskModule, imports: [InputMaskDirective], exports: [InputMaskDirective] });
    static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: InputMaskModule });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: InputMaskModule, decorators: [{
            type: NgModule,
            args: [{
                    imports: [InputMaskDirective],
                    exports: [InputMaskDirective],
                }]
        }] });

/**
 * Generated bundle index. Do not edit.
 */

export { InputMaskDirective, InputMaskModule };
//# sourceMappingURL=seniorsistemas-angular-components-input-mask.mjs.map
