// TODO these imports will not work if @pixi is not a dependency
type PIXIContainer = import("../node_modules/@pixi/display/index").Container;
type PIXIMatrix = import("../node_modules/@pixi/math/index").Matrix;
type PIXITransform = import("../node_modules/@pixi/math/index").Matrix;
type PIXIRenderer = import("../node_modules/@pixi/core/index").Renderer;
type PIXIRenderTexture =
  import("../node_modules/@pixi/core/index").RenderTexture;
type PIXITexture = import("../node_modules/@pixi/core/index").Texture;
type PIXIGeometry = import("../node_modules/@pixi/core/index").Geometry;
type PIXIGraphics = import("../node_modules/@pixi/graphics/index").Graphics;

/**
 * A factory to create a viewer
 */
declare module "@bimdata/2d-engine" {
  export default E2D.makeViewer;
}

interface DOMMouseEvent extends MouseEvent {}

declare namespace E2D {
  function makeViewer(cfg: ViewerCfg): Viewer;

  interface ViewerCfg extends LayerOptions {
    canvas: HTMLCanvasElement;
    autoStart?: boolean;
  }

  interface Destroyable {
    destroy(): void;
  }

  interface Viewer extends Destroyable {
    readonly canvas: HTMLCanvasElement;
    readonly scene: Scene;
    readonly camera: Camera;
    readonly renderer: Renderer;
    readonly textureManager: TextureManager;
    readonly styles: { default: Style; selected: Style; highlight: Style };
    readonly settings: Settings;
    readonly constants: Constants;
    readonly picker: Picker;
    readonly snapper: Snapper;
    readonly ticker: Ticker;
    readonly ui: UI;
    readonly utils: {
      readonly vector2D: Vector2DUtils;
      makeTimedListProcessor(
        callback: Function,
        maxTimeMs?: number
      ): TimedListProcessor;
      makeScheduler(): Scheduler;
      geometry: GeometryUtils;
      svg: SvgUtils;
    };
  }

  interface SvgUtils {
    parsePath(d: string): E2D.PathData[];
    makeModelDataFromSVG(svg: SVGElement): E2D.ModelData;
  }

  interface GeometryUtils {
    geometrize(viewer: Viewer, model: Model): GeometrizedModel;
    binarize(geometrizedModel: E2D.GeometrizedModel): ArrayBuffer;
    parse(buffer: ArrayBuffer): GeometrizedModel;
    versions: Array<Object>;
    getBinary(modelData: ModelData): Promise<ArrayBuffer>;
  }

  interface UI
    extends EventHandler<UIHandlerEvents<UIMouseEvent>>,
      Destroyable {
    pick: boolean;
    translatable: boolean;
    rotatable: boolean;
    scallable: boolean;
  }

  interface HTMLElementMouseEvent extends DOMMouseEvent {
    readonly canvasPosition: Point;
    readonly position: Point;
    readonly righClick: boolean;
  }

  interface UIMouseEvent extends HTMLElementMouseEvent {
    readonly object?: SceneObject;
    readonly previousObject?: SceneObject;
    readonly mouseDownCanvasPosition?: Point;
    readonly mouseDownPosition?: Point;
  }

  interface HTMLElementUIHandler
    extends EventHandler<HTMLElementUIHandlerEvents>,
      Destroyable {
    rightClick: boolean;
    mouseDownCanvasPosition?: Point;
  }

  type HTMLElementUIHandlerEvents = UIHandlerEvents<HTMLElementMouseEvent>;

  type UIHandlerEvents<T> = {
    click: T;
    mousemove: T;
    mousedown: T;
    mouseup: T;
    wheel: T;
    contextmenu: T;
    mouseleave: T;
  };

  enum LINE_CAP {
    BUTT,
    ROUND,
    SQUARE,
  }

  enum LINE_JOIN {
    BEVEL,
    MITER,
    ROUND,
  }

  type Constants = {
    LINE_CAP: {
      BUTT: LINE_CAP;
      ROUND: LINE_CAP;
      SQUARE: LINE_CAP;
    };
    LINE_JOIN: {
      BEVEL: LINE_JOIN;
      MITER: LINE_JOIN;
      ROUND: LINE_JOIN;
    };
    DEFAULT_MODEL_ID: string;
  };

  interface Vector2DUtils {
    distance(v1: Vector2D, v2: Vector2D): number;
    sub(v1: Vector2D, v2: Vector2D): Vector2D;
    add(v1: Vector2D, v2: Vector2D): Vector2D;
    normalize(v: Vector2D): Vector2D;
    length(v: Vector2D): number;
    dot(v1: Vector2D, v2: Vector2D): number;
    cross(v1: Vector2D, v2: Vector2D): number;
    /**
     * @param angle in radians anticlockwise.
     */
    rotateAround(v: Vector2D, center: Vector2D, angle: number): Vector2D;
    /**
     * @returns { number } angle in radians anticlockwise.
     */
    angle(v1: Vector2D, v2: Vector2D): number;
  }

  interface Settings {
    dragKey: string;
    rotateKey: string;
    clickMaxOffset: number;
    scaleSpeed: number;
    scaleDeltaMax: number;
    rotateSpeed: number;
    maxScale: number;
    minScale: number;
    fitViewRatio: number;
    snapMargin: number;
    curves: {
      adaptive: boolean;
      maxLength: number;
      maxSegments: number;
      minSegments: number;
    };
  }

  interface Ticker extends EventHandler<{ tick: number }>, Scheduler {
    start(): void;
    stop(): void;
    running: boolean;
  }

  interface Scheduler {
    tasks: Map<string, Task>;
    run(...args: any[]): void;
    add(name: string, fn: Function, priority?: number, once?: boolean): void;
    addOnce(name: string, fn: Function, priority?: number): void;
    remove(name: string): boolean;
    clear(): void;
  }

  interface Task {
    name: string;
    fn: Function;
    priority: number;
    once: boolean;
  }

  interface Picker {
    pick(x: number, y: number): SceneObject | undefined;
    oncePickingReady(name: string, callback: Function): Promise<void>;
  }

  interface Snapper {
    getSnap(canvasPosition: Point, options?: SnapOptions): Point;
  }

  interface SnapOptions {
    canvasDistance?: number;
    snapVertices?: boolean;
    snapLines?: boolean;
  }

  /**
   * A camera allows to see the world. Two coordinate systems can be manipulated using it:
   * - the world coordinates, mentionned as `position`.
   * - the canvas coordinates, mentionned as `canvasPosition`.
   *
   * A camera emits events:
   * - "update", payload { MatrixExtended } : the camera transform. Emited when the camera is updated.
   *
   * To express the zoom level, the camera has two units:
   * - zoom, expressed in percentages, as 100 times the scale.
   * - scale ]0,+Infitiy[.
   *
   */
  interface Camera
    extends Transformable,
      Positionable,
      Readonly<EventHandler<{ update: PIXITransform }>>,
      Destroyable {
    fitView(
      target: Scene | Scene[] | Model | Model[] | SceneObject | SceneObject[]
    ): void;
    /**
     * @param factor ]-Infinity, +Infinity[
     * @param origin the zoom transform center.
     */
    zoomIn(factor?: number, origin?: Point): void;
    /**
     * @param factor ]-Infinity, +Infinity[
     * @param origin the zoom transform center.
     */
    zoomOut(factor?: number, origin?: Point): void;
    /**
     * @param angle in degree clockwise.
     * @param origin the rotation transform center.
     */
    transform: PIXIMatrix;
    position: Point;
    /**
     * @returns { number } the camera rotation angle in degree clockwise.
     */
    rotation: number;
    /**
     * @returns { number } ]0, +Infinity[
     */
    getScale(): number;
    /**
     * @returns { number } ]0, +Infinity[
     */
    zoom: number;
    /**
     * Returns the position of the given canvas position.
     */
    getPosition(canvasPosition: Point): Point;
    /**
     * Returns the canvas position of the given position.
     */
    getCanvasPosition(position: Point): Point;
    getViewpoint(): Viewpoint;
    setViewpoint(viewpoint: Viewpoint): Viewpoint;
  }

  interface Viewpoint {
    x: number;
    y: number;
    /**
     * degree clockwise
     */
    rotation: number;
    /**
     * ]-Infinity, +Infinity[
     */
    zoom: number;
  }

  interface Point {
    x: number;
    y: number;
  }

  interface ReactivePoint extends Point, ReactiveEntity {
    index: number;
  }

  interface Vector2D extends Point {}

  type Events = {
    [eventName: string]: Object;
  };

  interface EventHandler<T extends Events> {
    on<N extends keyof T>(eventName: N, callback: (arg: T[N]) => void): number;
    once<N extends keyof T>(
      eventName: N,
      callback: (arg: T[N]) => void
    ): number;
    off(subscriptionId: number): void;
    emit<N extends keyof T>(eventName: N, payload: T[N]): void;
    clear(): void;
  }

  interface Typed<T> {
    type: T;
  }

  enum EntityType {
    scene = "scene",
    model = "model",
    object = "object",
    shape = "shape",
    geometry = "geometry",
  }

  interface Scene
    extends Readonly<EventHandler<SceneEvents>>,
      Typed<EntityType.scene>,
      Positionable {
    dirty: boolean;
    cache: Object;
    readonly viewer: Viewer;
    generateReport(): SceneReport;
    // Models
    readonly orphanObjectsModel: Model;
    readonly models: Model[];
    readonly modelsMap: Map<string, Model>;
    addModel(modelData: ModelData): Promise<Model>;
    removeModel(id: string): boolean;
    _removedModels: Model[];
    // Objects
    readonly objects: SceneObject[];
    readonly objectsMap: Map<string, SceneObject>;
    readonly pickingIds: Map<number, SceneObject>;
    addObject(objectData: SceneObjectData, model?: Model): SceneObject;
    removeObject(objectId: string): boolean;
    _shapeId: number;
    _geometryId: number;
    // Objects setters
    showObjects(ids: string[]): void;
    hideObjects(ids: string[]): void;
    selectObjects(ids: string[]): void;
    deselectObjects(ids: string[]): void;
    highlightObjects(ids: string[]): void;
    unhighlightObjects(ids: string[]): void;
    setObjectsPickable(ids: string[]): void;
    setObjectsUnpickable(ids: string[]): void;
    // Object getters
    readonly shownObjects: SceneObject[];
    readonly hiddenObjects: SceneObject[];
    readonly selectedObjects: SceneObject[];
    readonly unselectedObjects: SceneObject[];
    readonly highlightedObjects: SceneObject[];
    readonly unhighlightedObjects: SceneObject[];
    readonly pickableObjects: SceneObject[];
    readonly unpickableObjects: SceneObject[];
  }

  type SceneEvents = {
    "model-added": Model;
    "model-removed": Model;
    "object-added": SceneObject;
    "object-removed": SceneObject;
    "object-update": {
      object: SceneObject;
      property: keyof SceneStateObjectData | keyof Style | "shapes";
      value?: number | Array<number> | string;
      oldValue?: number | Array<number> | string;
    };
  };

  interface ModelData {
    readonly id?: string;
    readonly objects?: SceneObjectData[];
    readonly textures?: TextureData[];
  }

  interface TextureData {
    name: string;
    uri: string;
    width?: number;
    height?: number;
    resolution?: number;
  }

  /**
   * A model is where objects live.
   */
  interface Model extends Transformable, Positionable, Typed<EntityType.model> {
    readonly id: string;
    dirty: boolean;
    readonly scene: Scene;
    addObject(objectData: SceneObjectData): SceneObject;
    removeObject(objectId: string): boolean;
    readonly objects: SceneObject[];
  }

  interface GeometrizedModel {
    readonly id: string;
    objects: GeometrizedObject[];
    textures: TextureData[];
  }

  interface Translate {
    dx: number;
    dy: number;
  }

  interface SceneStateObjectData {
    visible: boolean;
    selected: boolean;
    highlighted: boolean;
    pickable: boolean;
  }

  interface SceneObjectData extends SceneStateObjectData {
    readonly id?: string;
    readonly zIndex?: number;
    readonly pickingZIndex?: number;
    readonly shapes?: ShapeData[];
    readonly texts?: Text[];
    readonly geometries?: Geometry[];
  }

  interface SceneObject
    extends Transformable,
      Positionable,
      Typed<EntityType.object> {
    readonly scene: Scene;
    readonly model: Model;
    readonly id: string;
    readonly readonly?: boolean;
    readonly zIndex: number;
    readonly pickingId: number;
    readonly pickingZIndex: number;
    readonly pickingMargin: number;
    readonly shapes: Shape[];
    getShape(index?: number): Shape;
    addShape(shapeData: ShapeData, index?: number): Shape;
    removeShape(index?: number): Shape;
    readonly texts?: Text[];
    getText(index?: number): Text;
    addText(text: Text, index?: number): Text;
    removeText(index?: number): Text;
    readonly geometries?: Geometry[];
    flags: number;
  }

  type SceneObjectFactory = (
    objectData: SceneObjectData,
    model: Model
  ) => SceneObject;

  interface GeometrizedObject {
    id: string;
    zIndex: number;
    pickingZIndex: number;
    pickable: boolean;
    texts: Text;
    readonly: boolean;
    aabb: AABB;
    geometries: GeometryData[];
  }

  interface GeometryData {
    type: "line" | "fill";
    points: number[];
    indices: number[];
    color: number | string;
    alpha: number;
    native?: boolean;
    readonly aabb: AABB;
  }

  interface Geometry extends GeometryData, Positionable {
    readonly id: number;
    readonly object: SceneObject;
    readonly style: Style; // simplified style with only `lineColor`, `lineOpacity`, `fill` & `fillOpacity`
  }

  interface Drawable {
    draw(
      graphics: PIXIGraphics,
      translate: E2D.Translate,
      lineSegments?: number[],
      startPath?: boolean
    ): void;
  }

  interface Shape extends ReactiveEntity, Positionable {
    readonly id: number;
    readonly type: EntityType.shape;
    readonly object: SceneObject;
    readonly binary?: DataView;
    readonly paths?: Path[];
    readonly style: Style;
    getPath(index?: number): Path | undefined;
    addPath(path: PathData, index?: number): Path;
    removePath(index?: number): Path | undefined;
  }

  interface ShapeData {
    paths: Array<PathData>;
    style: Style;
  }

  interface PathDataAbstract {
    type?: PathType;
    close?: boolean;
    start?: boolean;
  }

  type PathData = LineData | ArcData | CurveData;

  interface LineData extends PathDataAbstract {
    type: PathType.line;
    points: number[];
  }

  interface Line extends Path {
    readonly type: PathType.line;
    cache: LineCache;
    readonly points: Point[];
    getPoint(index?: number): Point | undefined;
    addPoint(point: Point, index?: number): Point;
    removePoint(index?: number): Point | undefined;
  }

  interface LineCache {
    aabb?: AABB;
    center?: Point;
  }

  interface ArcData extends PathDataAbstract {
    type: PathType.arc;
    x: number;
    y: number;
    radius: number;
    startAngle?: number; // (0 is at the 3 o'clock position of the arc's circle)
    endAngle?: number;
    anticlockwise?: boolean;
  }

  interface Arc extends Path {
    readonly type: PathType.arc;
    cache: ArcCache;
    x: number;
    y: number;
    radius: number;
    startAngle: number;
    endAngle: number;
    anticlockwise?: boolean;
  }

  interface ArcCache {
    aabb?: AABB;
  }

  interface CurveData extends PathDataAbstract {
    type: PathType.curve;
    x: number;
    y: number;
    toX: number;
    toY: number;
    cpX1: number;
    cpY1: number;
    cpX2: number;
    cpY2: number;
  }

  interface Curve extends Path {
    readonly type: PathType.curve;
    cache: CurveCache;
    x: number;
    y: number;
    /**
     * The x coordinate of the first control point.
     */
    cpX1: number;
    /**
     * The y coordinate of the first control point.
     */
    cpY1: number;
    /**
     * The x coordinate of the second control point.
     */
    cpX2: number;
    /**
     * The y coordinate of the second control point.
     */
    cpY2: number;
    toX: number;
    toY: number;
  }

  interface CurveCache {
    aabb?: AABB;
    center?: Point;
  }

  enum PathType {
    line = "line",
    arc = "arc",
    curve = "curve",
  }

  interface Path extends ReactiveEntity, Drawable, Positionable, Transformable {
    readonly type: PathType;
    getAABB(): AABB;
    getCenter(): Point;
    cache: any;
    readonly close: boolean;
    readonly start: boolean;
  }

  type AABB = [minX: number, minY: number, maxX: number, maxY: number];

  interface Positionable {
    readonly aabb: AABB;
    readonly center: Point;
  }

  interface Transformable {
    move(position: Point): void;
    translate(dx: number, dy: number): void;
    /**
     * @param angle in degree, clockwise
     * @param origin
     */
    rotate(angle: number, origin?: Point): void;
    scale(ds: number, origin?: Point): void;
  }

  interface ReactiveEntity {
    onUpdate?(cfg: any): void;
  }

  interface Style {
    // FILL
    fill?: number | string; // number for hex color, string for a registered texture name
    fillOpacity?: number;
    // LINE
    lineWidth?: number;
    lineColor?: number;
    lineOpacity?: number;
    lineDash?: number[];
    lineCap?: LINE_CAP;
    lineJoin?: LINE_JOIN;
    nonScalingLineWidth?: boolean;
    performanceLine?: boolean;
  }

  interface TextureManager extends Destroyable {
    modelTextures: Map<string, Map<string, NamedPIXITexture>>;
    textureMatrix: TextureMatrix;
    load(modelId: string, texturesData: TextureData[]): Promise<void>;
    unload(modelId: string): boolean;
  }

  interface NamedPIXITexture extends PIXITexture {
    name: string;
  }

  interface TextureMatrix
    extends Readonly<
      EventHandler<{
        update: { rotate?: number; scale?: Point; translate?: Point };
      }>
    > {
    readonly matrix: PIXIMatrix;
    rotate(angle: number): void;
    scale(x: number, y?: number): void;
    translate(x: number, y: number): void;
  }

  interface TextStyle {
    fill: string;
    fontFamily: string | string[];
    fontSize: number | string;
    fontStyle: string;
    fontWeight: string;
    lineHeight: number;
  } // other properties availables https://pixijs.download/v6.0.3/docs/PIXI.TextStyle.html

  interface Text extends TextStyle {
    content?: string;
    x?: number;
    y?: number;
    anchor?: [number, number];
    rotation: number;
    scale: number;
  }

  interface SceneReport {
    addedObjects?: SceneObject[];
    removedObjects?: SceneObject[];
    visibleObjects?: SceneObject[];
    selectedObjects?: SceneObject[];
    highlightedObjects?: SceneObject[];
    pickableObjects?: SceneObject[];
    stylizedObjects?: SceneObject[];
    shapedObjects?: SceneObject[];
    textObjects?: SceneObject[];
  }

  interface Renderer extends Readonly<EventHandler<RendererEvents>> {
    readonly viewer: Viewer;
    readonly renderer: PIXIRenderer;
    transform: PIXIMatrix;
    /**
     * @param { boolean } [force=false] The Renderer does not render each frame if nothing change. If `true`, it redraws the in memory shapes on the canvas.
     * @param { boolean } [clear=false] Clear the in memory shapes to redraw from scratch.
     */
    render(force?: boolean, clear?: boolean): void;
    transformUpdated: boolean;
    offset: Translate;
    readonly layers: Map<string, Layer>;
    resize(): void;
    makeShapeDrawer(
      style: Style,
      textures?: Map<string, NamedPIXITexture>,
      textureMatrix?: PIXIMatrix
    ): Drawer;
    makeShapePickingDrawer(
      snap?: boolean,
      scaleDependant?: boolean
    ): ShapePickingDrawer;
    makeGeometryDrawer(
      style: Style,
      textures?: Map<string, NamedPIXITexture>,
      textureMatrix?: PIXIMatrix
    ): Drawer;
    geometrize(model: Model): GeometrizedModel;
  }

  type RendererEvents = {
    resize: { dx: number; dy: number };
    "pre-draw": undefined;
    "post-draw": undefined;
  };

  interface Drawer extends Destroyable {
    displayObject: PIXIContainer;
    clear(): void;
    draw(elements: Shape[] | Geometry[]): void;
    readonly scaleDependant: boolean;
    readonly initialLineWidth?: number;
    lineWidth?: number;
  }

  interface PickingDrawer extends Drawer {
    registerOnceDrawn(name: string, callback: Function): void;
  }

  interface ShapePickingDrawer extends PickingDrawer {}

  interface Layer extends Destroyable {
    name: string;
    visible: boolean;
    renderer: Renderer;
    renderTexture: PIXIRenderTexture;
    stage: PIXIContainer;
    resize(width: number, height: number): void;
    addObjects(objects: SceneObject[]): void;
    updateObjects(objects: SceneObject[]): void;
    removeObjects(objects: SceneObject): void;
    render(forceDirty?: boolean): void;
    dirty: boolean;
    clear(): void;
  }

  interface PickingLayer extends Layer {
    pickColorAtCanvasPosition(
      position: Point,
      snap?: boolean
    ): number | undefined;
    registerOnceDrawn(name: string, callback: Function): void;
  }

  interface BatchLayer extends Layer {
    objects: SceneObject[];
    stage: DirtyableContainer;
    batches: Batch[];
    render(forceDirty?: boolean, matrix?: PIXIMatrix): void;
  }

  interface LayerOptions {
    select?: boolean;
    highlight?: boolean;
    picking?: boolean;
    showPicking?: boolean;
    snap?: boolean;
    showSnap?: boolean;
    text?: boolean;
  }

  interface LayerHandlers {
    handleAddedObjects(): void;
    handleRemovedObjects(): void;
    handleUpdatedObjects(): void;
  }

  interface DirtyableContainer extends PIXIContainer {
    dirty: boolean;
  }

  interface Batch extends Destroyable {
    readonly drawer: Drawer;
    dirty: boolean;
    addEntities(entities: any[]): void;
    removeEntities(ids: number[]): void;
    entities: Map<number, any>;
  }

  interface EntityBatch<T> extends Batch {
    type: EntityType;
    modelId: string;
    key: string;
    zIndex: number;
    entities: Map<number, T>;
    addEntity(entities: T[]): void;
  }

  interface GeometryBatch extends EntityBatch<Geometry> {
    type: EntityType.geometry;
  }
  interface ShapeBatch extends EntityBatch<Shape> {
    type: EntityType.shape;
  }

  interface TimedListProcessor {
    readonly done: boolean;
    list: any[];
    start(list: any[], onceDone?: Function): void;
    stop(): void;
  }
}
