import {
	Component,
	ElementRef,
	EventEmitter,
	Input,
	OnChanges,
	OnDestroy,
	Output,
	Renderer2,
	SimpleChanges,
	ViewChild
} from '@angular/core';

// Интерфейсы
import {
	IColumn,
	IOffset
} from '../../interfaces';

// Перечисления
import {
	CursorType,
	SortType,
	SwapCommand
} from '../../enums';

// Сервисы
import { CrmHelperService } from '../../services';

// Шапка таблицы
@Component({
	selector: 'crm-table-header',
	templateUrl: './crm-table-header.component.html',
	styleUrls: ['./crm-table-header.component.scss']
})
export class CrmTableHeaderComponent
	implements OnChanges, OnDestroy {

	// Конструктор
	constructor(private readonly renderer: Renderer2) {}

	// Закрепленные столбцы
	@Input() public pinnedColumns: IColumn[];

	// Ширина области контента закрепленных столбцов
	@Input() public pinnedCanvasWidth: number;

	// Обычные столбцы
	@Input() public defaultColumns: IColumn[];

	// Ширина области контента обычных столбцов
	@Input() public defaultCanvasWidth: number;

	// Ширина области отображения обычных столбцов
	@Input() public defaultViewportWidth: number;

	// Смещение области контента в области отображения
	@Input() public offset: number;

	// Событие изменения сортировки
	@Output() public changeSort = new EventEmitter<IColumn>();

	// Событие изменения закрепления столбца
	@Output() public changePin = new EventEmitter<IColumn>();

	// Событие изменения ширины столбца
	@Output() public changeSize = new EventEmitter<IColumn>();

	// Событие изменения позиции
	@Output() public changeSwap = new EventEmitter<SwapCommand>();

	// Событие прокрутки контента
	@Output() public changeScroll = new EventEmitter<number>();

	// Ссылка на область контента обычных столбцов
	@ViewChild('view') public view: ElementRef;

	// Ссылка на обьект для touch событий
	private touchTarget: Element;

	// Минимальная ширина столбца при изменении размера (px)
	private readonly COLUMN_MIN_SIZE: number = 100;

	// Граница срабатывания (закрепить / снять) (px)
	private readonly PIN_BORDER_SIZE: number = 25;

	// Столбец для изменения размера
	private resizerColumn: IColumn;

	// Размер столбца перед изменением ширины
	private resizerColumnWidthBefore: number = 0;

	// Смещение от разделителя до левого края экрана
	private resizerPosStart: number = 0;

	// Столбец для перемещения
	private swapColumn: IColumn;

	// Расстояние срабатывания начала перемещения (px)
	private readonly SWAP_TRIGGER_DISTANCE: number = 16;

	// Позиция до начала перемещения (для расчета расстояния)
	private readonly swapPosStart: IOffset = {
		left: 0,
		top: 0
	};

	// Последняя позиция курсора
	private swapPosLast: number = 0;

	// Функции для отключения прослушивания событий
	private mouseMoveListen: () => void;
	private mouseUpListen: () => void;
	private touchMoveListen: () => void;
	private touchEndListen: () => void;

	// Типы сортировки
	public SortType: typeof SortType = SortType;

	// Тип курсора для всей страницы
	public cursorType: CursorType = CursorType.Auto;

	// Граница срабатывания автопрокрутки (px)
	private readonly SCROLL_BORDER_SIZE: number = 1;

	// Интервал автопрокрутки (ms)
	private readonly SCROLL_DELAY: number = 20;

	// Шаг автопрокрутки (px)
	private readonly SCROLL_STEP: number = 10;

	// Ссылка на запущенную автопрокрутку
	private scrollTimerId: any = null;

	// Флаг, прокрутка влево возможна
	private get canLeftScroll(): boolean {
		return !!CrmHelperService.clamp(this.offset);
	}

	// Флаг, прокрутка вправо возможна
	private get canRightScroll(): boolean {
		return !!CrmHelperService.clamp(
			this.defaultCanvasWidth -
			this.defaultViewportWidth -
			this.offset
		);
	}

	// Размер допустимой прокрутки влево
	private get leftScroll(): number {
		return this.canLeftScroll
			? CrmHelperService.clamp(this.offset - this.swapColumn.offset)
			: 0;
	}

	// Размер допустимой прокрутки вправо
	private get rightScroll(): number {
		return this.canRightScroll
			? CrmHelperService.clamp(
					(this.swapColumn.offset + this.swapColumn.width) -
					(this.offset + this.defaultViewportWidth)
				)
			: 0;
	}

	// --------------------------------------------------------------------------
	// Смена сортировки
	public onSortToggle(column: IColumn): void {
		if (column.isSorted) {
			column.sortType = column.sortType === SortType.None
			? SortType.Asc
			: column.sortType === SortType.Asc
				? SortType.Desc
				: SortType.None;
			this.changeSort.emit(column);
		}
	}

	// Закрепление столбца
	public onPinToggle(column: IColumn): void {
		if (column.isPinned) {
			// Снимаем все пины справа
			let isPinned = true;
			this.pinnedColumns.forEach((value: IColumn) => {
				isPinned = isPinned && value !== column;
				value.isPinned = isPinned;
			});
		} else {
			// Установить пин можно только крайнему столбцу слева
			column.isPinned = column === this.defaultColumns[0];
		}
		this.changePin.emit(column);
	}

	// --------------------------------------------------------------------------
	// RESIZE MOUSE
	// Обработчик нажатия мыши на разделитель столбцов
	public onResizeMouseDown(event: MouseEvent, column: IColumn): void {
		if (event && event.button === 0 && column) {
			this.resizerColumn = column;
			this.resizerPressed(event.clientX);
			this.setCursorStyle(CursorType.ColResize);

			// Подписка на события
			this.mouseMoveListen = this.renderer.listen(
				document,
				'mousemove',
				this.onResizeMouseMove.bind(this)
			);
			this.mouseUpListen = this.renderer.listen(
				document,
				'mouseup',
				this.onResizeMouseUp.bind(this)
			);
		}
	}

	// Обработчик перемещения мыши
	public onResizeMouseMove(event: MouseEvent): void {
		this.resizerMove(event.clientX);
	}

	// Обработчик отжатия мыши
	public onResizeMouseUp(event: MouseEvent): void {
		this.resizerReleased();
	}

	// --------------------------------------------------------------------------
	// RESIZE TOUCH
	// Обработчик нажатия сенсором на разделитель столбцов
	public onResizeTouchStart(event: TouchEvent, column: IColumn): void {
		const touch = event.touches[event.touches.length - 1];
		const touchTarget = (
			event.target ||
			event.srcElement ||
			event.currentTarget
		) as Element;

		if (touch && column) {
			this.touchTarget = touchTarget;
			this.resizerColumn = column;
			this.resizerPressed(touch.clientX);
			this.setCursorStyle(CursorType.ColResize);

			// Подписка на события
			this.touchMoveListen = this.renderer.listen(
				this.touchTarget,
				'touchmove',
				this.onResizeTouchMove.bind(this)
			);
			this.touchEndListen = this.renderer.listen(
				this.touchTarget,
				'touchend',
				this.onResizeTouchEnd.bind(this)
			);
		}
	}

	// Обработчик перемещения
	public onResizeTouchMove(event: TouchEvent): void {
		const touch = event.touches[event.touches.length - 1];
		this.resizerMove(touch.clientX);
	}

	// Обработчик отжатия
	public onResizeTouchEnd(event: TouchEvent): void {
		this.resizerReleased();
	}

	// --------------------------------------------------------------------------
	// RESIZE
	// Обновить смещения
	public updateOffset(): void {
		const view = this.view && this.view.nativeElement;
		if (view) {
			view.scrollLeft = this.offset;
		}
	}

	// Захват разделителя
	public resizerPressed(x: number): void {
		this.resizerColumnWidthBefore = this.resizerColumn.width;
		this.resizerPosStart = x;
	}

	// Перемещение разделителя
	public resizerMove(x: number): void {
		const delta = this.resizerPosStart - x;
		const width = this.resizerColumnWidthBefore - delta;
		this.resizerColumn.width = CrmHelperService.clamp(width, this.COLUMN_MIN_SIZE);
		this.changeSize.emit(this.resizerColumn);
	}

	// Освобождение разделителя от захвата
	public resizerReleased(): void {
		this.resizerColumn = null;
		this.setCursorStyle();
		this.removeEvents();
		this.removeLinks();
	}

	// --------------------------------------------------------------------------
	// SWAP MOUSE
	// Обработчик нажатия мыши на шапку столбца
	public onSwapMouseDown(event: MouseEvent, column: IColumn): void {
		if (event && event.button === 0 && column) {
			this.swapColumn = column;
			this.swapPressed(event.clientX, event.clientY);

			// Подписка на события
			this.mouseMoveListen = this.renderer.listen(
				document,
				'mousemove',
				this.onSwapMouseMove.bind(this)
			);
			this.mouseUpListen = this.renderer.listen(
				document,
				'mouseup',
				this.onSwapMouseUp.bind(this)
			);
		}
	}

	// Обработчик перемещения мыши
	public onSwapMouseMove(event: MouseEvent): void {
		this.swapMove(event.clientX, event.clientY);
	}

	// Обработчик отжатия мыши
	public onSwapMouseUp(event: MouseEvent): void {
		this.swapReleased();
	}

	// --------------------------------------------------------------------------
	// SWAP TOUCH
	// Обработчик касания шапки столбца
	public onSwapTouchStart(event: TouchEvent, column: IColumn): void {
		const touch = event.touches[event.touches.length - 1];
		const touchTarget = (
			event.target ||
			event.srcElement ||
			event.currentTarget
		) as Element;

		if (touch && column) {
			this.touchTarget = touchTarget;
			this.swapColumn = column;
			this.swapPressed(touch.clientX, touch.clientY);

			// Подписка на события
			this.touchMoveListen = this.renderer.listen(
				this.touchTarget,
				'touchmove',
				this.onSwapTouchMove.bind(this)
			);
			this.touchEndListen = this.renderer.listen(
				this.touchTarget,
				'touchend',
				this.onSwapTouchEnd.bind(this)
			);
		}
	}

	// Обработчик перемещения
	public onSwapTouchMove(event: TouchEvent): void {
		const touch = event.touches[event.touches.length - 1];
		this.swapMove(touch.clientX, touch.clientY);
	}

	// Обработчик отжатия
	public onSwapTouchEnd(event: TouchEvent): void {
		this.swapReleased();
	}

	// --------------------------------------------------------------------------
	// SWAP
	// Захват столбца
	private swapPressed(x: number, y: number): void {
		this.swapPosStart.left = x;
		this.swapPosStart.top = y;
	}

	// Перемещение столбца
	private swapMove(x: number, y: number): void {
		if (this.swapColumn.isSwap) {
			this.updateSwapOffset(x);
		} else {
			// Расчет расстояния для срабатывания перемещения
			const offset = { left: x, top: y };
			const distance = CrmHelperService.distance(this.swapPosStart, offset);
			if (distance > this.SWAP_TRIGGER_DISTANCE) {
				// Запуск перемещения
				this.swapColumn.isSwap = true;
				this.swapPosLast = x;
				this.setCursorStyle(CursorType.Move);
				this.changeSwap.emit();
			}
		}
	}

	// Освобождение столбца от захвата
	private swapReleased(): void {
		// Завершение перемещения (если было активированно)
		if (this.swapColumn.isSwap) {
			this.swapColumn.isSwap = false;
			this.captureClick();
			this.setCursorStyle();
			this.cancelAutoScroll();
			this.changeSwap.emit();
		}
		// Базовый сброс
		this.swapColumn = null;
		this.removeEvents();
		this.removeLinks();
	}

	// Обновить смещение столбца
	private updateSwapOffset(left: number): void {
		const delta = this.swapPosLast - left;
		this.swapPosLast = left;
		this.swapColumn.offset -= delta;
		this.checkSwap();
		this.checkScroll();
	}

	// Проверка необходимости прокрутки контента
	private checkScroll(): void {
		const needStop = !this.leftScroll === !this.rightScroll;
		if (needStop || this.swapColumn.isPinned) {
			this.cancelAutoScroll();
		} else {
			this.checkRunScroll();
		}
	}

	// Проверка на необходимость запустить автопрокрутку
	private checkRunScroll(): void {
		const step = this.leftScroll > this.SCROLL_BORDER_SIZE
			? -this.SCROLL_STEP
			: this.rightScroll > this.SCROLL_BORDER_SIZE
				? this.SCROLL_STEP
				: 0;

		if (step) {
			this.scrollTimerId = this.scrollTimerId || setInterval(
				(): void => {
					this.autoScroll(step);
				},
				this.SCROLL_DELAY
			);
		}
	}

	// Автопрокрутка
	private autoScroll(step: number): void {
		this.swapColumn.offset += step;
		this.changeScroll.emit(this.offset + step);
		this.checkScroll();
	}

	// Остановить автопрокрутку
	private cancelAutoScroll(): void {
		clearInterval(this.scrollTimerId);
		this.scrollTimerId = null;
	}

	// Проверка перемещения столбцов
	private checkSwap(): void {
		const cur = this.swapColumn;
		const columns = (
				cur.isPinned
					? this.pinnedColumns
					: this.defaultColumns
			)
			.filter((value: any) => !value.isPlaceholder);

		const index = columns.indexOf(this.swapColumn);
		const prev = columns[index - 1];
		const next = columns[index + 1];
		const HALF = 0.5;

		// Перемещение влево
		if (prev && (cur.offset < prev.offset + prev.width * HALF)) {
			this.changeSwap.emit(SwapCommand.SwapLeft);

			return;
		}

		// Перемещение вправо
		if (next && (cur.offset + cur.width > next.offset + next.width * HALF)) {
			this.changeSwap.emit(SwapCommand.SwapRight);

			return;
		}

		// Проверки зоны закрепления
		if (cur.isPinned) {
			// Перемещение из зоны закрепления
			if (cur.offset + cur.width - this.PIN_BORDER_SIZE > this.pinnedCanvasWidth) {
				cur.isPinned = false;
				cur.offset = cur.offset - (this.pinnedCanvasWidth - cur.width);
				this.changeSwap.emit();

				return;
			}
		} else {
			// Перемещение в зону закрепления
			if (cur.offset < - this.PIN_BORDER_SIZE) {
				cur.isPinned = true;
				cur.offset = cur.offset + this.pinnedCanvasWidth;
				this.changeSwap.emit();

				return;
			}
		}
	}

	// Установка типа курсора для всей страницы
	private setCursorStyle(type: CursorType = CursorType.Auto): void {
		this.cursorType = type;
	}

	// Убрать обработчики
	private removeEvents(): void {
		if (this.mouseMoveListen) { this.mouseMoveListen(); }
		if (this.mouseUpListen) { this.mouseUpListen(); }
		if (this.touchMoveListen) { this.touchMoveListen(); }
		if (this.touchEndListen) { this.touchEndListen(); }
	}

	// Очистить ссылки на DOM
	public removeLinks(): void {
		this.touchTarget = null;
	}

	// Перехват и отмена события клика
	private captureClick(): void {
		const fn = (event: MouseEvent): void => {
			event.stopPropagation();
			document.removeEventListener('click', fn, true);
		};
		document.addEventListener('click', fn, true);
	}

	// --------------------------------------------------------------------------
	// HOOKS
	// Изменение входных параметров
	public ngOnChanges(changes: SimpleChanges): void {
		if (changes.offset) {
			this.updateOffset();
		}
	}

	// Уничтожение
	public ngOnDestroy(): void {
		this.cancelAutoScroll();
		this.setCursorStyle();
		this.removeEvents();
		this.removeLinks();
	}
}
