/* 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 {
  uuid,
  resolvePromisesDict,
  Dict,
  ViewList,
  WidgetModel,
} from '@jupyter-widgets/base';
import * as _ from 'underscore';

import { MessageLoop } from '@lumino/messaging';

import { Widget } from '@lumino/widgets';

import { Scale } from 'bqscales';

import * as d3 from 'd3';
// var d3 =Object.assign({}, require("d3-array"), require("d3-format"), require("d3-selection"), require("d3-selection-multi"), require("d3-shape"));
import { Figure } from './Figure';
import { MarketMapModel } from './MarketMapModel';
import { Tooltip } from './Tooltip';
import * as popperreference from './PopperReference';
import popper from 'popper.js';
import { applyAttrs, applyStyles, getLuminoWidget } from './utils';

export class MarketMap extends Figure {
  protected async renderImpl() {
    const figureSize = this.getFigureSize();
    this.width = figureSize.width;
    this.height = figureSize.height;

    this.id = uuid();

    this.scales = {};
    this.set_top_el_style();

    this.num_rows = this.model.get('rows');
    this.num_cols = this.model.get('cols');
    this.row_groups = this.model.get('row_groups');
    this.enable_select = this.model.get('enable_select');

    this.update_data();
    // set the number of rows and columns in the map
    this.set_area_dimensions(this.data.length);

    if (this.model.get('theme')) {
      this.svg.classed(this.model.get('theme'), true);
    }
    this.fig = this.svg
      .append('g')
      .attr(
        'transform',
        'translate(' + this.margin.left + ',' + this.margin.top + ')'
      );

    this.fig_map = this.fig.append('g');
    this.fig_axes = this.fig.append('g');
    this.fig_click = this.fig.append('g');
    this.fig_hover = this.fig.append('g');
    this.fig_names = this.fig
      .append('g')
      .style('display', this.model.get('show_groups') ? 'inline' : 'none');

    this.fig_map.classed('g-map', true);
    this.fig_axes.classed('g-axes', true);
    this.fig_click.classed('g-click', true);
    this.fig_hover.classed('g-hover', true);
    this.fig_names.classed('g-names', true);

    // code for tool tip to be displayed
    this.tooltip_div = d3
      .select(document.createElement('div'))
      .attr('class', 'bqplot_mark_tooltip');
    applyStyles(this.tooltip_div, { opacity: 0, 'pointer-events': 'none' });

    const freeze_tooltip_loc = this.model.get('freeze_tooltip_location');
    if (freeze_tooltip_loc) {
      this.popper_reference = new popperreference.ElementReference(
        this.svg.node()
      );
    } else {
      this.popper_reference = new popperreference.PositionReference({
        x: 0,
        y: 0,
        width: 20,
        height: 20,
      });
    }
    this.popper = new popper(this.popper_reference, this.tooltip_div.node(), {
      placement: 'auto',
    });

    this.update_default_tooltip();

    this.selected_stroke = this.model.get('selected_stroke');
    this.hovered_stroke = this.model.get('hovered_stroke');

    this.update_plotarea_dimensions();

    this.title = this.fig
      .append('text')
      .attr('class', 'mainheading')
      .text(this.model.get('title'));
    applyAttrs(this.title, {
      x: 0.5 * this.width,
      y: -(this.margin.top / 2.0),
      dy: '1em',
    });
    applyStyles(this.title, this.model.get('title_style'));

    await this.create_scale_views().then(async () => {
      this.create_listeners();

      this.axis_views = new ViewList(this.add_axis, null, this);
      const axis_views_updated = this.axis_views.update(this.model.get('axes'));
      this.model.on('change:axes', (model, value, options) => {
        this.axis_views.update(value);
      });

      this.draw_map();
      document.body.appendChild(this.tooltip_div.node());
      this.create_tooltip_widget();

      // In the classic notebook, we should relayout the figure on
      // resize of the main window.
      window.addEventListener('resize', this.debouncedRelayout);
      this.once('remove', () => {
        window.removeEventListener('resize', this.debouncedRelayout);
      });

      await Promise.all([Promise.resolve(), axis_views_updated]);
    });
  }

  set_top_el_style() {
    this.el.style['user-select'] = 'none';
    this.el.style['ms-user-select'] = 'none';
    this.el.style['moz-user-select'] = 'none';
    this.el.style['khtml-user-select'] = 'none';
    this.el.style['webkit-user-select'] = 'none';
  }

  update_plotarea_dimensions() {
    this.width = this.width - this.margin.left - this.margin.right;
    this.height = this.height - this.margin.top - this.margin.bottom;
    this.column_width = parseFloat((this.width / this.num_cols).toFixed(2));
    this.row_height = parseFloat((this.height / this.num_rows).toFixed(2));
  }

  reset_drawing_controls() {
    // Properties useful in drawing the map
    this.prev_x = 0;
    this.prev_y = -1;
    this.y_direction = 1; // for y direction 1 means going to the right
    this.x_direction = 1; // for x direction 1 means going down
    this.group_iter = 1;
  }

  create_listeners() {
    this.listenTo(this.model, 'change:scales', this.create_scale_views);
    this.listenTo(this.model, 'change:color', this.recolor_chart);
    this.listenTo(this.model, 'change:colors', this.colors_updated);
    this.listenTo(this.model, 'change:show_groups', this.show_groups);
    this.listenTo(
      this.model,
      'change:selected_stroke',
      this.update_selected_stroke
    );
    this.listenTo(
      this.model,
      'change:hovered_stroke',
      this.update_hovered_stroke
    );
    this.listenTo(this.model, 'change:font_style', this.update_font_style);
    this.model.on_some_change(
      ['title', 'title_style'],
      this.update_title,
      this
    );
    this.listenTo(this.model, 'change:selected', function () {
      this.clear_selected();
      this.apply_selected();
    });
    this.model.on_some_change(
      ['names', 'groups', 'ref_data'],
      function () {
        this.update_data();
        this.compute_dimensions_and_draw();
      },
      this
    );
    this.listenTo(this.model, 'change:rows', function (model, value) {
      this.num_rows = value;
      this.compute_dimensions_and_draw();
    });
    this.listenTo(this.model, 'change:cols', function (model, value) {
      this.num_cols = value;
      this.compute_dimensions_and_draw();
    });
    this.listenTo(this.model, 'change:row_groups', function (model, value) {
      this.row_groups = value;
      this.compute_dimensions_and_draw();
    });
    this.listenTo(
      this.model,
      'change:tooltip_widget',
      this.create_tooltip_widget
    );
    this.listenTo(
      this.model,
      'change:tooltip_fields',
      this.update_default_tooltip
    );
    this.listenTo(
      this.model,
      'change:tooltip_formats',
      this.update_default_tooltip
    );
  }

  update_title(model, value) {
    this.title.text(this.model.get('title'));
    applyStyles(this.title, this.model.get('title_style'));
  }

  relayout() {
    const that = this;

    const figureSize = this.getFigureSize();
    this.width = figureSize.width;
    this.height = figureSize.height;

    window.requestAnimationFrame(() => {
      // update ranges
      that.update_plotarea_dimensions();

      // transform figure
      that.fig.attr(
        'transform',
        'translate(' + that.margin.left + ',' + that.margin.top + ')'
      );
      applyAttrs(that.title, {
        x: 0.5 * that.width,
        y: -(that.margin.top / 2.0),
        dy: '1em',
      });

      that.draw_map();

      // Drawing the selected cells
      that.clear_selected();
      that.apply_selected();

      // When map is expanded or contracted, there should not be any
      // accidental hovers. To prevent this, the following call is made.
      that.fig_hover.selectAll('rect').remove();
      that.hide_tooltip();
      that.trigger('margin_updated');
    });
  }

  update_data() {
    const that = this;
    this.model.set('selected', []);
    this.data = this.model.get('names') || [];
    this.ref_data = this.model.get('ref_data');
    this.group_data = this.model.get('groups');
    this.groups = _.uniq(this.group_data, true);
    let display_text = this.model.get('display_text');
    display_text =
      display_text === null || display_text.length === 0
        ? this.data
        : display_text;

    this.colors = this.model.get('colors');
    const num_colors = this.colors.length;
    this.colors_map = function (d) {
      return that.get_color(d, num_colors);
    };
    const color_data = this.model.get('color');
    const mapped_data = _.map(this.data, (d, i) => {
      return {
        display: display_text[i],
        name: d,
        color: Number.isNaN(color_data[i]) ? undefined : color_data[i],
        group: this.group_data[i],
        ref_data:
          this.ref_data === null || this.ref_data === undefined
            ? null
            : this.ref_data[i],
      };
    });

    this.update_domains();
    this.grouped_data = _.groupBy(mapped_data, (d, i) => {
      return that.group_data[i];
    });
    this.groups = [];
    this.running_sums = [];
    this.running_sums[0] = 0;
    let count = 0;
    for (const key in this.grouped_data) {
      this.groups.push(key);
      count += this.grouped_data[key].length;
      this.running_sums.push(count);
    }
    this.running_sums.pop();
  }

  update_domains() {
    const color_scale_model = this.model.getScales().color;
    const color_data = this.model.get('color');
    if (color_scale_model && color_data.length > 0) {
      color_scale_model.computeAndSetDomain(color_data, this.model.model_id);
    }
  }

  set_area_dimensions(num_items) {
    this.num_rows = this.model.get('rows');
    this.num_cols = this.model.get('cols');
    this.row_groups = this.model.get('row_groups');

    if (
      this.num_cols !== undefined &&
      this.num_cols !== null &&
      this.num_cols !== 0
    ) {
      // When the number of row groups is greater than 1, the number
      // of columns has to be an odd number. This is to
      // ensure the continuity of the waffles when groups are spread
      // across multiple row groups
      if (this.row_groups > 1 && this.num_cols % 2 === 0) {
        this.num_cols++;
      }
      this.num_rows = Math.floor(num_items / this.num_cols);
      this.num_rows =
        num_items % this.num_cols === 0 ? this.num_rows : this.num_rows + 1;
    } else if (
      this.num_rows !== undefined &&
      this.num_rows !== null &&
      this.num_rows !== 0
    ) {
      this.num_cols = Math.floor(num_items / this.num_rows);
      this.num_cols =
        num_items % this.num_rows === 0 ? this.num_cols : this.num_cols + 1;
      if (this.row_groups > 1 && this.num_cols % 2 === 0) {
        this.num_cols++;
      }
    } else {
      this.num_cols = Math.floor(Math.sqrt(num_items));
      if (this.row_groups > 1 && this.num_cols % 2 === 0) {
        this.num_cols++;
      }
      this.num_rows = Math.floor(num_items / this.num_cols);
      this.num_rows =
        num_items % this.num_cols === 0 ? this.num_rows : this.num_rows + 1;
    }

    // row_groups cannot be greater than the number of rows
    this.row_groups = Math.min(this.row_groups, this.num_rows);
    // if there is only one row_group, then the number of columns are
    // not necessarily equal to the variable this.num_cols as we draw
    // row first. So we need to adjust the this.num_cols variable
    // according to the num_rows.
    if (this.row_groups == 1) {
      this.num_cols = Math.floor(num_items / this.num_rows);
      this.num_cols =
        num_items % this.num_rows === 0 ? this.num_cols : this.num_cols + 1;
    }
    // depending on the number of rows, we need to decide when to
    // switch direction. The below functions tells us where to switch
    // direction.
    this.set_row_limits();
  }

  compute_dimensions_and_draw() {
    this.set_area_dimensions(this.data.length);
    this.update_plotarea_dimensions();
    this.draw_map();

    this.clear_selected();
    this.apply_selected();

    // when data is changed
    this.fig_hover.selectAll('rect').remove();
    this.hide_tooltip();
  }

  update_default_tooltip() {
    this.tooltip_fields = this.model.get('tooltip_fields');
    const formats = this.model.get('tooltip_formats');
    this.tooltip_formats = this.tooltip_fields.map((field, index) => {
      const fmt = formats[index];
      if (fmt === undefined || fmt === '') {
        return (d) => d;
      } else {
        return d3.format(fmt) as (d: number) => string;
      }
    });
  }

  create_scale_views() {
    for (const key in this.scales) {
      this.stopListening(this.scales[key]);
    }
    const scale_models = this.model.getScales();
    const scale_promises = {};
    _.each(scale_models, (model: WidgetModel, key) => {
      scale_promises[key] = this.create_child_view(model);
    });

    return resolvePromisesDict<Scale>(scale_promises).then((d: Dict<Scale>) => {
      this.scales = d;
      this.set_scales();
    });
  }

  set_scales() {
    const that = this;
    const color_scale = this.scales.color;
    if (color_scale) {
      color_scale.on('color_scale_range_changed', that.update_map_colors, that);
      this.update_domains();
      this.listenTo(color_scale, 'domain_changed', () => {
        that.update_map_colors();
      });
      this.update_map_colors();
    }
  }

  show_groups(model, value) {
    this.fig_names.style('display', value ? 'inline' : 'none');
    this.fig_map
      .selectAll('.market_map_text')
      .style('opacity', value ? 0.2 : 1);
    this.fig_map
      .selectAll('.market_map_rect')
      .style('stroke-opacity', value ? 0.2 : 1);
  }

  draw_map() {
    this.reset_drawing_controls();
    // Removing pre existing elements from the map
    this.fig_map.selectAll('.element_group').remove();
    this.fig_names.selectAll('.names_object').remove();
    this.rect_groups = this.fig_map
      .selectAll('.element_group')
      .data(this.groups);
    const color_scale = this.scales.color;

    const that = this;
    this.rect_groups = this.rect_groups
      .enter()
      .append('g')
      .attr('class', 'element_group')
      .attr('transform', (d, i) => {
        return that.get_group_transform(i);
      })
      .merge(
        this.rect_groups as d3.Selection<
          SVGGElement,
          any,
          SVGGraphicsElement,
          any
        >
      );

    this.rect_groups.exit().remove();
    this.end_points = [];
    this.rect_groups.nodes().forEach((d, i) => {
      const data = that.grouped_data[that.groups[i]];
      const return_arr = that.get_new_cords();
      const ends = that.get_end_points(
        return_arr[2],
        data.length,
        return_arr[0],
        return_arr[1],
        return_arr[3],
        return_arr[4]
      );
      ends.forEach((point) => {
        that.end_points.push(point);
      });
      const element_count = that.running_sums[i];

      let groups = d3
        .select(d)
        .selectAll<SVGGElement, undefined>('.rect_element')
        .data(data);

      // Appending the <g> <rect> and <text> elements to the newly
      // added nodes
      const new_groups = groups
        .enter()
        .append('g')
        .classed('rect_element', true);

      new_groups
        .append('rect')
        .attr('x', 0)
        .attr('y', 0)
        .classed('market_map_rect', true);

      // Grouping calls to style into a single call to styles
      // leads to build error despite the import of d3-selection-multi
      new_groups
        .append('text')
        .classed('market_map_text', true)
        .style('text-anchor', 'middle')
        .style('pointer-events', 'none')
        .style('dominant-baseline', 'central');

      groups = new_groups.merge(groups);

      // Update the attributes of the entire set of nodes
      groups
        .attr('transform', (data, ind) => {
          return that.get_cell_transform(ind);
        })
        .on('click', function (data, ind) {
          that.cell_click_handler(data, element_count + ind, this);
        })
        .on('mouseover', function (data, ind) {
          that.mouseover_handler(data, element_count + ind, this);
        })
        .on('mousemove', (data) => {
          that.mousemove_handler();
        })
        .on('mouseout', function (data, ind) {
          that.mouseout_handler(data, element_count + ind, this);
        })
        .attr('class', function (data, index) {
          return (
            d3.select(this).attr('class') +
            ' ' +
            'rect_' +
            (element_count + index)
          );
        });

      groups
        .selectAll('.market_map_rect')
        .attr('width', that.column_width)
        .attr('height', that.row_height)
        .style('stroke-opacity', that.model.get('show_groups') ? 0.2 : 1.0)
        .style('stroke', that.model.get('stroke'))
        .style('fill', (elem: any, j) => {
          return color_scale && elem.color !== undefined && elem.color !== null
            ? color_scale.scale(elem.color)
            : that.colors_map(i);
        });

      groups
        .selectAll('.market_map_text')
        .attr('x', that.column_width / 2.0)
        .attr('y', that.row_height / 2.0)
        .text((data: any, j) => {
          return data.display;
        })
        .style('opacity', that.model.get('show_groups') ? 0.2 : 1.0);

      // Removing the old nodes
      groups.exit().remove();
      that.create_bounding_path(d, ends);
      const min_x = d3.min(ends, (end_point: any) => {
        return end_point.x;
      });
      const min_y = d3.min(ends, (end_point: any) => {
        return end_point.y;
      });

      const obj = that.fig_names
        .append('foreignObject')
        .attr('class', 'names_object')
        .attr('x', min_x)
        .attr('y', min_y)
        .append('xhtml:div')
        .attr('class', 'names_div')
        .text(that.groups[i]);
      applyStyles(obj, {
        display: 'flex',
        'flex-direction': 'row',
        'align-content': 'center',
        'align-items': 'center',
        width: '100%',
        height: '100%',
        'justify-content': 'center',
        'word-wrap': 'break-word',
        font: '24px sans-serif',
        color: 'black',
      });
    });
    this.update_font_style();
    this.draw_group_names();
  }

  draw_group_names() {
    // Get all the bounding rects of the paths around each of the
    // sectors. Get their client bounding rect.
    const paths = this.svg
      .selectAll('.bounding_path')
      .nodes() as SVGPathElement[];
    const clientRects = paths.map((path) => {
      return path.getBoundingClientRect();
    });
    const text_elements = this.fig_names
      .selectAll('.names_object')
      .data(clientRects);
    text_elements
      .attr('width', (d) => {
        return d.width;
      })
      .attr('height', (d) => {
        return d.height;
      });
  }

  recolor_chart() {
    const that = this;
    this.update_data();
    this.rect_groups = this.fig.selectAll('.element_group').data(this.groups);
    const color_scale = this.scales.color;

    this.rect_groups.nodes().forEach((d, i) => {
      const data = that.grouped_data[that.groups[i]];
      d3.select(d)
        .selectAll('.rect_element')
        .data(data)
        .select('rect')
        .style('stroke', that.model.get('stroke'))
        .style('fill', (elem: any, j) => {
          return color_scale && elem.color !== undefined && elem.color !== null
            ? color_scale.scale(elem.color)
            : that.colors_map(i);
        });
    });
  }

  update_font_style() {
    // This is a bit awkward because we did not figure out how to get
    // Typescript to recognize the d3-select-multi typings.
    const x: any = this.svg.selectAll('.market_map_text');
    applyStyles(x, this.model.get('font_style'));
  }

  update_map_colors() {
    if (this.rect_groups !== undefined && this.rect_groups !== null) {
      this.recolor_chart();
    }
  }

  cell_click_handler(data, id, cell) {
    if (this.model.get('enable_select')) {
      const selected = Object.assign(
        this.model.get('selected').slice(),
        this.model.get('selected')
      );
      const index = selected.indexOf(data.name);
      if (index == -1) {
        // not already selected, so add to selected
        selected.push(data.name);
      } else {
        // already in selected list, so delete from selected
        selected.splice(index, 1);
      }
      this.model.set('selected', selected);
      this.touch();
    }
  }

  apply_selected() {
    const selected = this.model.get('selected');
    const that = this;
    if (selected === undefined || selected === null || selected.length === 0) {
      this.clear_selected();
    } else {
      selected.forEach((data) => {
        const selected_cell = that.fig_map
          .selectAll('.rect_element')
          .filter((d: any, i) => {
            return d.name === data;
          });

        const rect = that.fig_click
          .append('rect')
          .data(selected_cell.data())
          .attr('transform', selected_cell.attr('transform'))
          .attr('x', 0)
          .attr('y', 0)
          .attr('width', that.column_width)
          .attr('height', that.row_height);
        applyStyles(rect, {
          stroke: that.selected_stroke,
          'stroke-width': '3px',
          fill: 'none',
        });
      });
    }
  }

  clear_selected() {
    this.fig_click.selectAll('rect').remove();
  }

  mouseover_handler(data, id, cell) {
    const transform = d3.select(cell).attr('transform');
    if (this.model.get('enable_hover')) {
      const rect = this.fig_hover
        .append('rect')
        .attr('class', 'hover_' + id)
        .attr('transform', transform)
        .attr('x', 0)
        .attr('y', 0)
        .attr('width', this.column_width)
        .attr('height', this.row_height);
      applyStyles(rect, {
        stroke: this.hovered_stroke,
        'stroke-width': '3px',
        fill: 'none',
        'pointer-events': 'none',
      });
      this.show_tooltip(d3.event, data);
      this.send({ event: 'hover', data: data.name, ref_data: data.ref_data });
    }
  }

  update_selected_stroke(model, value) {
    this.selected_stroke = value;
    this.fig_click.selectAll('rect').style('stroke', value);
  }

  update_hovered_stroke(model, value) {
    this.hovered_stroke = value;
    // I do not need to update anything else because when hovered color
    // is being updated you are not hovering over anything.
  }

  mouseout_handler(data, id, cell) {
    this.fig_hover.select('.hover_' + id).remove();
    this.hide_tooltip();
  }

  show_tooltip(event, data) {
    const that = this;
    if (
      !this.tooltip_view &&
      (!this.tooltip_fields || this.tooltip_fields.length == 0)
    ) {
      return;
    } else {
      const tooltip_div = this.tooltip_div;
      tooltip_div.transition().style('opacity', 0.9).style('display', null);

      this.move_tooltip();
      tooltip_div.select('table').remove();

      const ref_data = data.ref_data;
      if (!this.tooltip_view) {
        const tooltip_table = tooltip_div
          .append('table')
          .selectAll('tr')
          .data(this.tooltip_fields);

        tooltip_table.exit().remove();
        const table_rows = tooltip_table.enter().append('tr');

        table_rows
          .append('td')
          .attr('class', 'tooltiptext')
          .text((datum) => {
            return datum;
          });

        table_rows
          .append('td')
          .attr('class', 'tooltiptext')
          .text((datum, index) => {
            if (ref_data === null || ref_data === undefined) {
              return null;
            }

            if (ref_data[datum] === null || ref_data[datum] === undefined) {
              return 'N/A';
            }

            return that.tooltip_formats[index](ref_data[datum]);
          });
      }
      this.popper.enableEventListeners();
      this.move_tooltip();
    }
  }

  mousemove_handler() {
    this.move_tooltip();
  }

  move_tooltip() {
    (this.popper_reference as any).x = d3.event.clientX;
    (this.popper_reference as any).y = d3.event.clientY;
    this.popper.scheduleUpdate();
  }

  hide_tooltip() {
    this.tooltip_div.style('pointer-events', 'none');
    this.tooltip_div.transition().style('opacity', 0).style('display', 'none');
    this.popper.disableEventListeners();
  }

  create_tooltip_widget() {
    const tooltip_model = this.model.get('tooltip_widget');
    if (this.tooltip_view !== null && this.tooltip_view !== undefined) {
      //remove the previous tooltip
      this.tooltip_view.remove();
      this.tooltip_view = null;
    }
    const that = this;
    if (tooltip_model) {
      const tooltip_widget_creation_promise =
        this.create_child_view(tooltip_model);
      tooltip_widget_creation_promise.then((view) => {
        that.tooltip_view = view as Tooltip;

        MessageLoop.sendMessage(getLuminoWidget(view), Widget.Msg.BeforeAttach);
        that.tooltip_div.node().appendChild(view.el);
        MessageLoop.sendMessage(getLuminoWidget(view), Widget.Msg.AfterAttach);
      });
    }
  }

  get_group_transform(index) {
    return 'translate(' + '0' + ', 0)';
  }

  get_cell_transform(index) {
    if (!this.past_border_y()) {
      if (this.past_border_x()) {
        this.y_direction = -1 * this.y_direction;
        this.prev_x += this.x_direction;
      } else {
        this.x_direction = -1 * this.x_direction;
        this.prev_y += this.y_direction;
        this.group_iter += 1;
      }
    } else {
      this.prev_y += this.y_direction;
    }
    return (
      'translate(' +
      this.prev_x * this.column_width +
      ', ' +
      this.prev_y * this.row_height +
      ')'
    );
  }

  get_new_cords() {
    let new_x = this.prev_x;
    let new_y = this.prev_y;
    let y_direction = this.y_direction;
    let x_direction = this.x_direction;
    let group_iter = this.group_iter;
    if (!this.past_border_y()) {
      if (this.past_border_x()) {
        y_direction = -1 * this.y_direction;
        new_x += this.x_direction;
      } else {
        x_direction = -1 * this.x_direction;
        new_y += this.y_direction;
        group_iter += 1;
      }
    } else {
      new_y += this.y_direction;
    }
    return [
      new_x,
      new_y,
      group_iter,
      x_direction,
      y_direction,
      new_x * this.column_width,
      new_y * this.row_height,
    ];
  }

  past_border_y() {
    if (this.y_direction == 1) {
      return this.prev_y + 1 < this.row_limits[this.group_iter];
    } else {
      return this.prev_y - 1 > this.row_limits[this.group_iter - 1] - 1;
    }
  }

  past_border_x() {
    if (this.x_direction == 1) {
      return this.prev_x + 1 < this.num_cols;
    } else {
      return this.prev_x - 1 > -1;
    }
  }

  colors_updated() {
    this.colors = this.model.get('colors');
    this.recolor_chart();
  }

  get_color(index, length) {
    return this.colors[index % length];
  }

  set_row_limits() {
    const step = Math.floor(this.num_rows / this.row_groups);
    this.row_limits = [];
    for (let iter = this.row_groups - 1; iter > -1; iter--) {
      this.row_limits.unshift(iter * step);
    }
    this.row_limits[this.row_groups] = this.num_rows;
  }

  get_end_points(
    group_iter,
    num_cells,
    start_col,
    start_row,
    x_direction,
    y_direction
  ) {
    //start_row is the 0-based index and not a 1-based index, i.e., it
    //is not the column number in the truest sense
    // Function to get the end points of the rectangle representing the
    // groups.
    // Requires the direction variables to be updated before this
    // function is called
    let top_row = this.row_limits[group_iter - 1];
    let bottom_row = this.row_limits[group_iter];
    let across = false;

    let init_x = x_direction;
    let init_y = y_direction;
    const end_points = [];
    let current_row;

    const rows_remaining =
      init_y == 1 ? bottom_row - start_row : start_row - top_row + 1;
    let cols_remaining =
      init_x == 1 ? this.num_cols - 1 - start_col : start_col; // this is the num of columns remaining
    //after the cuirrent column has been filled
    let elem_remaining = num_cells;
    //this holds the number of continuous cols that will be filled
    //between the current top and bottom rows
    let num_rows = bottom_row - top_row;

    if (elem_remaining !== 0) {
      // starting corner of the path
      this.calc_end_point_source(start_col, start_row, init_x, init_y).forEach(
        (d) => {
          end_points.push(d);
        }
      );
      const elem_filled = Math.min(rows_remaining, elem_remaining);

      if (elem_filled === elem_remaining) {
        // There are enough elements only to fill one column
        // partially. We add the three end points and exit
        // The adjacent corner from the starting corner. This is
        // required because the elements are filled in the first
        // row itself.
        this.calc_end_point_source(
          start_col,
          start_row,
          -1 * init_x,
          init_y
        ).forEach((d) => {
          end_points.push(d);
        });

        current_row = start_row + (elem_remaining - 1) * init_y;
        this.calc_end_point_dest(
          start_col,
          current_row,
          -1 * init_x,
          init_y
        ).forEach((e) => {
          end_points.push(e);
        });
        this.calc_end_point_dest(
          start_col,
          current_row,
          init_x,
          init_y
        ).forEach((e) => {
          end_points.push(e);
        });

        return end_points;
      }
      elem_remaining = elem_remaining - elem_filled;
      if (cols_remaining === 0) {
        // Since this is the last column, the adjacent corner from
        // the starting corner is added here too
        this.calc_end_point_source(
          start_col,
          start_row,
          init_x * -1,
          init_y
        ).forEach((d) => {
          end_points.push(d);
        });
      } else if (rows_remaining !== bottom_row - top_row) {
        // If the starting row is not the starting row of a group,
        // the poirnt adjacent to the starting point needs to be
        // added.
        this.calc_end_point_source(
          start_col,
          start_row,
          init_x * -1,
          init_y
        ).forEach((d) => {
          end_points.push(d);
        });

        if (elem_remaining > num_rows) {
          // If next row is completely filled, then the top row
          // element of the next column is an end point. That is
          // being added here.
          this.calc_end_point_dest(
            start_col + init_x,
            init_y == 1 ? top_row : bottom_row - 1,
            init_x * -1,
            init_y * -1
          ).forEach((d) => {
            end_points.push(d);
          });
        }
      } else if (elem_remaining < num_rows) {
        // one continuous row in this case
        this.calc_end_point_source(
          start_col,
          start_row,
          -1 * init_x,
          init_y
        ).forEach((d) => {
          end_points.push(d);
        });
      }
      start_row = start_row + init_y * (elem_filled - 1);
      //first set of end points are added here
      this.calc_end_point_dest(
        start_col,
        start_row,
        -1 * init_x,
        init_y
      ).forEach((d) => {
        end_points.push(d);
      });
      if (elem_remaining === 0) {
        this.calc_end_point_dest(start_col, start_row, init_x, init_y).forEach(
          (d) => {
            end_points.push(d);
          }
        );

        return end_points;
      }
      if (cols_remaining !== 0 && elem_remaining > num_rows) {
        start_col = start_col + init_x;
      }
    }

    while (elem_remaining > num_rows) {
      let no_cont_cols;
      if (num_rows * cols_remaining < elem_remaining) {
        no_cont_cols = cols_remaining;
        const leftover_elem = elem_remaining - no_cont_cols * num_rows;
        no_cont_cols += Math.floor(
          leftover_elem /
            (this.row_limits[group_iter + 1] - this.row_limits[group_iter])
        );
      } else {
        no_cont_cols = Math.floor(elem_remaining / num_rows);
      }

      if (no_cont_cols > cols_remaining) {
        start_col = init_x === 1 ? this.num_cols - 1 : 0;
        if (cols_remaining !== 0) {
          this.calc_end_point_dest(start_col, top_row, init_x, -1).forEach(
            (d) => {
              end_points.push(d);
            }
          );
        }
        no_cont_cols = cols_remaining;
        cols_remaining = this.num_cols;
        group_iter += 1;
        top_row = bottom_row;
        bottom_row = this.row_limits[group_iter];
        start_row = top_row;
        init_x = -1 * init_x;
        init_y = Math.pow(-1, no_cont_cols) * init_y * -1;
        this.calc_end_point_dest(
          start_col,
          bottom_row - 1,
          -1 * init_x,
          1
        ).forEach((d) => {
          end_points.push(d);
        });
      } else if (no_cont_cols === cols_remaining) {
        start_col = init_x === 1 ? this.num_cols - 1 : 0;
        if (cols_remaining !== 0) {
          this.calc_end_point_dest(start_col, top_row, init_x, -1).forEach(
            (d) => {
              end_points.push(d);
            }
          );
        }
        no_cont_cols = cols_remaining;
        cols_remaining = this.num_cols;
        group_iter += 1;
        if (group_iter < this.row_limits.length) {
          top_row = bottom_row;
          bottom_row = this.row_limits[group_iter];
          start_row = top_row;
          init_x = -1 * init_x;
          init_y = Math.pow(-1, no_cont_cols) * init_y * -1;
          across = true;
        } else {
          init_y = 1;
          init_x = 1;
        }
      } else {
        // The number of elements are such that the row group is
        // not exhausted.
        init_y = Math.pow(-1, no_cont_cols) * init_y;
        //As I am moving down this time, next time I will move up
        //and I might not reach the top row, it might be an end
        //point.
        start_row = init_y === 1 ? top_row : bottom_row - 1;
        start_col = start_col + init_x * (no_cont_cols - 1);
        this.calc_end_point_source(
          start_col,
          start_row,
          -1 * init_x,
          init_y
        ).forEach((d) => {
          end_points.push(d);
        });
      }
      elem_remaining -= no_cont_cols * num_rows;
      num_rows = bottom_row - top_row;
      //reset direction
      //this is an end point
    }
    //all elements are exhausted
    if (elem_remaining === 0) {
      // The column is exactly filled. In this case, the only end
      // point I need to add is the outer edge w.r.t. the direction
      // in which we are travelling
      start_row = init_y === 1 ? bottom_row - 1 : top_row;
      init_x = across ? -1 * init_x : init_x;
      this.calc_end_point_dest(start_col, start_row, init_x, init_y).forEach(
        (d) => {
          end_points.push(d);
        }
      );
    } else {
      // The previous column was exactly filled and the last column
      // is partially filled
      init_y = -1 * init_y; // Since we are in the next column, the direction of y has to be reversed
      start_row = init_y === 1 ? top_row : bottom_row - 1;
      start_col = across ? start_col : start_col + init_x;

      // this is the outer edge of the start of the last column w.r.t
      // the current direction of travel.
      this.calc_end_point_source(
        start_col,
        start_row,
        init_x * -1,
        init_y
      ).forEach((d) => {
        end_points.push(d);
      });

      // The points corresponding to the cell at which we stop.
      current_row = start_row + (elem_remaining - 1) * init_y; // this is the row in which we end
      // Two points need to be added. The boundary of the last cell
      // in the y-direction.
      this.calc_end_point_dest(start_col, current_row, init_x, init_y).forEach(
        (d) => {
          end_points.push(d);
        }
      );
      this.calc_end_point_dest(
        start_col,
        current_row,
        -1 * init_x,
        init_y
      ).forEach((d) => {
        end_points.push(d);
      });
    }

    return end_points;
  }

  create_bounding_path(elem, end_points) {
    const values = [];
    const editing_copy = end_points.slice();
    values.push(end_points[0]);
    editing_copy.splice(0, 1);
    //do union based on which direction you are trying to move in and
    //draw the path
    //best way seems to be horizaontal followed by vertical
    const props = ['x', 'y'];
    let iter = 0;
    let prop = props[iter % 2];
    let other_prop = props[(iter + 1) % 2];
    const curr_elem = values[0];
    let match = curr_elem[prop];
    let dim = curr_elem[other_prop];
    let max_iter = 2 * editing_copy.length;
    let final_val = 0;
    while (editing_copy.length > 1 && max_iter > 0) {
      const filtered_array = editing_copy.filter((elem) => {
        return elem[prop] == match;
      });
      if (filtered_array.length > 0) {
        iter++;
        const min_elem = d3.min(filtered_array, (elem) => {
          return elem[other_prop] as number;
        });
        const max_elem = d3.max(filtered_array, (elem) => {
          return elem[other_prop] as number;
        });
        if (min_elem < dim && max_elem > dim) {
          if (prop == 'y') {
            if (this.x_direction == 1) {
              final_val = max_elem;
            } else {
              final_val = min_elem;
            }
          } else {
            // There are elements greater than and lesser than
            // reference value. I am trying to see if there are
            // multiple elements greater or lesser. If there is
            // only one in one of the directions, that is the
            // direction I draw the line in.
            const lesser_arr = filtered_array.filter((elem) => {
              return elem[other_prop] < dim;
            });
            const greater_arr = filtered_array.filter((elem) => {
              return elem[other_prop] > dim;
            });

            if (lesser_arr.length == 1) {
              final_val = min_elem;
            } else if (greater_arr.length == 1) {
              final_val = max_elem;
            } else {
              final_val = d3.max(lesser_arr, (elem) => {
                return elem[other_prop] as number;
              });
            }
          }
        } else {
          if (min_elem > dim) {
            final_val = min_elem;
          } else {
            final_val = max_elem;
          }
        }
        const match_elem = editing_copy.filter((elem) => {
          return elem[prop] == match && elem[other_prop] == final_val;
        });
        match_elem.forEach((elem) => {
          editing_copy.splice(editing_copy.indexOf(elem), 1);
        });
        const value = {};
        value[prop] = match;
        value[other_prop] = final_val;
        values.push(value);
      } else {
        final_val = dim;
      }
      //swap prop and other_prop
      const temp = prop;
      prop = other_prop;
      other_prop = temp;

      dim = match;
      match = final_val;
      max_iter--;
    }
    if (editing_copy.length > 0) {
      values.push(editing_copy[0]);
    }
    values.push(end_points[0]);
    const line = d3
      .line()
      .curve(d3.curveLinear)
      .x((d: any) => {
        return d.x;
      })
      .y((d: any) => {
        return d.y;
      });
    const bounding_path = d3
      .select(elem)
      .append('path')
      .attr('class', 'bounding_path')
      .attr('d', () => {
        return line(values);
      })
      .attr('fill', 'none')
      .style('stroke', this.model.get('group_stroke'))
      .style('stroke-width', 3);
    return bounding_path;
  }

  calc_end_point_source(curr_x, curr_y, x_direction, y_direction) {
    curr_y = y_direction == 1 ? curr_y : curr_y + 1;
    curr_x = x_direction == 1 ? curr_x : curr_x + 1;
    return [{ x: curr_x * this.column_width, y: curr_y * this.row_height }];
  }

  calc_end_point_dest(curr_x, curr_y, x_direction, y_direction) {
    curr_y = y_direction == -1 ? curr_y : curr_y + 1;
    curr_x = x_direction == -1 ? curr_x : curr_x + 1;
    return [{ x: curr_x * this.column_width, y: curr_y * this.row_height }];
  }

  scales: Dict<Scale>;
  num_rows: number;
  num_cols: number;
  row_groups: number;
  enable_select: boolean;
  data: string[];
  fig_map: d3.Selection<SVGGElement, any, any, any>;
  fig_click: d3.Selection<SVGGElement, any, any, any>;
  fig_hover: d3.Selection<SVGGElement, any, any, any>;
  fig_names: d3.Selection<SVGGElement, any, any, any>;
  selected_stroke: string;
  hovered_stroke: string;
  column_width: number;
  row_height: number;
  prev_x: number;
  prev_y: number;
  y_direction: number;
  x_direction: number;
  group_iter: number;
  ref_data: any[];
  group_data: number[];
  groups: any[];
  colors: string[];
  colors_map: any;
  grouped_data: { [key: string]: { [key: string]: any }[] };
  running_sums: number[];
  tooltip_fields: (number | string)[];
  tooltip_formats: ((d: number | string) => number | string)[];
  rect_groups: d3.Selection<d3.BaseType, any, SVGGraphicsElement, any>;
  end_points: number[];
  tooltip_view: Tooltip;
  row_limits: number[];
  model: MarketMapModel;
}
