import { AfterViewInit, ChangeDetectionStrategy, Component, computed, effect, ElementRef, inject, input, OnDestroy, output, signal, TemplateRef, ViewChild, } from '@angular/core';
import * as util from '../utils/util';
import { FAST_KENDO_COMMON } from '../../app.config';
import { FastButtonComponent } from "./fast-button.component";
import { FastLabelComponent } from "./fast-label.component";
import { FastSVGComponent } from './fast-svg.component';
import { CdkDrag, CdkDragHandle, CdkDragEnd } from '@angular/cdk/drag-drop';
import { FastTextAreaComponent } from './fast-textarea.component';
import { NgTemplateOutlet } from '@angular/common';
import { ToastService } from '../services/fast-toast.service';

type WindowType = 'basic' | 'error' | 'info';
type WindowState = 'normal' | 'minimized' | 'maximized';

@Component({
  selector: 'fast-window',
  imports: [FAST_KENDO_COMMON, CdkDrag, CdkDragHandle, FastButtonComponent, FastLabelComponent, FastSVGComponent, FastTextAreaComponent, NgTemplateOutlet],
  template: `
    <dialog #dialog cdkDrag [cdkDragDisabled]="windowState() === 'maximized'" (cdkDragEnded)="onDragEnd($event)"
    [style.height]="useAutoHeight() ? 'fit-content' : currentHeight() + 'px'"
    [style.width]="useAutoWidth() ? 'fit-content' : currentWidth() + 'px'"
    [style.top.px]="currentTop()"
    [style.left.px]="currentLeft()"
    class="
    bg-base-white-500
    dark:bg-alt-gray-500
    inset-auto overflow-hidden flex flex-col p-0 border-0 rounded-xl shadow-lg shadow-black z-2 max-w-none max-h-none">
      <header #windowTitlebar cdkDragHandle [cdkDragHandleDisabled]="windowState() === 'maximized'"
      class="m-0 p-0 items-center justify-center border-none h-8.5 select-none" [class]="conditionalClasses()" [class.cursor-move]="windowState() !== 'maximized'" [class.cursor-pointer]="windowState() === 'maximized'">
        <div class="max-w-full flex flex-row grow basis-full pl-1">
            @if(titlebarContent()){
              <ng-container *ngTemplateOutlet="titlebarContent()" />
            }
            @else {
              <div class="overflow-hidden flex grow-0 items-start justify-center mt-0 mr-1 mb-0 ml-2">
                <fast-label class="grow truncate text-lg">{{ title() }}</fast-label>
              </div>
              <div class="flex grow gap-1">
                @if(type() === 'info' || type() === 'error'){
                  <fast-button class="w-fit" (click)="copyInfo()" [tabIndex]="-1">
                    <fast-svg icon="copy" />
                  </fast-button>
                }
                @if(hasRefresh()) {
                <fast-button class="w-fit" (click)="refresh.emit()" [tabIndex]="-1">
                  <fast-svg icon="refresh" />
                </fast-button>
                }
                @if(hasPrint()) {
                <fast-button class="w-fit" (click)="print.emit()" [tabIndex]="-1">
                  <fast-svg icon="print" />
                </fast-button>
                }
              </div>
            }
          <div class="titlebar-actions flex flex-row absolute right-0">
            @let isNormalState = windowState() === 'normal';
            @let isMinOrMaxState = windowState() === 'minimized' || windowState() === 'maximized';
            @if (type() === 'basic' && hasMinMaxActions() && isNormalState) {
              <fast-button class="w-10 h-8.5" type="transparent" (click)="minimize()" [tabIndex]="-1"><fast-svg icon="minus" /></fast-button>
              <fast-button class="w-10 h-8.5" type="transparent" (click)="maximize()" [tabIndex]="-1"><fast-svg icon="maximize" /></fast-button>
            }
            @else if (type() === 'basic' && hasMinMaxActions() && isMinOrMaxState) {
              <fast-button class="w-10 h-8.5" type="transparent" (click)="restoreSize()" [tabIndex]="-1"><fast-svg icon="restore" /></fast-button>
            }
            <fast-button class="w-10 h-8.5 border-0 rounded-xl" type="transparent" (click)="closeWindow()" [tabIndex]="-1"><fast-svg icon="x" /></fast-button>
          </div>
        </div>
      </header>
      <div class="flex flex-col grow overflow-hidden" #content [hidden]="type() !== 'basic'">
        <ng-content class="flex flex-col grow"></ng-content>
      </div>
      @if (type() === 'error' || type() === 'info') {
        <div class="flex flex-col grow overflow-hidden m-3">
          <fast-textarea class="grow" #copyInfoElem [readonly]="true" [value]="textContent()"/>
          <fast-button class="w-full mt-2" (click)="close.emit()">OK</fast-button>
        </div>
      }

      @if(windowState() === 'normal'){
        <div class="absolute z-10 resize-e" (mousedown)="startResize($event, 'e')"></div>
        <div class="absolute z-10 resize-s" (mousedown)="startResize($event, 's')"></div>
        <div class="absolute z-10 resize-w" (mousedown)="startResize($event, 'w')"></div>
        <div class="absolute z-10 resize-n" (mousedown)="startResize($event, 'n')"></div>
        <div class="absolute z-10 resize-se" (mousedown)="startResize($event, 'se')"></div>
        <div class="absolute z-10 resize-sw" (mousedown)="startResize($event, 'sw')"></div>
        <div class="absolute z-10 resize-ne" (mousedown)="startResize($event, 'ne')"></div>
        <div class="absolute z-10 resize-nw" (mousedown)="startResize($event, 'nw')"></div>
      }
    </dialog>
  `,
  styles: `
    :host {
      top: 0;
      bottom: 0;
      left: 0;
      right: 0;
      z-index: auto;
      transition: all 0.3 ease-in-out;
    }

    dialog::backdrop {
      background: rgba(0, 0, 0, 0.4);
    }

    dialog:focus-visible {
      outline: none;
    }

    .resize-e, .resize-w {
      width: 8px;
      top: 0;
      bottom: 0;
      cursor: ew-resize;
    }

    .resize-e { right: 0; }
    .resize-w { left: 0; }

    .resize-n, .resize-s {
      height: 8px;
      left: 0;
      right: 0;
      cursor: ns-resize;
    }

    .resize-n { top: 0; }
    .resize-s { bottom: 0; }

    .resize-se, .resize-sw, .resize-ne, .resize-nw {
      width: 12px;
      height: 12px;
    }

    .resize-se {
      bottom: 0;
      right: 0;
      cursor: nwse-resize;
    }

    .resize-sw {
      bottom: 0;
      left: 0;
      cursor: nesw-resize;
    }

    .resize-ne {
      top: 0;
      right: 0;
      cursor: nesw-resize;
    }

    .resize-nw {
      top: 0;
      left: 0;
      cursor: nwse-resize;
    }
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class FastWindowComponent implements AfterViewInit, OnDestroy {
  @ViewChild('dialog') dialog!: ElementRef<HTMLDialogElement>;
  @ViewChild('windowTitlebar') windowTitlebar!: ElementRef<HTMLHeadElement>;
  @ViewChild(CdkDrag, { static: true }) drag!: CdkDrag;
  @ViewChild('content') content!: ElementRef<HTMLDivElement>;
  @ViewChild("copyInfoElem") copyInfoElem: FastTextAreaComponent;

  // eslint-disable-next-line @angular-eslint/no-output-native
  resize = output<{ width: number; height: number }>();

  constructor() {
    effect(() => {
      this.resize.emit({ width: this.currentWidth(), height: this.currentHeight() });
    })
  }

  ngAfterViewInit() {
    const dialog = this.dialog.nativeElement;
    const windowTitlebar = this.windowTitlebar.nativeElement;
    this.textContent.set(this.content?.nativeElement?.textContent || '');

    dialog.addEventListener('toggle', this.onToggle);
    window.addEventListener('resize', this.onViewportResize);
    dialog.addEventListener('close', () => this.closeWindow());
    dialog.showModal();
    dialog.focus(); //focus the dialog itself so that the minimize button isn't autofocused when the dialog opens

    // event to check if clicking outside of window closes it
    // listen to mousedown instead of click so that clicking down within a window and then releasing outside does not close
    dialog.addEventListener('mousedown', (e: MouseEvent) => {
      if (this.isResizing || e.target !== dialog) { return; }
      const rect = dialog.getBoundingClientRect();
      const isInDialog =
        rect.top <= e.clientY &&
        e.clientY <= rect.top + rect.height &&
        rect.left <= e.clientX &&
        e.clientX <= rect.left + rect.width;
      if (!isInDialog) {
        this.closeWindow();
      }
    })

    windowTitlebar.addEventListener('dblclick', () => {
      if (!this.hasMinMaxActions()) { return; }
      if (this.windowState() === 'normal') {
        this.maximize();
      }
      else {
        this.restoreSize();
      }
    })

    // Initialize current dimensions with viewport clamping
    let initWidth = this.width() || this.minWidth();
    let initHeight = this.height() || this.minHeight();

    // clamp width/height so it fits inside viewport
    initWidth = Math.min(initWidth, window.innerWidth - 40);
    initHeight = Math.min(initHeight, window.innerHeight - 40);

    this.currentWidth.set(initWidth);
    this.currentHeight.set(initHeight);

    // center the window safely
    const top = Math.max(0, (window.innerHeight - initHeight) / 2);
    const left = Math.max(0, (window.innerWidth - initWidth) / 2);
    this.currentTop.set(top);
    this.currentLeft.set(left);

    if (this.state() === 'maximized')
      this.maximize();
    else if (this.state() === 'minimized')
      this.minimize();
  }

  ngOnDestroy() {
    this.dialog?.nativeElement?.removeEventListener('close', () => this.closeWindow());
    window.removeEventListener('resize', this.onViewportResize);
    this.removeResizeListeners();
  }

  toast = inject(ToastService);

  type = input<WindowType>('basic');
  layerLevel = input<number>(1);
  minHeight = input<number>(350);
  minWidth = input<number>(350);
  height = input<number>(null);
  width = input<number>(null);
  title = input<string>('');
  state = input<WindowState>('normal');
  titlebarContent = input<TemplateRef<unknown> | null>(null);
  refresh = output();
  print = output();
  hasRefresh = util.hasOutputListener(this.refresh);
  hasPrint = util.hasOutputListener(this.print);
  hasMinMaxActions = input(true);
  hasCloseAction = input(true);
  windowState = signal<WindowState>('normal');
  textContent = signal<string>('');

  // eslint-disable-next-line @angular-eslint/no-output-native
  close = output();

  // resize variables
  currentHeight = signal<number>(350);
  currentWidth = signal<number>(300);
  currentTop = signal<number | null>(null);
  currentLeft = signal<number | null>(null);
  useAutoHeight = signal<boolean>(false);
  useAutoWidth = signal<boolean>(false);
  private isResizing = false;
  private resizeDirection = '';
  private startX = 0;
  private startY = 0;
  private startWidth = 0;
  private startHeight = 0;

  conditionalClasses = computed(() => {
    const classes = [] as string[];

    classes.push(...this.getLightBaseClasses(this.type()));
    classes.push(...this.getDarkBaseClasses(this.type()));

    const conditionalClasses = this.getConditionalClassesFromArrays(classes);
    return conditionalClasses;
  });

  getConditionalClassesFromArrays(classArray: string[]): { [key: string]: boolean } {
    const classes: { [key: string]: boolean } = {};
    classArray.forEach(className => {
      classes[className] = true;
    });
    return classes;
  }

  getLightBaseClasses(type: WindowType) {
    if (type == 'basic' || type == 'info')
      return ["bg-gradient-to-b", "from-base-white-1000", "via-base-white-1000", "to-base-white-250"];

    if (type == 'error')
      return ["bg-gradient-to-b", "from-base-red-1000", "via-base-red-250", "to-base-white-250"];
  }

  getDarkBaseClasses(type: WindowType) {
    if (type == 'basic' || type == 'info')
      return ["dark:bg-gradient-to-b", "dark:from-alt-gray-1000", "dark:via-alt-gray-1000", "dark:to-alt-gray-500"];

    if (type == 'error')
      return ["dark:bg-gradient-to-b", "dark:from-alt-red-1000", "dark:via-alt-red-250", "dark:to-alt-blue-750"];
  }

  async copyInfo() {
    this.copyInfoElem.textAreaElement.nativeElement.select();
    const selection = window.getSelection()?.toString();
    if (selection)
      await navigator.clipboard.writeText(selection);
    this.toast.success('copied');
  }

  private startTop = 0;
  private startLeft = 0;

  startResize(event: MouseEvent, direction: string) {
    if (this.windowState() === 'maximized') { return; }
    event.preventDefault();
    event.stopPropagation();

    this.isResizing = true;
    this.resizeDirection = direction;
    this.startX = event.clientX;
    this.startY = event.clientY;

    const dialog = this.dialog.nativeElement;
    const rect = dialog.getBoundingClientRect();
    this.startWidth = rect.width;
    this.startHeight = rect.height;
    this.startTop = this.currentTop();
    this.startLeft = this.currentLeft();

    document.addEventListener('mousemove', this.onResizeMove);
    document.addEventListener('mouseup', this.onResizeEnd);
  }

  private onResizeMove = (event: MouseEvent) => {
    if (!this.isResizing) return;

    const deltaX = event.clientX - this.startX;
    const deltaY = event.clientY - this.startY;

    let newWidth = this.startWidth;
    let newHeight = this.startHeight;
    let newTop = this.startTop;
    let newLeft = this.startLeft;

    if (this.resizeDirection.includes('e')) {
      newWidth = Math.max(this.minWidth(), this.startWidth + deltaX);
    }
    if (this.resizeDirection.includes('w')) {
      const proposedWidth = this.startWidth - deltaX;
      if (proposedWidth >= this.minWidth()) {
        newWidth = proposedWidth;
        newLeft = this.startLeft + deltaX;
      }
      else {
        newWidth = this.minWidth();
        newLeft = this.startLeft + (this.startWidth - this.minWidth());
      }
    }

    if (this.resizeDirection.includes('s')) {
      newHeight = Math.max(this.minHeight(), this.startHeight + deltaY);
    }
    if (this.resizeDirection.includes('n')) {
      const proposedHeight = this.startHeight - deltaY;
      if (proposedHeight >= this.minHeight()) {
        newHeight = proposedHeight;
        newTop = this.startTop + deltaY;
      }
      else {
        newHeight = this.minHeight();
        newTop = this.startTop + (this.startHeight - this.minHeight());
      }
    }

    this.currentWidth.set(newWidth);
    this.currentHeight.set(newHeight);
    this.currentTop.set(newTop);
    this.currentLeft.set(newLeft);
  }

  private onResizeEnd = () => {
    setTimeout(() => {
      this.isResizing = false;
      this.removeResizeListeners();
    }, 10); // delay to prevent immediate re-triggering of click events outside of bounding box
  };

  private removeResizeListeners() {
    document.removeEventListener('mousemove', this.onResizeMove);
    document.removeEventListener('mouseup', this.onResizeEnd);
  }

  private onViewportResize = () => {
    const maxWidth = window.innerWidth - 40;
    const maxHeight = window.innerHeight - 40;

    const newWidth = Math.max(this.minWidth(), Math.min(this.currentWidth(), maxWidth));
    const newHeight = Math.max(this.minHeight(), Math.min(this.currentHeight(), maxHeight));

    this.currentWidth.set(newWidth);
    this.currentHeight.set(newHeight);

    const top = Math.max(0, Math.min(this.currentTop(), window.innerHeight - newHeight));
    const left = Math.max(0, Math.min(this.currentLeft(), window.innerWidth - newWidth));

    this.currentTop.set(top);
    this.currentLeft.set(left);

    if (this.windowState() === 'maximized') {
      this.maximize();
    }
  };

  // maximize / minimize methods + variables
  protected storedHeight: number = null;
  protected storedWidth: number = null;
  protected storedTop: number = null;
  protected storedLeft: number = null;


  protected minimize() {
    this.storedHeight = this.currentHeight();
    this.storedWidth = this.currentWidth();
    this.storedTop = this.currentTop();
    this.storedLeft = this.currentLeft();
    this.currentHeight.set(32); // about the height of the titlebar

    this.windowState.set('minimized');
  }

  protected maximize() {
    this.storedHeight = this.currentHeight();
    this.storedWidth = this.currentWidth();
    this.storedTop = this.currentTop();
    this.storedLeft = this.currentLeft();

    this.currentTop.set(0);
    this.currentLeft.set(0);
    this.currentHeight.set(window.innerHeight);
    this.currentWidth.set(window.innerWidth);

    this.windowState.set('maximized');


    if (this.drag && this.dialog?.nativeElement)
      this.drag.reset();
  }

  protected restoreSize() {
    if (this.storedHeight && this.storedWidth) {
      this.currentHeight.set(this.storedHeight);
      this.currentWidth.set(this.storedWidth);
    }

    this.currentTop.set(this.storedTop);
    this.currentLeft.set(this.storedLeft);

    this.storedHeight = null;
    this.storedWidth = null;
    this.storedTop = null;
    this.storedLeft = null;

    this.windowState.set('normal');
  }

  onDragEnd(event: CdkDragEnd) {
    const transform = event.source.getFreeDragPosition();
    this.currentLeft.update(prev => (prev ?? 0) + transform.x);
    this.currentTop.update(prev => (prev ?? 0) + transform.y);

    // Reset transform since we’ve applied it to absolute positioning
    event.source.reset();
  }

  private onToggle = (e: ToggleEvent) => {
    if (e.newState === 'open')
      this.autoFocus();
  }

  private autoFocus() {
    setTimeout(() => {
      const contentEl = this.content?.nativeElement;
      if (!contentEl) return;

      // find first naturally focusable element in content
      const firstFocusable = contentEl.querySelector<HTMLElement>(`
        input:not([disabled]):not([tabindex="-1"]),
        select:not([disabled]):not([tabindex="-1"]),
        textarea:not([disabled]):not([tabindex="-1"])
      `);

      firstFocusable?.focus();
    }, 250);
  }

  protected closeWindow() {
    this.close.emit();
  }

}
