// Copyright (C) 2023 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import m from 'mithril';
import * as vega from 'vega';
import * as vegaLite from 'vega-lite';
import {getErrorMessage} from '../../base/errors';
import {isString, shallowEquals} from '../../base/object_utils';
import {SimpleResizeObserver} from '../../base/resize_observer';
import {Engine} from '../../trace_processor/engine';
import {QueryError} from '../../trace_processor/query_result';
import {Spinner} from '../../widgets/spinner';

function isVegaLite(spec: unknown): boolean {
  if (typeof spec === 'object') {
    const schema = (spec as {$schema: unknown})['$schema'];
    if (schema !== undefined && isString(schema)) {
      // If the schema is available use that:
      return schema.includes('vega-lite');
    }
  }
  // Otherwise assume vega-lite:
  return true;
}

// Vega-Lite specific interactions
// types (https://vega.github.io/vega-lite/docs/selection.html#select)
export enum VegaLiteSelectionTypes {
  INTERVAL = 'interval',
  POINT = 'point',
}

// Vega-Lite Field Types
// These are for axis field (data) types
// https://vega.github.io/vega-lite/docs/type.html
export type VegaLiteFieldType =
  | 'quantitative'
  | 'temporal'
  | 'ordinal'
  | 'nominal'
  | 'geojson';

// Vega-Lite supported aggregation operations
// https://vega.github.io/vega-lite/docs/aggregate.html#ops
export type VegaLiteAggregationOps =
  | 'count'
  | 'valid'
  | 'values'
  | 'missing'
  | 'distinct'
  | 'sum'
  | 'product'
  | 'mean'
  | 'average'
  | 'variance'
  | 'variancep'
  | 'stdev'
  | 'stdevp'
  | 'stderr'
  | 'median'
  | 'q1'
  | 'q3'
  | 'ci0'
  | 'ci1'
  | 'min'
  | 'max'
  | 'argmin'
  | 'argmax';

export type VegaEventType =
  | 'click'
  | 'dblclick'
  | 'dragenter'
  | 'dragleave'
  | 'dragover'
  | 'keydown'
  | 'keypress'
  | 'keyup'
  | 'mousedown'
  | 'mousemove'
  | 'mouseout'
  | 'mouseover'
  | 'mouseup'
  | 'mousewheel'
  | 'touchend'
  | 'touchmove'
  | 'touchstart'
  | 'wheel';

export interface VegaViewData {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  [name: string]: any;
}

type VegaSignalListenerHandler = (args: {
  view: vega.View;
  name: string;
  value: vega.SignalValue;
}) => void;
type VegaEventListenerHandler = (args: {
  view: vega.View;
  event: vega.ScenegraphEvent;
  item?: vega.Item | null;
}) => void;

interface VegaViewAttrs {
  spec: string;
  data: VegaViewData;
  engine?: Engine;
  signalHandlers?: {
    readonly name: string;
    readonly handler: VegaSignalListenerHandler;
  }[];
  eventHandlers?: {
    readonly name: string;
    readonly handler: VegaEventListenerHandler;
  }[];
  onViewDestroyed?: () => void;
}

// VegaWrapper is in exactly one of these states:
enum Status {
  // Has not visualisation to render.
  Empty,
  // Currently loading the visualisation.
  Loading,
  // Failed to load or render the visualisation. The reason is
  // retrievable via |error|.
  Error,
  // Displaying a visualisation:
  Done,
}

class EngineLoader implements vega.Loader {
  private engine?: Engine;
  private loader: vega.Loader;

  constructor(engine: Engine | undefined) {
    this.engine = engine;
    this.loader = vega.loader();
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  async load(uri: string, _options?: any): Promise<string> {
    if (this.engine === undefined) {
      return '';
    }
    try {
      const result = await this.engine.query(uri);
      const columns = result.columns();
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const rows: any[] = [];
      for (const it = result.iter({}); it.valid(); it.next()) {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const row: any = {};
        for (const name of columns) {
          let value = it.get(name);
          if (typeof value === 'bigint') {
            value = Number(value);
          }
          row[name] = value;
        }
        rows.push(row);
      }
      return JSON.stringify(rows);
    } catch (e) {
      if (e instanceof QueryError) {
        console.error(e);
        return '';
      } else {
        throw e;
      }
    }
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  sanitize(uri: string, options: any): Promise<{href: string}> {
    return this.loader.sanitize(uri, options);
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  http(uri: string, options: any): Promise<string> {
    return this.loader.http(uri, options);
  }

  file(filename: string): Promise<string> {
    return this.loader.file(filename);
  }
}

class VegaWrapper {
  private dom: Element;
  private _spec?: string;
  private _data?: VegaViewData;
  private view?: vega.View;
  private pending?: Promise<vega.View>;
  private _status: Status;
  private _error?: string;
  private _engine?: Engine;

  private _signalHandlers?: {
    readonly name: string;
    readonly handler: vega.SignalListenerHandler;
  }[];
  private _eventHandlers?: {
    readonly name: string;
    readonly handler: vega.EventListenerHandler;
  }[];
  private _onViewDestroyed?: () => void;

  constructor(dom: Element) {
    this.dom = dom;
    this._status = Status.Empty;
  }

  get status(): Status {
    return this._status;
  }

  get error(): string {
    return this._error ?? '';
  }

  set spec(value: string) {
    if (this._spec !== value) {
      this._spec = value;
      this.updateView();
    }
  }

  set data(value: VegaViewData) {
    if (this._data === value || shallowEquals(this._data, value)) {
      return;
    }
    this._data = value;
    this.updateView();
  }

  set engine(engine: Engine | undefined) {
    this._engine = engine;
  }

  set signalHandlers(
    handlers: {
      readonly name: string;
      readonly handler: VegaSignalListenerHandler;
    }[],
  ) {
    for (const {name, handler} of this._signalHandlers ?? []) {
      this.view?.removeSignalListener(name, handler);
    }
    this._signalHandlers = handlers.map(({name, handler}) => {
      return {
        name,
        handler: (name2, value) =>
          handler({view: this.view!, name: name2, value}),
      };
    });
    for (const {name, handler} of this._signalHandlers) {
      this.view?.addSignalListener(name, handler);
    }
  }

  set eventHandlers(
    handlers: {
      readonly name: string;
      readonly handler: VegaEventListenerHandler;
    }[],
  ) {
    for (const {name, handler} of this._eventHandlers ?? []) {
      this.view?.removeEventListener(name, handler);
    }
    this._eventHandlers = handlers.map(({name, handler}) => {
      return {
        name,
        handler: (event, item) => handler({view: this.view!, event, item}),
      };
    });
    for (const {name, handler} of this._eventHandlers) {
      this.view?.addEventListener(name, handler);
    }
  }

  set onViewDestroyed(handler: (() => void) | undefined) {
    this._onViewDestroyed = handler;
  }

  onResize() {
    if (this.view) {
      this.view.resize();
    }
  }

  private updateView() {
    this._status = Status.Empty;
    this._error = undefined;

    // We no longer care about inflight renders:
    if (this.pending) {
      this.pending = undefined;
    }

    // Destroy existing view if needed:
    if (this.view) {
      this._onViewDestroyed?.();
      this.view.finalize();
      this.view = undefined;
    }

    // If the spec and data are both available then create a new view:
    if (this._spec !== undefined && this._data !== undefined) {
      let spec;
      try {
        spec = JSON.parse(this._spec);
      } catch (e) {
        this.setError(e);
        return;
      }

      if (isVegaLite(spec)) {
        try {
          spec = vegaLite.compile(spec, {}).spec;
        } catch (e) {
          this.setError(e);
          return;
        }
      }

      // Create the runtime and view the bind the host DOM element
      // and any data.
      const runtime = vega.parse(spec);
      this.view = new vega.View(runtime, {
        loader: new EngineLoader(this._engine),
      });
      this.view.hover();
      this.view.initialize(this.dom);
      for (const [key, value] of Object.entries(this._data)) {
        this.view.data(key, value);
      }

      if (isVegaLite(this._spec)) {
        for (const {name, handler} of this._signalHandlers ?? []) {
          this.view.addSignalListener(name, handler);
        }
        for (const {name, handler} of this._eventHandlers ?? []) {
          this.view.addEventListener(name, handler);
        }
      }

      const pending = this.view.runAsync();
      pending
        .then(() => {
          this.handleComplete(pending);
        })
        .catch((err) => {
          this.handleError(pending, err);
        });
      this.pending = pending;
      this._status = Status.Loading;
    }
  }

  private handleComplete(pending: Promise<vega.View>) {
    if (this.pending !== pending) {
      return;
    }
    this._status = Status.Done;
    this.pending = undefined;
    m.redraw();
  }

  private handleError(pending: Promise<vega.View>, err: unknown) {
    if (this.pending !== pending) {
      return;
    }
    this.pending = undefined;
    this.setError(err);
  }

  private setError(err: unknown) {
    this._status = Status.Error;
    this._error = getErrorMessage(err);
    m.redraw();
  }

  [Symbol.dispose]() {
    this._data = undefined;
    this._spec = undefined;
    this.updateView();
  }
}

export class VegaView implements m.ClassComponent<VegaViewAttrs> {
  private wrapper?: VegaWrapper;
  private resize?: Disposable;

  oncreate({dom, attrs}: m.CVnodeDOM<VegaViewAttrs>) {
    const wrapper = new VegaWrapper(dom.firstElementChild!);
    wrapper.spec = attrs.spec;
    wrapper.data = attrs.data;
    wrapper.engine = attrs.engine;
    wrapper.signalHandlers = attrs.signalHandlers ?? [];
    wrapper.eventHandlers = attrs.eventHandlers ?? [];
    wrapper.onViewDestroyed = attrs.onViewDestroyed;

    this.wrapper = wrapper;
    this.resize = new SimpleResizeObserver(dom, () => {
      wrapper.onResize();
    });
  }

  onupdate({attrs}: m.CVnodeDOM<VegaViewAttrs>) {
    if (this.wrapper) {
      this.wrapper.spec = attrs.spec;
      this.wrapper.data = attrs.data;
      this.wrapper.engine = attrs.engine;
      this.wrapper.signalHandlers = attrs.signalHandlers ?? [];
      this.wrapper.eventHandlers = attrs.eventHandlers ?? [];
      this.wrapper.onViewDestroyed = attrs.onViewDestroyed;
    }
  }

  onremove() {
    if (this.resize) {
      this.resize[Symbol.dispose]();
      this.resize = undefined;
    }
    if (this.wrapper) {
      this.wrapper[Symbol.dispose]();
      this.wrapper = undefined;
    }
  }

  view(_: m.Vnode<VegaViewAttrs>) {
    return m(
      '.pf-vega-view',
      m(''),
      this.wrapper?.status === Status.Loading &&
        m('.pf-vega-view-status', m(Spinner)),
      this.wrapper?.status === Status.Error &&
        m('.pf-vega-view-status', this.wrapper?.error ?? 'Error'),
    );
  }
}
