import { NgIf, NgClass, NgFor, NgTemplateOutlet } from '@angular/common';
import {
    Component,
    ElementRef,
    EventEmitter,
    HostBinding,
    Input,
    OnDestroy,
    OnInit,
    Output,
    ViewChild,
    ContentChild,
    Inject,
    AfterViewInit,
    Injector,
    PipeTransform,
    ChangeDetectorRef,
    LOCALE_ID, Optional, ContentChildren, QueryList, HostListener
} from '@angular/core';
import {
    ControlValueAccessor,
    NG_VALUE_ACCESSOR,
    NgControl,
    AbstractControl,
    ValidationErrors,
    Validator,
    NG_VALIDATORS
} from '@angular/forms';

import { IgxInputGroupComponent } from '../input-group/input-group.component';
import { IgxInputDirective, IgxInputState } from '../directives/input/input.directive';
import { IgxInputGroupType, IGX_INPUT_GROUP_TYPE } from '../input-group/public_api';
import { DisplayDensityToken, IDisplayDensityOptions } from '../core/density';
import {
    IgxItemListDirective,
    IgxTimeItemDirective
} from './time-picker.directives';
import { Subscription, noop, fromEvent } from 'rxjs';
import { IgxTimePickerBase, IGX_TIME_PICKER_COMPONENT } from './time-picker.common';
import { AbsoluteScrollStrategy } from '../services/overlay/scroll';
import { AutoPositionStrategy } from '../services/overlay/position';
import { OverlaySettings } from '../services/overlay/utilities';
import { takeUntil } from 'rxjs/operators';
import { IgxButtonDirective } from '../directives/button/button.directive';

import { IgxDateTimeEditorDirective } from '../directives/date-time-editor/date-time-editor.directive';
import { IgxToggleDirective } from '../directives/toggle/toggle.directive';
import { ITimePickerResourceStrings } from '../core/i18n/time-picker-resources';
import { CurrentResourceStrings } from '../core/i18n/resources';
import { IBaseEventArgs, isEqual, isDate, PlatformUtil, IBaseCancelableBrowserEventArgs } from '../core/utils';
import { PickerInteractionMode } from '../date-common/types';
import { IgxTextSelectionDirective } from '../directives/text-selection/text-selection.directive';
import { IgxLabelDirective } from '../directives/label/label.directive';
import { PickerBaseDirective } from '../date-common/picker-base.directive';
import { DateTimeUtil } from '../date-common/util/date-time.util';
import { DatePart, DatePartDeltas } from '../directives/date-time-editor/public_api';
import { PickerHeaderOrientation } from '../date-common/types';
import { IgxPickerActionsDirective, IgxPickerClearComponent } from '../date-common/picker-icons.common';
import { TimeFormatPipe, TimeItemPipe } from './time-picker.pipes';
import { IgxSuffixDirective } from '../directives/suffix/suffix.directive';
import { IgxIconComponent } from '../icon/icon.component';
import { IgxPrefixDirective } from '../directives/prefix/prefix.directive';

let NEXT_ID = 0;
export interface IgxTimePickerValidationFailedEventArgs extends IBaseEventArgs {
    previousValue: Date | string;
    currentValue: Date | string;
}

@Component({
    providers: [
        {
            provide: NG_VALUE_ACCESSOR,
            useExisting: IgxTimePickerComponent,
            multi: true
        },
        {
            provide: IGX_TIME_PICKER_COMPONENT,
            useExisting: IgxTimePickerComponent
        },
        {
            provide: NG_VALIDATORS,
            useExisting: IgxTimePickerComponent,
            multi: true
        }
    ],
    selector: 'igx-time-picker',
    templateUrl: 'time-picker.component.html',
    styles: [
        `:host {
            display: block;
        }`
    ],
    standalone: true,
    imports: [IgxInputGroupComponent, IgxInputDirective, IgxDateTimeEditorDirective, IgxTextSelectionDirective, NgIf, IgxPrefixDirective, IgxIconComponent, IgxSuffixDirective, IgxButtonDirective, IgxToggleDirective, NgClass, IgxItemListDirective, NgFor, IgxTimeItemDirective, NgTemplateOutlet, TimeFormatPipe, TimeItemPipe]
})
export class IgxTimePickerComponent extends PickerBaseDirective
    implements
    IgxTimePickerBase,
    ControlValueAccessor,
    OnInit,
    OnDestroy,
    AfterViewInit,
    Validator {
    /**
     * An @Input property that sets the value of the `id` attribute.
     * ```html
     * <igx-time-picker [id]="'igx-time-picker-5'" [displayFormat]="h:mm tt" ></igx-time-picker>
     * ```
     */
    @HostBinding('attr.id')
    @Input()
    public id = `igx-time-picker-${NEXT_ID++}`;

    /**
     * The format used when editable input is not focused. Defaults to the `inputFormat` if not set.
     *
     * @remarks
     * Uses Angular's `DatePipe`.
     *
     * @example
     * ```html
     * <igx-time-picker displayFormat="mm:ss"></igx-time-picker>
     * ```
     *
     */
    @Input()
    public override displayFormat: string;

    /**
     * The expected user input format and placeholder.
     *
     * @remarks
     * Default is `hh:mm tt`
     *
     * @example
     * ```html
     * <igx-time-picker inputFormat="HH:mm"></igx-time-picker>
     * ```
     */
    @Input()
    public override inputFormat: string = DateTimeUtil.DEFAULT_TIME_INPUT_FORMAT;

    /**
     * Gets/Sets the interaction mode - dialog or drop down.
     *
     * @example
     * ```html
     * <igx-time-picker mode="dialog"></igx-time-picker>
     * ```
     */
    @Input()
    public override mode: PickerInteractionMode = PickerInteractionMode.DropDown;

    /**
     * The minimum value the picker will accept.
     *
     * @remarks
     * If a `string` value is passed in, it must be in ISO format.
     *
     * @example
     * ```html
     * <igx-time-picker [minValue]="18:00:00"></igx-time-picker>
     * ```
     */
    @Input()
    public set minValue(value: Date | string) {
        this._minValue = value;
        const date = this.parseToDate(value);
        if (date) {
            this._dateMinValue = new Date();
            this._dateMinValue.setHours(date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds());
            this.minDropdownValue = this.setMinMaxDropdownValue('min', this._dateMinValue);
        }
        this.setSelectedValue(this._selectedDate);
        this._onValidatorChange();
    }

    public get minValue(): Date | string {
        return this._minValue;
    }

    /**
     * Gets if the dropdown/dialog is collapsed
     *
     * ```typescript
     * let isCollapsed = this.timePicker.collapsed;
     * ```
     */
    public override get collapsed(): boolean {
        return this.toggleRef?.collapsed;
    }

    /**
     * The maximum value the picker will accept.
     *
     * @remarks
     * If a `string` value is passed in, it must be in ISO format.
     *
     * @example
     * ```html
     * <igx-time-picker [maxValue]="20:30:00"></igx-time-picker>
     * ```
     */
    @Input()
    public set maxValue(value: Date | string) {
        this._maxValue = value;
        const date = this.parseToDate(value);
        if (date) {
            this._dateMaxValue = new Date();
            this._dateMaxValue.setHours(date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds());
            this.maxDropdownValue = this.setMinMaxDropdownValue('max', this._dateMaxValue);
        }
        this.setSelectedValue(this._selectedDate);
        this._onValidatorChange();
    }

    public get maxValue(): Date | string {
        return this._maxValue;
    }

    /**
     * An @Input property that determines the spin behavior. By default `spinLoop` is set to true.
     * The seconds, minutes and hour spinning will wrap around by default.
     * ```html
     * <igx-time-picker [spinLoop]="false"></igx-time-picker>
     * ```
     */
    @Input()
    public spinLoop = true;

    /**
     * Gets/Sets a custom formatter function on the selected or passed date.
     *
     * @example
     * ```html
     * <igx-time-picker [value]="date" [formatter]="formatter"></igx-time-picker>
     * ```
     */
    @Input()
    public formatter: (val: Date) => string;

    /**
     * Sets the orientation of the picker's header.
     *
     * @remarks
     * Available in dialog mode only. Default value is `horizontal`.
     *
     * ```html
     * <igx-time-picker [headerOrientation]="'vertical'"></igx-time-picker>
     * ```
     */
    @Input()
    public headerOrientation: PickerHeaderOrientation = PickerHeaderOrientation.Horizontal;

    /** @hidden @internal */
    @Input()
    public readOnly = false;

    /**
     * Emitted after a selection has been done.
     *
     * @example
     * ```html
     * <igx-time-picker (selected)="onSelection($event)"></igx-time-picker>
     * ```
     */
    @Output()
    public selected = new EventEmitter<Date>();

    /**
     * Emitted when the picker's value changes.
     *
     * @remarks
     * Used for `two-way` bindings.
     *
     * @example
     * ```html
     * <igx-time-picker [(value)]="date"></igx-time-picker>
     * ```
     */
    @Output()
    public valueChange = new EventEmitter<Date | string>();

    /**
     * Emitted when the user types/spins invalid time in the time-picker editor.
     *
     *  @example
     * ```html
     * <igx-time-picker (validationFailed)="onValidationFailed($event)"></igx-time-picker>
     * ```
     */
    @Output()
    public validationFailed = new EventEmitter<IgxTimePickerValidationFailedEventArgs>();

    /** @hidden */
    @ViewChild('hourList')
    public hourList: ElementRef;

    /** @hidden */
    @ViewChild('minuteList')
    public minuteList: ElementRef;

    /** @hidden */
    @ViewChild('secondsList')
    public secondsList: ElementRef;

    /** @hidden */
    @ViewChild('ampmList')
    public ampmList: ElementRef;

    /** @hidden @internal */
    @ContentChildren(IgxPickerClearComponent)
    public clearComponents: QueryList<IgxPickerClearComponent>;

    /** @hidden @internal */
    @ContentChild(IgxLabelDirective)
    public label: IgxLabelDirective;

    /** @hidden @internal */
    @ContentChild(IgxPickerActionsDirective)
    public timePickerActionsDirective: IgxPickerActionsDirective;

    @ViewChild(IgxInputDirective, { read: IgxInputDirective })
    private inputDirective: IgxInputDirective;

    @ViewChild(IgxInputGroupComponent)
    private _inputGroup: IgxInputGroupComponent;

    @ViewChild(IgxDateTimeEditorDirective, { static: true })
    private dateTimeEditor: IgxDateTimeEditorDirective;

    @ViewChild(IgxToggleDirective)
    private toggleRef: IgxToggleDirective;

    /** @hidden */
    public cleared = false;

    /** @hidden */
    public isNotEmpty = false;

    /** @hidden */
    public currentHour: number;

    /** @hidden */
    public currentMinutes: number;

    /** @hidden */
    public get showClearButton(): boolean {
        if (this.clearComponents.length) {
            return false;
        }
        if (DateTimeUtil.isValidDate(this.value)) {
            // TODO: Update w/ clear behavior
            return this.value.getHours() !== 0 || this.value.getMinutes() !== 0 || this.value.getSeconds() !== 0;
        }
        return !!this.dateTimeEditor.value;
    }

    /** @hidden */
    public get showHoursList(): boolean {
        return this.inputFormat.indexOf('h') !== - 1 || this.inputFormat.indexOf('H') !== - 1;
    }

    /** @hidden */
    public get showMinutesList(): boolean {
        return this.inputFormat.indexOf('m') !== - 1;
    }

    /** @hidden */
    public get showSecondsList(): boolean {
        return this.inputFormat.indexOf('s') !== - 1;
    }

    /** @hidden */
    public get showAmPmList(): boolean {
        return this.inputFormat.indexOf('t') !== - 1 || this.inputFormat.indexOf('a') !== - 1;
    }

    /** @hidden */
    public get isTwelveHourFormat(): boolean {
        return this.inputFormat.indexOf('h') !== - 1;
    }

    /** @hidden @internal */
    public get isVertical(): boolean {
        return this.headerOrientation === PickerHeaderOrientation.Vertical;
    }

    /** @hidden @internal */
    public get selectedDate(): Date {
        return this._selectedDate;
    }

    /** @hidden @internal */
    public get minDateValue(): Date {
        if (!this._dateMinValue) {
            const minDate = new Date();
            minDate.setHours(0, 0, 0, 0);
            return minDate;
        }

        return this._dateMinValue;
    }

    /** @hidden @internal */
    public get maxDateValue(): Date {
        if (!this._dateMaxValue) {
            const maxDate = new Date();
            maxDate.setHours(23, 59, 59, 999);
            return maxDate;
        }

        return this._dateMaxValue;
    }

    private get required(): boolean {
        if (this._ngControl && this._ngControl.control && this._ngControl.control.validator) {
            // Run the validation with empty object to check if required is enabled.
            const error = this._ngControl.control.validator({} as AbstractControl);
            return !!(error && error.required);
        }

        return false;
    }

    private get dialogOverlaySettings(): OverlaySettings {
        return Object.assign({}, this._defaultDialogOverlaySettings, this.overlaySettings);
    }

    private get dropDownOverlaySettings(): OverlaySettings {
        return Object.assign({}, this._defaultDropDownOverlaySettings, this.overlaySettings);
    }

    /** @hidden @internal */
    public displayValue: PipeTransform = { transform: (date: Date) => this.formatter(date) };
    /** @hidden @internal */
    public minDropdownValue: Date;
    /** @hidden @internal */
    public maxDropdownValue: Date;
    /** @hidden @internal */
    public hourItems = [];
    /** @hidden @internal */
    public minuteItems = [];
    /** @hidden @internal */
    public secondsItems = [];
    /** @hidden @internal */
    public ampmItems = [];

    private _value: Date | string;
    private _dateValue: Date;
    private _dateMinValue: Date;
    private _dateMaxValue: Date;
    private _selectedDate: Date;
    private _resourceStrings = CurrentResourceStrings.TimePickerResStrings;
    private _okButtonLabel = null;
    private _cancelButtonLabel = null;
    private _itemsDelta: Pick<DatePartDeltas, 'hours' | 'minutes' | 'seconds'> = { hours: 1, minutes: 1, seconds: 1 };

    private _statusChanges$: Subscription;
    private _ngControl: NgControl = null;
    private _onChangeCallback: (_: Date | string) => void = noop;
    private _onTouchedCallback: () => void = noop;
    private _onValidatorChange: () => void = noop;

    private _defaultDialogOverlaySettings: OverlaySettings = {
        closeOnOutsideClick: true,
        modal: true,
        closeOnEscape: true,
        outlet: this.outlet
    };
    private _defaultDropDownOverlaySettings: OverlaySettings = {
        target: this.element.nativeElement,
        modal: false,
        closeOnOutsideClick: true,
        scrollStrategy: new AbsoluteScrollStrategy(),
        positionStrategy: new AutoPositionStrategy(),
        outlet: this.outlet
    };


    /**
     * The currently selected value / time from the drop-down/dialog
     *
     * @remarks
     * The current value is of type `Date`
     *
     * @example
     * ```typescript
     * const newValue: Date = new Date(2000, 2, 2, 10, 15, 15);
     * this.timePicker.value = newValue;
     * ```
     */
    public get value(): Date | string {
        return this._value;
    }

    /**
     * An accessor that allows you to set a time using the `value` input.
     * ```html
     * public date: Date = new Date(Date.now());
     *  //...
     * <igx-time-picker [value]="date" format="h:mm tt"></igx-time-picker>
     * ```
     */
    @Input()
    public set value(value: Date | string) {
        const oldValue = this._value;
        this._value = value;
        const date = this.parseToDate(value);
        if (date) {
            this._dateValue = new Date();
            this._dateValue.setHours(date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds());
            this.setSelectedValue(this._dateValue);
        } else {
            this._dateValue = null;
            this.setSelectedValue(null);
        }
        if (this.dateTimeEditor) {
            this.dateTimeEditor.value = date;
        }
        this.emitValueChange(oldValue, this._value);
        this._onChangeCallback(this._value);
    }

    /**
     * An accessor that sets the resource strings.
     * By default it uses EN resources.
     */
    @Input()
    public set resourceStrings(value: ITimePickerResourceStrings) {
        this._resourceStrings = Object.assign({}, this._resourceStrings, value);
    }

    /**
     * An accessor that returns the resource strings.
     */
    public get resourceStrings(): ITimePickerResourceStrings {
        return this._resourceStrings;
    }

    /**
     * An @Input property that renders OK button with custom text. By default `okButtonLabel` is set to OK.
     * ```html
     * <igx-time-picker okButtonLabel='SET' [value]="date" format="h:mm tt"></igx-time-picker>
     * ```
     */
    @Input()
    public set okButtonLabel(value: string) {
        this._okButtonLabel = value;
    }

    /**
     * An accessor that returns the label of ok button.
     */
    public get okButtonLabel(): string {
        if (this._okButtonLabel === null) {
            return this.resourceStrings.igx_time_picker_ok;
        }
        return this._okButtonLabel;
    }

    /**
     * An @Input property that renders cancel button with custom text.
     * By default `cancelButtonLabel` is set to Cancel.
     * ```html
     * <igx-time-picker cancelButtonLabel='Exit' [value]="date" format="h:mm tt"></igx-time-picker>
     * ```
     */
    @Input()
    public set cancelButtonLabel(value: string) {
        this._cancelButtonLabel = value;
    }

    /**
     * An accessor that returns the label of cancel button.
     */
    public get cancelButtonLabel(): string {
        if (this._cancelButtonLabel === null) {
            return this.resourceStrings.igx_time_picker_cancel;
        }
        return this._cancelButtonLabel;
    }

    /**
     * Delta values used to increment or decrement each editor date part on spin actions and
     * to display time portions in the dropdown/dialog.
     * By default `itemsDelta` is set to `{hour: 1, minute: 1, second: 1}`
     * ```html
     * <igx-time-picker [itemsDelta]="{hour:3, minute:5, second:10}" id="time-picker"></igx-time-picker>
     * ```
     */
    @Input()
    public set itemsDelta(value: Pick<DatePartDeltas, 'hours' | 'minutes' | 'seconds'>) {
        Object.assign(this._itemsDelta, value);
    }

    public get itemsDelta(): Pick<DatePartDeltas, 'hours' | 'minutes' | 'seconds'> {
        return this._itemsDelta;
    }

    constructor(
        element: ElementRef,
        @Inject(LOCALE_ID) _localeId: string,
        @Optional() @Inject(DisplayDensityToken) _displayDensityOptions: IDisplayDensityOptions,
        @Optional() @Inject(IGX_INPUT_GROUP_TYPE) _inputGroupType: IgxInputGroupType,
        private _injector: Injector,
        private platform: PlatformUtil,
        private cdr: ChangeDetectorRef) {
        super(element, _localeId, _displayDensityOptions, _inputGroupType);
        this.locale = this.locale || this._localeId;
    }

    /** @hidden @internal */
    @HostListener('keydown', ['$event'])
    public onKeyDown(event: KeyboardEvent): void {
        switch (event.key) {
            case this.platform.KEYMAP.ARROW_UP:
                if (event.altKey && this.isDropdown) {
                    this.close();
                }
                break;
            case this.platform.KEYMAP.ARROW_DOWN:
                if (event.altKey && this.isDropdown) {
                    this.open();
                }
                break;
            case this.platform.KEYMAP.ESCAPE:
                this.cancelButtonClick();
                break;
            case this.platform.KEYMAP.SPACE:
                this.open();
                event.preventDefault();
                break;
        }
    }

    /** @hidden @internal */
    public getPartValue(value: Date, type: string): string {
        const inputDateParts = DateTimeUtil.parseDateTimeFormat(this.inputFormat);
        const part = inputDateParts.find(element => element.type === type);
        return DateTimeUtil.getPartValue(value, part, part.format.length);
    }

    /** @hidden @internal */
    public toISOString(value: Date): string {
        return value.toLocaleTimeString('en-GB', {
            hour: '2-digit',
            minute: '2-digit',
            second: '2-digit',
        });
    }

    // #region ControlValueAccessor

    /** @hidden @internal */
    public writeValue(value: Date | string) {
        this._value = value;
        const date = this.parseToDate(value);
        if (date) {
            this._dateValue = new Date();
            this._dateValue.setHours(date.getHours(), date.getMinutes(), date.getSeconds());
            this.setSelectedValue(this._dateValue);
        } else {
            this.setSelectedValue(null);
        }
        if (this.dateTimeEditor) {
            this.dateTimeEditor.value = date;
        }
    }

    /** @hidden @internal */
    public registerOnChange(fn: (_: Date | string) => void) {
        this._onChangeCallback = fn;
    }

    /** @hidden @internal */
    public registerOnTouched(fn: () => void) {
        this._onTouchedCallback = fn;
    }

    /** @hidden @internal */
    public registerOnValidatorChange(fn: any) {
        this._onValidatorChange = fn;
    }

    /** @hidden @internal */
    public validate(control: AbstractControl): ValidationErrors | null {
        if (!control.value) {
            return null;
        }
        // InvalidDate handling
        if (isDate(control.value) && !DateTimeUtil.isValidDate(control.value)) {
            return { value: true };
        }

        const errors = {};
        const value = DateTimeUtil.isValidDate(control.value) ? control.value : DateTimeUtil.parseIsoDate(control.value);
        Object.assign(errors, DateTimeUtil.validateMinMax(value, this.minValue, this.maxValue, true, false));
        return Object.keys(errors).length > 0 ? errors : null;
    }

    /** @hidden @internal */
    public setDisabledState(isDisabled: boolean): void {
        this.disabled = isDisabled;
    }
    //#endregion

    /** @hidden */
    public override ngOnInit(): void {
        this._ngControl = this._injector.get<NgControl>(NgControl, null);
        this.minDropdownValue = this.setMinMaxDropdownValue('min', this.minDateValue);
        this.maxDropdownValue = this.setMinMaxDropdownValue('max', this.maxDateValue);
        this.setSelectedValue(this._dateValue);
        super.ngOnInit();
    }

    /** @hidden */
    public override ngAfterViewInit(): void {
        super.ngAfterViewInit();
        this.subscribeToDateEditorEvents();
        this.subscribeToToggleDirectiveEvents();

        this._defaultDropDownOverlaySettings.excludeFromOutsideClick = [this._inputGroup.element.nativeElement];

        fromEvent(this.inputDirective.nativeElement, 'blur')
            .pipe(takeUntil(this._destroy$))
            .subscribe(() => {
                if (this.collapsed) {
                    this.updateValidityOnBlur();
                }
            });

        this.subToIconsClicked(this.clearComponents, () => this.clear());
        this.clearComponents.changes.pipe(takeUntil(this._destroy$))
            .subscribe(() => this.subToIconsClicked(this.clearComponents, () => this.clear()));

        if (this._ngControl) {
            this._statusChanges$ = this._ngControl.statusChanges.subscribe(this.onStatusChanged.bind(this));
            this._inputGroup.isRequired = this.required;
            this.cdr.detectChanges();
        }
    }

    /** @hidden */
    public override ngOnDestroy(): void {
        super.ngOnDestroy();
        if (this._statusChanges$) {
            this._statusChanges$.unsubscribe();
        }
    }

    /** @hidden */
    public getEditElement(): HTMLInputElement {
        return this.dateTimeEditor.nativeElement;
    }

    /**
     * Opens the picker's dialog UI.
     *
     * @param settings OverlaySettings - the overlay settings to use for positioning the drop down or dialog container according to
     * ```html
     * <igx-time-picker #picker [value]="date"></igx-time-picker>
     * <button (click)="picker.open()">Open Dialog</button>
     * ```
     */
    public open(settings?: OverlaySettings): void {
        if (this.disabled || !this.toggleRef.collapsed) {
            return;
        }

        this.setSelectedValue(this._dateValue);
        const overlaySettings = Object.assign({}, this.isDropdown
            ? this.dropDownOverlaySettings
            : this.dialogOverlaySettings
            , settings);

        this.toggleRef.open(overlaySettings);
    }

    /**
     * Closes the dropdown/dialog.
     * ```html
     * <igx-time-picker #timePicker></igx-time-picker>
     * ```
     * ```typescript
     * @ViewChild('timePicker', { read: IgxTimePickerComponent }) picker: IgxTimePickerComponent;
     * picker.close();
     * ```
     */
    public close(): void {
        this.toggleRef.close();
    }

    public toggle(settings?: OverlaySettings): void {
        if (this.toggleRef.collapsed) {
            this.open(settings);
        } else {
            this.close();
        }
    }

    /**
     * Clears the time picker value if it is a `string` or resets the time to `00:00:00` if the value is a Date object.
     *
     * @example
     * ```typescript
     * this.timePicker.clear();
     * ```
     */
    public clear(): void {
        if (this.disabled) {
            return;
        }

        if (!this.toggleRef.collapsed) {
            this.close();
        }

        if (DateTimeUtil.isValidDate(this.value)) {
            const oldValue = new Date(this.value);
            this.value.setHours(0, 0, 0);
            if (this.value.getTime() !== oldValue.getTime()) {
                this.emitValueChange(oldValue, this.value);
                this._dateValue.setHours(0, 0, 0);
                this.dateTimeEditor.value = new Date(this.value);
                this.setSelectedValue(this._dateValue);
            }
        } else {
            this.value = null;
        }
    }

    /**
     * Selects time from the igxTimePicker.
     *
     * @example
     * ```typescript
     * this.timePicker.select(date);
     *
     * @param date Date object containing the time to be selected.
     */
    public select(date: Date | string): void {
        this.value = date;
    }

    /**
     * Increment a specified `DatePart`.
     *
     * @param datePart The optional DatePart to increment. Defaults to Hour.
     * @param delta The optional delta to increment by. Overrides `itemsDelta`.
     * @example
     * ```typescript
     * this.timePicker.increment(DatePart.Hours);
     * ```
     */
    public increment(datePart?: DatePart, delta?: number): void {
        this.dateTimeEditor.increment(datePart, delta);
    }

    /**
     * Decrement a specified `DatePart`
     *
     * @param datePart The optional DatePart to decrement. Defaults to Hour.
     * @param delta The optional delta to decrement by. Overrides `itemsDelta`.
     * @example
     * ```typescript
     * this.timePicker.decrement(DatePart.Seconds);
     * ```
     */
    public decrement(datePart?: DatePart, delta?: number): void {
        this.dateTimeEditor.decrement(datePart, delta);
    }

    /** @hidden @internal */
    public cancelButtonClick(): void {
        this.setSelectedValue(this._dateValue);
        this.dateTimeEditor.value = this.parseToDate(this.value);
        this.close();
    }

    /** @hidden @internal */
    public okButtonClick(): void {
        this.updateValue(this._selectedDate);
        this.close();
    }

    /** @hidden @internal */
    public onItemClick(item: string, dateType: string): void {
        let date = new Date(this._selectedDate);
        switch (dateType) {
            case 'hourList':
                let ampm: string;
                const selectedHour = parseInt(item, 10);
                let hours = selectedHour;

                if (this.showAmPmList) {
                    ampm = this.getPartValue(date, 'ampm');
                    hours = this.toTwentyFourHourFormat(hours, ampm);
                    const minHours = this.minDropdownValue?.getHours() || 0;
                    const maxHours = this.maxDropdownValue?.getHours() || 24;
                    if (hours < minHours || hours > maxHours) {
                        hours = hours < 12 ? hours + 12 : hours - 12;
                    }
                }

                date.setHours(hours);
                date = this.validateDropdownValue(date);

                if (this.valueInRange(date, this.minDropdownValue, this.maxDropdownValue)) {
                    this.setSelectedValue(date);
                }
                break;
            case 'minuteList': {
                const minutes = parseInt(item, 10);
                date.setMinutes(minutes);
                date = this.validateDropdownValue(date);
                this.setSelectedValue(date);
                break;
            }
            case 'secondsList': {
                const seconds = parseInt(item, 10);
                date.setSeconds(seconds);
                if (this.valueInRange(date, this.minDropdownValue, this.maxDropdownValue)) {
                    this.setSelectedValue(date);
                }
                break;
            }
            case 'ampmList': {
                let hour = this._selectedDate.getHours();
                hour = item === 'AM' ? hour - 12 : hour + 12;
                date.setHours(hour);
                date = this.validateDropdownValue(date, true);
                this.setSelectedValue(date);
                break;
            }
        }
        this.updateEditorValue();
    }

    /** @hidden @internal */
    public nextHour(delta: number) {
        delta = delta > 0 ? 1 : -1;
        const previousDate = new Date(this._selectedDate);
        const minHours = this.minDropdownValue?.getHours();
        const maxHours = this.maxDropdownValue?.getHours();
        const previousHours = previousDate.getHours();
        let hours = previousHours + delta * this.itemsDelta.hours;
        if ((previousHours === maxHours && delta > 0) || (previousHours === minHours && delta < 0)) {
            hours = !this.spinLoop ? previousHours : delta > 0 ? minHours : maxHours;
        }

        this._selectedDate.setHours(hours);
        this._selectedDate = this.validateDropdownValue(this._selectedDate);
        this._selectedDate = new Date(this._selectedDate);
        this.updateEditorValue();
    }

    /** @hidden @internal */
    public nextMinute(delta: number) {
        delta = delta > 0 ? 1 : -1;
        const minHours = this.minDropdownValue.getHours();
        const maxHours = this.maxDropdownValue.getHours();
        const hours = this._selectedDate.getHours();
        let minutes = this._selectedDate.getMinutes();
        const minMinutes = hours === minHours ? this.minDropdownValue.getMinutes() : 0;
        const maxMinutes = hours === maxHours ? this.maxDropdownValue.getMinutes() :
            60 % this.itemsDelta.minutes > 0 ? 60 - (60 % this.itemsDelta.minutes) :
                60 - this.itemsDelta.minutes;

        if ((delta < 0 && minutes === minMinutes) || (delta > 0 && minutes === maxMinutes)) {
            minutes = this.spinLoop && minutes === minMinutes ? maxMinutes : this.spinLoop && minutes === maxMinutes ? minMinutes : minutes;
        } else {
            minutes = minutes + delta * this.itemsDelta.minutes;
        }

        this._selectedDate.setMinutes(minutes);
        this._selectedDate = this.validateDropdownValue(this._selectedDate);
        this._selectedDate = new Date(this._selectedDate);
        this.updateEditorValue();
    }

    /** @hidden @internal */
    public nextSeconds(delta: number) {
        delta = delta > 0 ? 1 : -1;
        const minHours = this.minDropdownValue.getHours();
        const maxHours = this.maxDropdownValue.getHours();
        const hours = this._selectedDate.getHours();
        const minutes = this._selectedDate.getMinutes();
        const minMinutes = this.minDropdownValue.getMinutes();
        const maxMinutes = this.maxDropdownValue.getMinutes();
        let seconds = this._selectedDate.getSeconds();
        const minSeconds = (hours === minHours && minutes === minMinutes) ? this.minDropdownValue.getSeconds() : 0;
        const maxSeconds = (hours === maxHours && minutes === maxMinutes) ? this.maxDropdownValue.getSeconds() :
            60 % this.itemsDelta.seconds > 0 ? 60 - (60 % this.itemsDelta.seconds) :
                60 - this.itemsDelta.seconds;

        if ((delta < 0 && seconds === minSeconds) || (delta > 0 && seconds === maxSeconds)) {
            seconds = this.spinLoop && seconds === minSeconds ? maxSeconds : this.spinLoop && seconds === maxSeconds ? minSeconds : seconds;
        } else {
            seconds = seconds + delta * this.itemsDelta.seconds;
        }

        this._selectedDate.setSeconds(seconds);
        this._selectedDate = this.validateDropdownValue(this._selectedDate);
        this._selectedDate = new Date(this._selectedDate);
        this.updateEditorValue();
    }

    /** @hidden @internal */
    public nextAmPm(delta?: number) {
        const ampm = this.getPartValue(this._selectedDate, 'ampm');
        if (!delta || (ampm === 'AM' && delta > 0) || (ampm === 'PM' && delta < 0)) {
            let hours = this._selectedDate.getHours();
            const sign = hours < 12 ? 1 : -1;
            hours = hours + sign * 12;
            this._selectedDate.setHours(hours);
            this._selectedDate = this.validateDropdownValue(this._selectedDate, true);
            this._selectedDate = new Date(this._selectedDate);
            this.updateEditorValue();
        }
    }

    /** @hidden @internal */
    public setSelectedValue(value: Date) {
        this._selectedDate = value ? new Date(value) : null;
        if (!DateTimeUtil.isValidDate(this._selectedDate)) {
            this._selectedDate = new Date(this.minDropdownValue);
            return;
        }
        if (this.minValue && DateTimeUtil.lessThanMinValue(this._selectedDate, this.minDropdownValue, true, false)) {
            this._selectedDate = new Date(this.minDropdownValue);
            return;
        }
        if (this.maxValue && DateTimeUtil.greaterThanMaxValue(this._selectedDate, this.maxDropdownValue, true, false)) {
            this._selectedDate = new Date(this.maxDropdownValue);
            return;
        }

        if (this._selectedDate.getHours() % this.itemsDelta.hours > 0) {
            this._selectedDate.setHours(
                this._selectedDate.getHours() + this.itemsDelta.hours - this._selectedDate.getHours() % this.itemsDelta.hours,
                0,
                0
            );
        }

        if (this._selectedDate.getMinutes() % this.itemsDelta.minutes > 0) {
            this._selectedDate.setHours(
                this._selectedDate.getHours(),
                this._selectedDate.getMinutes() + this.itemsDelta.minutes - this._selectedDate.getMinutes() % this.itemsDelta.minutes,
                0
            );
        }

        if (this._selectedDate.getSeconds() % this.itemsDelta.seconds > 0) {
            this._selectedDate.setSeconds(
                this._selectedDate.getSeconds() + this.itemsDelta.seconds - this._selectedDate.getSeconds() % this.itemsDelta.seconds
            );
        }
    }

    protected onStatusChanged() {
        if (this._ngControl && !this._ngControl.disabled && this.isTouchedOrDirty) {
            if (this.hasValidators && this._inputGroup.isFocused) {
                this.inputDirective.valid = this._ngControl.valid ? IgxInputState.VALID : IgxInputState.INVALID;
            } else {
                this.inputDirective.valid = this._ngControl.valid ? IgxInputState.INITIAL : IgxInputState.INVALID;
            }
        } else {
            // B.P. 18 May 2021: IgxDatePicker does not reset its state upon resetForm #9526
            this.inputDirective.valid = IgxInputState.INITIAL;
        }

        if (this._inputGroup && this._inputGroup.isRequired !== this.required) {
            this._inputGroup.isRequired = this.required;
        }
    }

    private get isTouchedOrDirty(): boolean {
        return (this._ngControl.control.touched || this._ngControl.control.dirty);
    }

    private get hasValidators(): boolean {
        return (!!this._ngControl.control.validator || !!this._ngControl.control.asyncValidator);
    }

    private setMinMaxDropdownValue(type: string, time: Date): Date {
        let delta: number;

        const sign = type === 'min' ? 1 : -1;

        const hours = time.getHours();
        let minutes = time.getMinutes();
        let seconds = time.getSeconds();

        if (this.showHoursList && hours % this.itemsDelta.hours > 0) {
            delta = type === 'min' ? this.itemsDelta.hours - hours % this.itemsDelta.hours
                : hours % this.itemsDelta.hours;
            minutes = type === 'min' ? 0
                : 60 % this.itemsDelta.minutes > 0 ? 60 - 60 % this.itemsDelta.minutes
                    : 60 - this.itemsDelta.minutes;
            seconds = type === 'min' ? 0
                : 60 % this.itemsDelta.seconds > 0 ? 60 - 60 % this.itemsDelta.seconds
                    : 60 - this.itemsDelta.seconds;
            time.setHours(hours + sign * delta, minutes, seconds);
        } else if (this.showMinutesList && minutes % this.itemsDelta.minutes > 0) {
            delta = type === 'min' ? this.itemsDelta.minutes - minutes % this.itemsDelta.minutes
                : minutes % this.itemsDelta.minutes;
            seconds = type === 'min' ? 0
                : 60 % this.itemsDelta.seconds > 0 ? 60 - 60 % this.itemsDelta.seconds
                    : 60 - this.itemsDelta.seconds;
            time.setHours(hours, minutes + sign * delta, seconds);
        } else if (this.showSecondsList && seconds % this.itemsDelta.seconds > 0) {
            delta = type === 'min' ? this.itemsDelta.seconds - seconds % this.itemsDelta.seconds
                : seconds % this.itemsDelta.seconds;
            time.setHours(hours, minutes, seconds + sign * delta);
        }

        return time;
    }

    private initializeContainer() {
        requestAnimationFrame(() => {
            if (this.hourList) {
                this.hourList.nativeElement.focus();
            } else if (this.minuteList) {
                this.minuteList.nativeElement.focus();
            } else if (this.secondsList) {
                this.secondsList.nativeElement.focus();
            }
        });
    }

    private validateDropdownValue(date: Date, isAmPm = false): Date {
        if (date > this.maxDropdownValue) {
            if (isAmPm && date.getHours() !== this.maxDropdownValue.getHours()) {
                date.setHours(12);
            } else {
                date = new Date(this.maxDropdownValue);
            }
        }

        if (date < this.minDropdownValue) {
            date = new Date(this.minDropdownValue);
        }

        return date;
    }

    private emitValueChange(oldValue: Date | string, newValue: Date | string) {
        if (!isEqual(oldValue, newValue)) {
            this.valueChange.emit(newValue);
        }
    }

    private emitValidationFailedEvent(previousValue: Date | string) {
        const args: IgxTimePickerValidationFailedEventArgs = {
            owner: this,
            previousValue,
            currentValue: this.value
        };
        this.validationFailed.emit(args);
    }

    private updateValidityOnBlur() {
        this._onTouchedCallback();
        if (this._ngControl) {
            if (!this._ngControl.valid) {
                this.inputDirective.valid = IgxInputState.INVALID;
            } else {
                this.inputDirective.valid = IgxInputState.INITIAL;
            }
        }
    }

    private valueInRange(value: Date, minValue: Date, maxValue: Date): boolean {
        if (minValue && DateTimeUtil.lessThanMinValue(value, minValue, true, false)) {
            return false;
        }
        if (maxValue && DateTimeUtil.greaterThanMaxValue(value, maxValue, true, false)) {
            return false;
        }

        return true;
    }

    private parseToDate(value: Date | string): Date | null {
        return DateTimeUtil.isValidDate(value) ? value : DateTimeUtil.parseIsoDate(value);
    }

    private toTwentyFourHourFormat(hour: number, ampm: string): number {
        if (ampm === 'PM' && hour < 12) {
            hour += 12;
        } else if (ampm === 'AM' && hour === 12) {
            hour = 0;
        }

        return hour;
    }

    private updateValue(newValue: Date | null): void {
        if (!this.value) {
            this.value = newValue ? new Date(newValue) : newValue;
        } else if (isDate(this.value)) {
            const date = new Date(this.value);
            date.setHours(newValue?.getHours() || 0, newValue?.getMinutes() || 0, newValue?.getSeconds() || 0);
            this.value = date;
        } else {
            this.value = newValue ? this.toISOString(newValue) : newValue;
        }
    }

    private updateEditorValue(): void {
        const date = this.dateTimeEditor.value ? new Date(this.dateTimeEditor.value) : new Date();
        date.setHours(this._selectedDate.getHours(), this._selectedDate.getMinutes(), this._selectedDate.getSeconds());
        this.dateTimeEditor.value = date;
    }

    private subscribeToDateEditorEvents(): void {
        this.dateTimeEditor.valueChange.pipe(
            // internal date editor directive is only used w/ Date object values:
            takeUntil(this._destroy$)).subscribe((date: Date | null) => {
                this.updateValue(date);
            });

        this.dateTimeEditor.validationFailed.pipe(
            takeUntil(this._destroy$)).subscribe((event) => {
                this.emitValidationFailedEvent(event.oldValue);
            });
    }

    private subscribeToToggleDirectiveEvents(): void {
        if (this.toggleRef) {
            if (this._inputGroup) {
                this.toggleRef.element.style.width = this._inputGroup.element.nativeElement.getBoundingClientRect().width + 'px';
            }

            this.toggleRef.opening.pipe(takeUntil(this._destroy$)).subscribe((e) => {
                const args: IBaseCancelableBrowserEventArgs = { owner: this, event: e.event, cancel: false };
                this.opening.emit(args);
                e.cancel = args.cancel;
                if (args.cancel) {
                    return;
                }
                this.initializeContainer();
            });

            this.toggleRef.opened.pipe(takeUntil(this._destroy$)).subscribe(() => {
                this.opened.emit({ owner: this });
            });

            this.toggleRef.closed.pipe(takeUntil(this._destroy$)).subscribe(() => {
                this.closed.emit({ owner: this });
            });

            this.toggleRef.closing.pipe(takeUntil(this._destroy$)).subscribe((e) => {
                const args: IBaseCancelableBrowserEventArgs = { owner: this, event: e.event, cancel: false };
                this.closing.emit(args);
                e.cancel = args.cancel;
                if (args.cancel) {
                    return;
                }
                const value = this.parseToDate(this.value);
                if ((this.dateTimeEditor.value as Date)?.getTime() !== value?.getTime()) {
                    this.updateValue(this._selectedDate);
                }
                // Do not focus the input if clicking outside in dropdown mode
                const input = this.getEditElement();
                if (input && !(e.event && this.isDropdown)) {
                    input.focus();
                } else {
                    this.updateValidityOnBlur();
                }
            });
        }
    }
}
