import { animate, AnimationBuilder, AnimationPlayer, style } from '@angular/animations'
import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion'
import { ScrollStrategy, ScrollStrategyOptions } from '@angular/cdk/overlay'
import {
	AfterViewInit,
	ChangeDetectionStrategy,
	ChangeDetectorRef,
	Component,
	DOCUMENT,
	ElementRef,
	HostBinding,
	HostListener,
	inject,
	Input,
	input,
	OnChanges,
	OnDestroy,
	OnInit,
	output,
	QueryList,
	Renderer2,
	SimpleChanges,
	viewChild,
	ViewChildren,
	ViewEncapsulation,
} from '@angular/core'
import { NavigationEnd, Router } from '@angular/router'
import { FuseScrollbarDirective } from '@frontend/shared'
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'
import { AnyType } from '@core/types'
import { delay, filter, merge, ReplaySubject, Subscription } from 'rxjs'
import { fuseAnimations, FuseUtilsService } from '../../../shared'
import { FuseNavigationService } from '../../services'
import {
	FuseNavigationItem,
	FuseVerticalNavigationAppearance,
	FuseVerticalNavigationMode,
	FuseVerticalNavigationPosition,
} from '../../types'
import { FuseVerticalNavigationBasicItemComponent } from './components'

@UntilDestroy({ checkProperties: true })
@Component({
	// eslint-disable-next-line @angular-eslint/component-selector
	selector: 'fuse-vertical-navigation',
	templateUrl: './vertical.component.html',
	styleUrls: ['./vertical.component.scss'],
	animations: fuseAnimations,
	encapsulation: ViewEncapsulation.None,
	changeDetection: ChangeDetectionStrategy.OnPush,
	exportAs: 'fuseVerticalNavigation',
	imports: [FuseScrollbarDirective, FuseVerticalNavigationBasicItemComponent],
})
export class FuseVerticalNavigationComponent implements OnChanges, OnInit, AfterViewInit, OnDestroy {
	static ngAcceptInputType_inner: BooleanInput
	static ngAcceptInputType_opened: BooleanInput
	static ngAcceptInputType_transparentOverlay: BooleanInput
	readonly appearance = input<FuseVerticalNavigationAppearance>('default')
	readonly autoCollapse = input(true)
	//  Your application code writes to the input. This prevents migration.
	@Input() inner = false
	readonly mode = input<FuseVerticalNavigationMode>('side')
	readonly navigation = input.required<FuseNavigationItem[]>()
	//  Your application code writes to the input. This prevents migration.
	@Input() opened = true
	readonly position = input<FuseVerticalNavigationPosition>('left')
	//  Your application code writes to the input. This prevents migration.
	@Input() transparentOverlay = false
	readonly appearanceChanged = output<FuseVerticalNavigationAppearance>()
	readonly modeChanged = output<FuseVerticalNavigationMode>()
	readonly openedChanged = output<boolean>()
	// TODO: Skipped for migration because:
	readonly positionChanged = output<FuseVerticalNavigationPosition>()
	activeAsideItemId: string | null = null
	// TODO: Skipped for migration because:
	onCollapsableItemCollapsed: ReplaySubject<FuseNavigationItem> = new ReplaySubject<FuseNavigationItem>(1)
	onCollapsableItemExpanded: ReplaySubject<FuseNavigationItem> = new ReplaySubject<FuseNavigationItem>(1)
	// TODO: Skipped for migration because:
	onRefreshed: ReplaySubject<boolean> = new ReplaySubject<boolean>(1)
	private _animationBuilder = inject(AnimationBuilder)
	// TODO: Skipped for migration because:
	private _changeDetectorRef = inject(ChangeDetectorRef)
	private _elementRef = inject(ElementRef)
	private _renderer2 = inject(Renderer2)
	private _router = inject(Router)
	private _scrollStrategyOptions = inject(ScrollStrategyOptions)
	private _fuseNavigationService = inject(FuseNavigationService)
	private _fuseUtilsService = inject(FuseUtilsService)
	//  Your application code writes to the input. This prevents migration.
	@Input() name: string = this._fuseUtilsService.randomId()
	private _document = inject<Document>(DOCUMENT)
	private readonly _navigationContentEl = viewChild.required<ElementRef>('navigationContent')
	private _animationsEnabled = false
	private _asideOverlay: HTMLElement | null = null
	private readonly _handleAsideOverlayClick: AnyType
	private readonly _handleOverlayClick: AnyType
	private _hovered = false
	private _mutationObserver!: MutationObserver
	private _overlay: HTMLElement | null = null
	private _player!: AnimationPlayer
	private _scrollStrategy: ScrollStrategy = this._scrollStrategyOptions.block()
	private _fuseScrollbarDirectivesSubscription!: Subscription

	constructor() {
		this._handleAsideOverlayClick = (): void => {
			this.closeAside()
		}
		this._handleOverlayClick = (): void => {
			this.close()
		}
	}

	private _fuseScrollbarDirectives!: QueryList<FuseScrollbarDirective>

	//  Accessor queries cannot be migrated as they are too complex.
	@ViewChildren(FuseScrollbarDirective)
	set fuseScrollbarDirectives(fuseScrollbarDirectives: QueryList<FuseScrollbarDirective>) {
		// Store the directives
		this._fuseScrollbarDirectives = fuseScrollbarDirectives

		// Return if there are no directives
		if (fuseScrollbarDirectives.length === 0) {
			return
		}

		// Unsubscribe the previous subscriptions
		if (this._fuseScrollbarDirectivesSubscription) {
			this._fuseScrollbarDirectivesSubscription.unsubscribe()
		}

		// Update the scrollbars on collapsable items' collapse/expand
		this._fuseScrollbarDirectivesSubscription = merge(this.onCollapsableItemCollapsed, this.onCollapsableItemExpanded)
			.pipe(untilDestroyed(this), delay(250))
			.subscribe(() => {
				// Loop through the scrollbars and update them
				for (const fuseScrollbarDirective of fuseScrollbarDirectives) {
					fuseScrollbarDirective.update()
				}
			})
	}

	@HostBinding('class') get classList(): AnyType {
		return {
			'fuse-vertical-navigation-animations-enabled': this._animationsEnabled,
			[`fuse-vertical-navigation-appearance-${this.appearance()}`]: true,
			'fuse-vertical-navigation-hover': this._hovered,
			'fuse-vertical-navigation-inner': this.inner,
			'fuse-vertical-navigation-mode-over': this.mode() === 'over',
			'fuse-vertical-navigation-mode-side': this.mode() === 'side',
			'fuse-vertical-navigation-opened': this.opened,
			'fuse-vertical-navigation-position-left': this.position() === 'left',
			'fuse-vertical-navigation-position-right': this.position() === 'right',
		}
	}

	// TODO: Skipped for migration because:

	@HostBinding('style') get styleList(): AnyType {
		return {
			visibility: this.opened ? 'visible' : 'hidden',
		}
	}

	ngOnChanges(changes: SimpleChanges): void {
		// Appearance
		if ('appearance' in changes) {
			// Execute the observable
			this.appearanceChanged.emit(changes['appearance'].currentValue)
		}

		// Inner
		if ('inner' in changes) {
			// Coerce the value to a boolean
			this.inner = coerceBooleanProperty(changes['inner'].currentValue)
		}

		// Mode
		if ('mode' in changes) {
			// Get the previous and current values
			const currentMode = changes['mode'].currentValue
			const previousMode = changes['mode'].previousValue

			// Disable the animations
			this._disableAnimations()

			// If the mode changes: 'over -> side'
			if (previousMode === 'over' && currentMode === 'side') {
				// Hide the overlay
				this._hideOverlay()
			}

			// If the mode changes: 'side -> over'
			if (previousMode === 'side' && currentMode === 'over') {
				// Close the aside
				this.closeAside()

				// If the navigation is opened
				if (this.opened) {
					// Show the overlay
					this._showOverlay()
				}
			}

			// Execute the observable
			this.modeChanged.emit(currentMode)

			// Enable the animations after a delay
			// The delay must be bigger than the current transition-duration
			// to make sure nothing will be animated while the mode changing
			setTimeout(() => {
				this._enableAnimations()
			}, 500)
		}

		// Navigation
		if ('navigation' in changes) {
			// Mark for check
			this._changeDetectorRef.markForCheck()
		}

		// Opened
		if ('opened' in changes) {
			// Coerce the value to a boolean
			this.opened = coerceBooleanProperty(changes['opened'].currentValue)

			// Open/close the navigation
			this._toggleOpened(this.opened)
		}

		// Position
		if ('position' in changes) {
			// Execute the observable
			this.positionChanged.emit(changes['position'].currentValue)
		}

		// Transparent overlay
		if ('transparentOverlay' in changes) {
			// Coerce the value to a boolean
			this.transparentOverlay = coerceBooleanProperty(changes['transparentOverlay'].currentValue)
		}
	}

	ngOnInit(): void {
		// Make sure the name input is not an empty string
		if (this.name === '') {
			this.name = this._fuseUtilsService.randomId()
		}

		// Register the navigation component
		this._fuseNavigationService.registerComponent(this.name, this)

		// Subscribe to the 'NavigationEnd' event
		this._router.events
			.pipe(
				filter((event) => event instanceof NavigationEnd),
				untilDestroyed(this),
			)
			.subscribe(() => {
				// If the mode is 'over' and the navigation is opened...
				const mode = this.mode()
				if (mode === 'over' && this.opened) {
					// Close the navigation
					this.close()
				}

				// If the mode is 'side' and the aside is active...
				if (mode === 'side' && this.activeAsideItemId) {
					// Close the aside
					this.closeAside()
				}
			})
	}

	ngAfterViewInit(): void {
		// Fix for Firefox.
		//
		// Because 'position: sticky' doesn't work correctly inside a 'position: fixed' parent,
		// adding the '.cdk-global-scrollblock' to the html element breaks the navigation's position.
		// This fixes the problem by reading the 'top' value from the html element and adding it as a
		// 'marginTop' to the navigation itself.
		this._mutationObserver = new MutationObserver((mutations) => {
			for (const mutation of mutations) {
				const mutationTarget = mutation.target as HTMLElement
				if (mutation.attributeName === 'class') {
					if (mutationTarget.classList.contains('cdk-global-scrollblock')) {
						const top = Number.parseInt(mutationTarget.style.top, 10)
						this._renderer2.setStyle(this._elementRef.nativeElement, 'margin-top', `${Math.abs(top)}px`)
					} else {
						this._renderer2.setStyle(this._elementRef.nativeElement, 'margin-top', null)
					}
				}
			}
		})
		this._mutationObserver.observe(this._document.documentElement, {
			attributes: true,
			attributeFilter: ['class'],
		})

		setTimeout(() => {
			// Return if 'navigation content' element does not exist
			const _navigationContentEl = this._navigationContentEl()
			if (!_navigationContentEl) {
				return
			}

			// If 'navigation content' element doesn't have
			// perfect scrollbar activated on it...
			if (_navigationContentEl.nativeElement.classList.contains('ps')) {
				// Go through all the scrollbar directives
				for (const fuseScrollbarDirective of this._fuseScrollbarDirectives) {
					// Skip if not enabled
					if (!fuseScrollbarDirective.isEnabled()) {
						continue
					}

					// Scroll to the active element
					fuseScrollbarDirective.scrollToElement('.fuse-vertical-navigation-item-active', -120, true)
				}
			}
			// Otherwise
			else {
				// Find the active item
				const activeItem = _navigationContentEl.nativeElement.querySelector('.fuse-vertical-navigation-item-active')

				// If the active item exists, scroll it into view
				if (activeItem) {
					activeItem.scrollIntoView()
				}
			}
		})
	}

	ngOnDestroy(): void {
		// Disconnect the mutation observer
		this._mutationObserver.disconnect()

		// Forcefully close the navigation and aside in case they are opened
		this.close()
		this.closeAside()

		// Deregister the navigation component from the registry
		this._fuseNavigationService.deregisterComponent(this.name)
	}

	refresh(): void {
		// Mark for check
		this._changeDetectorRef.markForCheck()

		// Execute the observable
		this.onRefreshed.next(true)
	}

	open(): void {
		// Return if the navigation is already open
		if (this.opened) {
			return
		}

		// Set the opened
		this._toggleOpened(true)
	}

	close(): void {
		// Return if the navigation is already closed
		if (!this.opened) {
			return
		}

		// Close the aside
		this.closeAside()

		// Set the opened
		this._toggleOpened(false)
	}

	toggle(): void {
		// Toggle
		if (this.opened) {
			this.close()
		} else {
			this.open()
		}
	}

	openAside(item: FuseNavigationItem): void {
		// Return if the item is disabled
		if (item.disabled || !item.id) {
			return
		}

		// Open
		this.activeAsideItemId = item.id

		// Show the aside overlay
		this._showAsideOverlay()

		// Mark for check
		this._changeDetectorRef.markForCheck()
	}

	closeAside(): void {
		// Close
		this.activeAsideItemId = null

		// Hide the aside overlay
		this._hideAsideOverlay()

		// Mark for check
		this._changeDetectorRef.markForCheck()
	}

	toggleAside(item: FuseNavigationItem): void {
		// Toggle
		if (this.activeAsideItemId === item.id) {
			this.closeAside()
		} else {
			this.openAside(item)
		}
	}

	trackByFn(index: number, item: AnyType): AnyType {
		return item.id || index
	}

	@HostListener('mouseenter')
	protected _onMouseenter(): void {
		// Enable the animations
		this._enableAnimations()

		// Set the hovered
		this._hovered = true
	}

	@HostListener('mouseleave')
	protected _onMouseleave(): void {
		// Enable the animations
		this._enableAnimations()

		// Set the hovered
		this._hovered = false
	}

	private _enableAnimations(): void {
		// Return if the animations are already enabled
		if (this._animationsEnabled) {
			return
		}

		// Enable the animations
		this._animationsEnabled = true
	}

	private _disableAnimations(): void {
		// Return if the animations are already disabled
		if (!this._animationsEnabled) {
			return
		}

		// Disable the animations
		this._animationsEnabled = false
	}

	private _showOverlay(): void {
		// Return if there is already an overlay
		if (this._asideOverlay) {
			return
		}

		// Create the overlay element
		this._overlay = this._renderer2.createElement('div')

		// Add a class to the overlay element
		this._overlay?.classList.add('fuse-vertical-navigation-overlay')

		// Add a class depending on the transparentOverlay option
		if (this.transparentOverlay) {
			this._overlay?.classList.add('fuse-vertical-navigation-overlay-transparent')
		}

		// Append the overlay to the parent of the navigation
		this._renderer2.appendChild(this._elementRef.nativeElement.parentElement, this._overlay)

		// Enable block scroll strategy
		this._scrollStrategy.enable()

		// Create the enter animation and attach it to the player
		this._player = this._animationBuilder
			.build([animate('300ms cubic-bezier(0.25, 0.8, 0.25, 1)', style({ opacity: 1 }))])
			.create(this._overlay)

		// Play the animation
		this._player.play()

		// Add an event listener to the overlay
		this._overlay?.addEventListener('click', this._handleOverlayClick)
	}

	private _hideOverlay(): void {
		if (!this._overlay) {
			return
		}

		// Create the leave animation and attach it to the player
		this._player = this._animationBuilder
			.build([animate('300ms cubic-bezier(0.25, 0.8, 0.25, 1)', style({ opacity: 0 }))])
			.create(this._overlay)

		// Play the animation
		this._player.play()

		// Once the animation is done...
		this._player.onDone(() => {
			// If the overlay still exists...
			if (this._overlay) {
				this._overlay.removeEventListener('click', this._handleOverlayClick)
				this._overlay.remove()
				this._overlay = null
			}

			// Disable block scroll strategy
			this._scrollStrategy.disable()
		})
	}

	private _showAsideOverlay(): void {
		// Return if there is already an overlay
		if (this._asideOverlay) {
			return
		}

		// Create the aside overlay element
		this._asideOverlay = this._renderer2.createElement('div')

		// Add a class to the aside overlay element
		this._asideOverlay?.classList.add('fuse-vertical-navigation-aside-overlay')

		// Append the aside overlay to the parent of the navigation
		this._renderer2.appendChild(this._elementRef.nativeElement.parentElement, this._asideOverlay)

		// Create the enter animation and attach it to the player
		this._player = this._animationBuilder
			.build([animate('300ms cubic-bezier(0.25, 0.8, 0.25, 1)', style({ opacity: 1 }))])
			.create(this._asideOverlay)

		// Play the animation
		this._player.play()

		// Add an event listener to the aside overlay
		this._asideOverlay?.addEventListener('click', this._handleAsideOverlayClick)
	}

	private _hideAsideOverlay(): void {
		if (!this._asideOverlay) {
			return
		}

		this._player = this._animationBuilder
			.build([animate('300ms cubic-bezier(0.25, 0.8, 0.25, 1)', style({ opacity: 0 }))])
			.create(this._asideOverlay)

		this._player.play()

		this._player.onDone(() => {
			if (this._asideOverlay) {
				this._asideOverlay.removeEventListener('click', this._handleAsideOverlayClick)
				this._asideOverlay?.remove()
				this._asideOverlay = null
			}
		})
	}

	private _toggleOpened(open: boolean): void {
		// Set the opened
		this.opened = open

		// Enable the animations
		this._enableAnimations()

		// If the navigation opened, and the mode
		// is 'over', show the overlay
		if (this.mode() === 'over') {
			if (this.opened) {
				this._showOverlay()
			} else {
				this._hideOverlay()
			}
		}

		// Execute the observable
		this.openedChanged.emit(open)
	}
}
