/* Copyright 2015 Bloomberg Finance L.P.
 *
 * 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 * as d3 from 'd3';
import { LinearScale, OrdinalScale } from 'bqscales';
// const d3 =Object.assign({}, require("d3-selection"));
import { Interaction } from './Interaction';
import { convert_dates } from './utils';
import { WidgetView } from '@jupyter-widgets/base';

export abstract class BaseSelector extends Interaction {
  initialize(parameters) {
    this.setElement(
      document.createElementNS(d3.namespaces.svg, 'g') as HTMLElement
    );
    this.d3el = d3.select(this.el);
    // The following line is a workaround to avoid calling the initialize
    // method from Interaction. Indeed this last one wraps the area responsible
    // for capturing mouse events in a rect element whose width and height are
    // 0. The whole hierarchy of Interaction should be refactored to fix this.
    WidgetView.prototype.initialize.call(this, parameters);
    //super.initialize.apply(this, arguments);
  }

  render() {
    this.parent = this.options.parent;
    this.width = this.parent.plotareaWidth;
    this.height = this.parent.plotareaHeight;
    this.mark_views_promise = this.populate_mark_views();
  }

  create_listeners() {
    this.listenTo(this.parent, 'margin_updated', this.relayout);
    this.listenTo(this.model, 'change:selected', this.selected_changed);
    this.listenTo(this.model, 'change:marks', this.marks_changed);
    this.listenTo(this.model, 'msg:custom', this.handle_custom_messages);
  }

  relayout() {
    this.width = this.parent.plotareaWidth;
    this.height = this.parent.plotareaHeight;
  }

  async populate_mark_views() {
    const fig = this.parent;
    const mark_ids = this.model.get('marks').map((m) => m.model_id);
    const views = await Promise.all(fig.mark_views.views);

    const fig_mark_ids = fig.mark_views._models.map((m) => m.model_id);
    const mark_indices = mark_ids.map((mid) => fig_mark_ids.indexOf(mid));
    // return the views, based on the assumption that fig.mark_views is an
    // ordered list
    this.mark_views = mark_indices.map((elem) => views[elem]);
  }

  marks_changed() {
    this.populate_mark_views().then(() => this.selected_changed());
  }

  handle_custom_messages(msg) {
    if (msg.type === 'reset') {
      this.reset();
    }
  }

  abstract reset();

  abstract selected_changed();
  abstract create_scales();

  set_selected(name, value) {
    this.model.set(name, convert_dates(value));
  }

  width: number;
  height: number;
  mark_views_promise: Promise<void>;
}

export abstract class BaseXSelector extends BaseSelector {
  create_scales() {
    if (this.scale) {
      this.scale.remove();
    }
    if (this.model.get('scale')) {
      const that = this;
      return this.create_child_view(this.model.get('scale')).then((view) => {
        that.scale = view as WidgetView as LinearScale;
        // The argument is to suppress the update to gui
        that.update_scale_domain(true);
        that.set_range([that.scale]);
        that.scale.on('domain_changed', that.update_scale_domain, that);
        return view;
      });
    }
  }

  update_scale_domain(ignore_gui_update = false) {
    // When the domain of the scale is updated, the domain of the scale
    // for the selector must be expanded to account for the padding.
    const xy = this.model.get('orientation') == 'vertical' ? 'y' : 'x';
    const initial_range = this.parent.padded_range(xy, this.scale.model);
    const target_range = this.parent.range(xy);
    this.scale.expandDomain(initial_range, target_range);
  }

  set_range(array) {
    const xy = this.model.get('orientation') == 'vertical' ? 'y' : 'x';
    for (let iter = 0; iter < array.length; iter++) {
      array[iter].setRange(this.parent.range(xy));
    }
  }

  scale: LinearScale | OrdinalScale;
}

export abstract class BaseXYSelector extends BaseSelector {
  create_scales() {
    const that = this;
    if (this.x_scale) {
      this.x_scale.remove();
    }
    if (this.y_scale) {
      this.y_scale.remove();
    }
    const scale_promises = [];
    if (this.model.get('x_scale')) {
      scale_promises.push(
        this.create_child_view(this.model.get('x_scale')).then(
          // @ts-ignore
          (view: LinearScale | OrdinalScale) => {
            that.x_scale = view;
            that.update_xscale_domain();
            that.set_x_range([that.x_scale]);
            that.x_scale.on('domain_changed', that.update_xscale_domain, that);
            return view;
          }
        )
      );
    }
    if (this.model.get('y_scale')) {
      scale_promises.push(
        this.create_child_view(this.model.get('y_scale')).then(
          // @ts-ignore
          (view: LinearScale | OrdinalScale) => {
            that.y_scale = view;
            that.update_yscale_domain();
            that.set_y_range([that.y_scale]);
            that.y_scale.on('domain_changed', that.update_yscale_domain, that);
            return view;
          }
        )
      );
    }

    return Promise.all(scale_promises);
  }

  set_x_range(array) {
    for (let iter = 0; iter < array.length; iter++) {
      array[iter].setRange(this.parent.range('x'));
    }
  }

  set_y_range(array) {
    for (let iter = 0; iter < array.length; iter++) {
      array[iter].setRange(this.parent.range('y'));
    }
  }

  update_xscale_domain() {
    // When the domain of the scale is updated, the domain of the scale
    // for the selector must be expanded to account for the padding.
    const initial_range = this.parent.padded_range('x', this.x_scale.model);
    const target_range = this.parent.range('x');
    this.x_scale.expandDomain(initial_range, target_range);
  }

  update_yscale_domain() {
    // When the domain of the scale is updated, the domain of the scale
    // for the selector must be expanded to account for the padding.
    const initial_range = this.parent.padded_range('y', this.y_scale.model);
    const target_range = this.parent.range('y');
    this.y_scale.expandDomain(initial_range, target_range);
  }

  x_scale: LinearScale | OrdinalScale;
  y_scale: LinearScale | OrdinalScale;
}
