import { InjectionToken, signal, inject, DestroyRef, effect, untracked, computed, booleanAttribute } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Subject } from 'rxjs';
import { startWith } from 'rxjs/operators';

const SD_FORM_CONFIGURATION = new InjectionToken('sd.form.configuration');

class SdFormControl extends FormControl {
    sdChanges = new Subject();
    untouchChanges = new Subject();
    touchChanges = new Subject();
    pristineChanges = new Subject();
    constructor(formState, validatorOrOpts, asyncValidator) {
        super(formState, validatorOrOpts, asyncValidator);
    }
    markAsUntouched(opts) {
        super.markAsUntouched(opts);
        this.untouchChanges.next(true);
        this.sdChanges.next(true);
    }
    markAsTouched(opts) {
        super.markAsTouched(opts);
        this.touchChanges.next(true);
        this.sdChanges.next(true);
    }
    markAsPristine(opts) {
        super.markAsPristine(opts);
        this.pristineChanges.next(true);
        this.sdChanges.next(true);
    }
}

/**
 * Inline-error sentinel validator. Returns `{ inlineError: true }` so the form
 * template can render `<mat-error>{{ inlineError }}</mat-error>` whenever the
 * host component has a non-empty `[inlineError]` input. The error message
 * itself is read from the input — this validator only flags the state.
 *
 * why: each form component (input, textarea, select, checkbox, radio, switch,
 * date, datetime, input-number, autocomplete) previously declared the same
 * private `customInlineErrorValidator()` method. Centralized here so adding
 * another form component does not silently re-introduce the duplicate.
 */
const SdInlineErrorValidator = () => ({ inlineError: true });
const HandleSdCustomValidator = (func) => {
    return async (c) => {
        const value = c.value === 0 ? c.value : c.value || null;
        if (func && typeof func === 'function') {
            const result = func(value);
            if (result instanceof Promise) {
                const message = await result;
                if (message) {
                    return {
                        customValidator: message,
                    };
                }
                return null;
            }
            if (result) {
                return {
                    customValidator: result,
                };
            }
            return null;
        }
        return null;
    };
};

/**
 * Wrap an AbstractControl Signal into a reactive snapshot signal.
 *
 * Re-emits on every value, status, touched, or dirty change so
 * downstream consumers can derive `data-disabled`, `data-value`,
 * `data-empty`, and `data-invalid` host-binding attributes from a
 * single, lazily evaluated signal.
 *
 * "invalid" is intentionally gated on `touched || dirty` so validation
 * errors are not surfaced until the user has interacted with the field.
 *
 * Must be called inside an Angular injection context (constructor,
 * field initialiser, or `runInInjectionContext`).
 */
function sdFormControlState(control) {
    // A tick counter incremented on every control event (value, status,
    // touched, dirty). Written only from the effect below — never inside
    // a computed() — satisfying Angular's no-side-effect-in-reactive rule.
    const tick = signal(0, ...(ngDevMode ? [{ debugName: "tick" }] : []));
    const destroyRef = inject(DestroyRef);
    // Holds the active RxJS subscription so it can be torn down when the
    // control instance changes or the host component is destroyed.
    let subscription = null;
    // An effect re-runs whenever the control Signal changes, tears down the
    // old subscription and creates a fresh one for the incoming control.
    // `AbstractControl.events` (Angular 14+) covers value, status, touched,
    // and dirty changes — a superset of valueChanges + statusChanges.
    const effectRef = effect(() => {
        const c = control(); // tracked — effect re-runs on change
        // effect() runs asynchronously; the old subscription is torn down on the next
        // scheduled run, not synchronously on signal change. A one-tick window of
        // double-subscription is harmless due to computed() memoization.
        subscription?.unsubscribe();
        subscription = null;
        if (c) {
            subscription = c.events.pipe(startWith(null)).subscribe(() => {
                // Increment outside reactive context to avoid Angular's
                // "signal written from computed / template" error.
                untracked(() => tick.update(n => n + 1));
            });
        }
    }, ...(ngDevMode ? [{ debugName: "effectRef" }] : []));
    // Clean up effect and subscription when host component is destroyed.
    destroyRef.onDestroy(() => {
        effectRef.destroy();
        subscription?.unsubscribe();
    });
    return computed(() => {
        const _control = control();
        tick(); // reactive dependency — recomputes on every control event
        if (!_control) {
            return { value: undefined, disabled: false, invalid: false, touched: false };
        }
        return {
            value: _control.value,
            disabled: _control.disabled,
            invalid: _control.invalid && (_control.touched || _control.dirty),
            touched: _control.touched,
        };
    });
}

/**
 * `viewed` input transform. Keeps `booleanAttribute` coercion so a bare attribute
 * (`<sd-select viewed>`) still resolves to `true`, but intercepts the literal
 * `'inline'` first — `booleanAttribute('inline')` would otherwise coerce it to `true`.
 */
function sdViewedTransform(v) {
    return v === 'inline' ? 'inline' : booleanAttribute(v);
}
/**
 * Compose the tri-state `viewed` semantics into a control. `open` opens the control's
 * native picker (mat-select panel / mat-calendar / overlay). In `'inline'` mode the editor
 * is always rendered (chrome hidden via CSS), so `open()` can fire immediately on click —
 * no render-swap, the view text never disappears.
 *
 * @param viewed   the control's `viewed` input signal.
 * @param open     opens the control's picker; called by `enterInlineEdit`.
 * @param disabled the control's disabled state. why: a disabled `'inline'` field must behave
 *   like `viewed=true` (static, NOT click-to-edit) — you can't edit a disabled control.
 */
function sdViewedInline(viewed, open, disabled) {
    // why: disabled biến 'inline' thành static view (isInline=false, isViewed=true) — không cho sửa.
    const isInline = computed(() => viewed() === 'inline' && !disabled?.(), ...(ngDevMode ? [{ debugName: "isInline" }] : []));
    const isViewed = computed(() => viewed() === true || (viewed() === 'inline' && !!disabled?.()), ...(ngDevMode ? [{ debugName: "isViewed" }] : []));
    const enterInlineEdit = () => {
        if (isInline())
            open?.();
    };
    return { isInline, isViewed, enterInlineEdit };
}

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

export { HandleSdCustomValidator, SD_FORM_CONFIGURATION, SdFormControl, SdInlineErrorValidator, sdFormControlState, sdViewedInline, sdViewedTransform };
//# sourceMappingURL=sdcorejs-angular-forms-models.mjs.map
