/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import * as platform from '../../common/Platform';

interface IWindowChainElement {
  readonly window: WeakRef<Window>;
  readonly iframeElement: Element | null;
}

const sameOriginWindowChainCache = new WeakMap<Window, IWindowChainElement[] | null>();

function getParentWindowIfSameOrigin(w: Window): Window | null {
  if (!w.parent || w.parent === w) {
    return null;
  }

  try {
    const location = w.location;
    const parentLocation = w.parent.location;
    if (location.origin !== 'null' && parentLocation.origin !== 'null' && location.origin !== parentLocation.origin) {
      return null;
    }
  } catch {
    return null;
  }

  return w.parent;
}

class IframeUtils {

  private static _getSameOriginWindowChain(targetWindow: Window): IWindowChainElement[] {
    let windowChainCache = sameOriginWindowChainCache.get(targetWindow);
    if (!windowChainCache) {
      windowChainCache = [];
      sameOriginWindowChainCache.set(targetWindow, windowChainCache);
      let w: Window | null = targetWindow;
      let parent: Window | null;
      do {
        parent = getParentWindowIfSameOrigin(w);
        if (parent) {
          windowChainCache.push({
            window: new WeakRef(w),
            iframeElement: w.frameElement ?? null
          });
        } else {
          windowChainCache.push({
            window: new WeakRef(w),
            iframeElement: null
          });
        }
        w = parent;
      } while (w);
    }
    return windowChainCache.slice(0);
  }

  public static getPositionOfChildWindowRelativeToAncestorWindow(childWindow: Window, ancestorWindow: Window | null): { top: number, left: number } {

    if (!ancestorWindow || childWindow === ancestorWindow) {
      return {
        top: 0,
        left: 0
      };
    }

    let top = 0;
    let left = 0;

    const windowChain = this._getSameOriginWindowChain(childWindow);

    for (const windowChainEl of windowChain) {
      const windowInChain = windowChainEl.window.deref();
      top += windowInChain?.scrollY ?? 0;
      left += windowInChain?.scrollX ?? 0;

      if (windowInChain === ancestorWindow) {
        break;
      }

      if (!windowChainEl.iframeElement) {
        break;
      }

      const boundingRect = windowChainEl.iframeElement.getBoundingClientRect();
      top += boundingRect.top;
      left += boundingRect.left;
    }

    return {
      top: top,
      left: left
    };
  }
}

export interface IMouseEvent {
  readonly browserEvent: MouseEvent;
  readonly leftButton: boolean;
  readonly middleButton: boolean;
  readonly rightButton: boolean;
  readonly buttons: number;
  readonly target: HTMLElement;
  readonly detail: number;
  readonly posx: number;
  readonly posy: number;
  readonly ctrlKey: boolean;
  readonly shiftKey: boolean;
  readonly altKey: boolean;
  readonly metaKey: boolean;
  readonly timestamp: number;

  preventDefault(): void;
  stopPropagation(): void;
}

export class StandardMouseEvent implements IMouseEvent {

  public readonly browserEvent: MouseEvent;

  public readonly leftButton: boolean;
  public readonly middleButton: boolean;
  public readonly rightButton: boolean;
  public readonly buttons: number;
  public readonly target: HTMLElement;
  public detail: number;
  public readonly posx: number;
  public readonly posy: number;
  public readonly ctrlKey: boolean;
  public readonly shiftKey: boolean;
  public readonly altKey: boolean;
  public readonly metaKey: boolean;
  public readonly timestamp: number;

  constructor(targetWindow: Window, e: MouseEvent) {
    this.timestamp = Date.now();
    this.browserEvent = e;
    this.leftButton = e.button === 0;
    this.middleButton = e.button === 1;
    this.rightButton = e.button === 2;
    this.buttons = e.buttons;

    this.target = e.target as HTMLElement;

    this.detail = e.detail ?? 1;
    if (e.type === 'dblclick') {
      this.detail = 2;
    }
    this.ctrlKey = e.ctrlKey;
    this.shiftKey = e.shiftKey;
    this.altKey = e.altKey;
    this.metaKey = e.metaKey;

    if (typeof e.pageX === 'number') {
      this.posx = e.pageX;
      this.posy = e.pageY;
    } else {
      this.posx = e.clientX + this.target.ownerDocument.body.scrollLeft + this.target.ownerDocument.documentElement.scrollLeft;
      this.posy = e.clientY + this.target.ownerDocument.body.scrollTop + this.target.ownerDocument.documentElement.scrollTop;
    }

    const iframeOffsets = IframeUtils.getPositionOfChildWindowRelativeToAncestorWindow(targetWindow, e.view);
    this.posx -= iframeOffsets.left;
    this.posy -= iframeOffsets.top;
  }

  public preventDefault(): void {
    this.browserEvent.preventDefault();
  }

  public stopPropagation(): void {
    this.browserEvent.stopPropagation();
  }
}

export interface IMouseWheelEvent extends MouseEvent {
  readonly wheelDelta: number;
  readonly wheelDeltaX: number;
  readonly wheelDeltaY: number;

  readonly deltaX: number;
  readonly deltaY: number;
  readonly deltaZ: number;
  readonly deltaMode: number;
}

interface IWebKitMouseWheelEvent {
  wheelDeltaY: number;
  wheelDeltaX: number;
}

interface IGeckoMouseWheelEvent {
  HORIZONTAL_AXIS: number;
  VERTICAL_AXIS: number;
  axis: number;
  detail: number;
}

export class StandardWheelEvent {

  public readonly browserEvent: IMouseWheelEvent | null;
  public readonly deltaY: number;
  public readonly deltaX: number;
  public readonly target: Node | null;

  constructor(e: IMouseWheelEvent | null, deltaX: number = 0, deltaY: number = 0) {

    this.browserEvent = e ?? null;
    this.target = e ? (e.target ?? (e as any).targetNode ?? e.srcElement ?? null) : null;

    this.deltaY = deltaY;
    this.deltaX = deltaX;

    let shouldFactorDPR: boolean = false;
    if (platform.isChrome) {
      const chromeVersionMatch = navigator.userAgent.match(/Chrome\/(\d+)/);
      const chromeMajorVersion = chromeVersionMatch ? parseInt(chromeVersionMatch[1], 10) : 123;
      shouldFactorDPR = chromeMajorVersion <= 122;
    }

    if (e) {
      const e1 = e as IWebKitMouseWheelEvent as any;
      const e2 = e as unknown as IGeckoMouseWheelEvent;
      const devicePixelRatio = e.view?.devicePixelRatio ?? 1;

      if (typeof e1.wheelDeltaY !== 'undefined') {
        if (shouldFactorDPR) {
          this.deltaY = e1.wheelDeltaY / (120 * devicePixelRatio);
        } else {
          this.deltaY = e1.wheelDeltaY / 120;
        }
      } else if (typeof e2.VERTICAL_AXIS !== 'undefined' && e2.axis === e2.VERTICAL_AXIS) {
        this.deltaY = -e2.detail / 3;
      } else if (e.type === 'wheel') {
        const ev = e as unknown as WheelEvent;

        if (ev.deltaMode === ev.DOM_DELTA_LINE) {
          if (platform.isFirefox && !platform.isMac) {
            this.deltaY = -e.deltaY / 3;
          } else {
            this.deltaY = -e.deltaY;
          }
        } else {
          this.deltaY = -e.deltaY / 40;
        }
      }

      if (typeof e1.wheelDeltaX !== 'undefined') {
        if (platform.isSafari && platform.isWindows) {
          this.deltaX = -(e1.wheelDeltaX / 120);
        } else if (shouldFactorDPR) {
          this.deltaX = e1.wheelDeltaX / (120 * devicePixelRatio);
        } else {
          this.deltaX = e1.wheelDeltaX / 120;
        }
      } else if (typeof e2.HORIZONTAL_AXIS !== 'undefined' && e2.axis === e2.HORIZONTAL_AXIS) {
        this.deltaX = -e.detail / 3;
      } else if (e.type === 'wheel') {
        const ev = e as unknown as WheelEvent;

        if (ev.deltaMode === ev.DOM_DELTA_LINE) {
          if (platform.isFirefox && !platform.isMac) {
            this.deltaX = -e.deltaX / 3;
          } else {
            this.deltaX = -e.deltaX;
          }
        } else {
          this.deltaX = -e.deltaX / 40;
        }
      }

      if (this.deltaY === 0 && this.deltaX === 0 && e.wheelDelta) {
        if (shouldFactorDPR) {
          this.deltaY = e.wheelDelta / (120 * devicePixelRatio);
        } else {
          this.deltaY = e.wheelDelta / 120;
        }
      }
    }
  }

  public preventDefault(): void {
    this.browserEvent?.preventDefault();
  }

  public stopPropagation(): void {
    this.browserEvent?.stopPropagation();
  }
}
