text: /**
   * Coverage visualization of Alignment sources.
   * @flow
   */
  'use strict';
  
  import type {Alignment} from '../Alignment';
  import type {DataSource} from '../sources/DataSource';
  import Feature from '../data/feature';
  import type {DataCanvasRenderingContext2D} from 'data-canvas';
  import type {BinSummary} from './CoverageCache';
  import type {State} from '../types';
  import type {VizProps} from '../VisualizationWrapper';
  
  import React from 'react';
  import scale from '../scale';
  import shallowEquals from 'shallow-equals';
  import d3utils from './d3utils';
  import _ from 'underscore';
  import dataCanvas from 'data-canvas';
  import canvasUtils from './canvas-utils';
  import CoverageCache from './CoverageCache';
  import TiledCanvas from './TiledCanvas';
  import style from '../style';
  import ContigInterval from '../ContigInterval';
  
  // Basic setup (TODO: make this configurable by the user)
  const SHOW_MISMATCHES = true;
  
  // Only show mismatch information when there are more than this many
  // reads supporting that mismatch.
  const MISMATCH_THRESHOLD = 1;
  
  
  class CoverageTiledCanvas extends TiledCanvas {
    height: number;
    options: Object;
    cache: CoverageCache< Alignment | Feature> ;
    maxCoverage: number;
  
    constructor(cache: CoverageCache<(Alignment | Feature)>, height: number, options: Object) {
      super();
  
      this.cache = cache;
      this.height = Math.max(1, height);
      this.options = options;
      this.maxCoverage = 0;
    }
  
    heightForRef(ref: string): number {
      return this.height;
    }
  
    update(height: number, options: Object) {
      // workaround for an issue in PhantomJS where height always comes out to zero.
      this.height = Math.max(1, height);
      this.options = options;
    }
  
    yScaleForRef(bottomPadding: number, topPadding:number): (y: number) => number {
      return scale.linear()
        .domain([Math.max(10, this.maxCoverage), 0]) // minimum coverage = 10
        .range([bottomPadding, this.height - topPadding])
        .nice();
    }
  
    render(ctx: DataCanvasRenderingContext2D,
           xScale: (x: number)=>number,
           range: ContigInterval<string>) {
      var bins = this.cache.binsForRef(range.contig);
      var yScale = this.yScaleForRef(10, 10);
      var relaxedRange = new ContigInterval(
          range.contig, range.start() - 1, range.stop() + 1);
      renderBars(ctx, xScale, yScale, relaxedRange, bins, this.options);
    }
  }
  
  
  // Draw coverage bins & mismatches
  function renderBars(ctx: DataCanvasRenderingContext2D,
                      xScale: (num: number) => number,
                      yScale: (num: number) => number,
                      range: ContigInterval<string>,
                      bins: {[key: number]: BinSummary},
                      options: Object) {
    if (_.isEmpty(bins)) return;
  
    var barWidth = xScale(1) - xScale(0);
    var showPadding = (barWidth > style.COVERAGE_MIN_BAR_WIDTH_FOR_GAP);
    var padding = showPadding ? 1 : 0;
  
    var binPos = function(pos: number, count: number) {
      // Round to integer coordinates for crisp lines, without aliasing.
      var barX1 = Math.round(xScale(1 + pos)),
          barX2 = Math.round(xScale(2 + pos)) - padding,
          barY = Math.round(yScale(count));
      return {barX1, barX2, barY};
    };
  
    var mismatchBins = ({} : {[key:number]: BinSummary});  // keep track of which ones have mismatches
    var vBasePosY = yScale(0);  // the very bottom of the canvas
    var start = range.start(),
        stop = range.stop();
  
    // track last genomic position
    var lastPos = null;
  
    for (var pos = start; pos < stop; pos++) {
      var bin = bins[pos];
      if (!bin) continue;
  
      // Restart path if first position or no bins before
      if (!lastPos || !bins[pos-1]) {
        let {barX1} = binPos(pos, bin.count);
        ctx.fillStyle = `rgba(${options.color.rgb.r}, ${options.color.rgb.g}, ${options.color.rgb.b}, ${options.color.rgb.a})`;
        ctx.beginPath();
        ctx.moveTo(barX1, vBasePosY);
      }
  
      ctx.pushObject(bin);
      let {barX1, barX2, barY} = binPos(pos, bin.count);
      ctx.lineTo(barX1, barY);
      ctx.lineTo(barX2, barY);
      if (showPadding) {
        ctx.lineTo(barX2, vBasePosY);
        ctx.lineTo(barX2 + 1, vBasePosY);
      }
      // close current path if next bin is going to be undefined
      if (!bins[pos+1]) {
        ctx.lineTo(barX2, vBasePosY);  // right edge of the right bar.
        ctx.closePath();
        ctx.fill();
      }
  
      if (SHOW_MISMATCHES && !_.isEmpty(bin.mismatches)) {
        mismatchBins[pos] = bin;
      }
      lastPos = pos;
      ctx.popObject();
    }
  
    if (lastPos) {
      let {barX2} = binPos(lastPos, 0); // count does not matter, drawing vBasePosY
      ctx.lineTo(barX2, vBasePosY);  // right edge of the right bar.
      ctx.closePath();
      ctx.fill();
    }
  
    // Now render the mismatches
    _.each(mismatchBins, (bin, pos) => {
      if (!bin.mismatches) return;  // this is here for Flow; it can't really happen.
      const mismatches = _.clone(bin.mismatches);
      pos = Number(pos);  // object keys are strings, not numbers.
  
      // If this is a high-frequency variant, add in the reference.
      var mismatchCount = _.reduce(mismatches, (x, y) => x + y);
      var mostFrequentMismatch = _.max(mismatches);
      if (mostFrequentMismatch > MISMATCH_THRESHOLD &&
          mismatchCount > options.vafColorThreshold * bin.count &&
          mismatchCount < bin.count) {
        if (bin.ref) {  // here for flow; can't realy happen
          mismatches[bin.ref] = bin.count - mismatchCount;
        }
      }
  
      let {barX1, barX2} = binPos(pos, bin.count);
      ctx.pushObject(bin);
      var countSoFar = 0;
      _.chain(mismatches)
        .map((count, base) => ({count, base}))  // pull base into the object
        .filter(({count}) => count > MISMATCH_THRESHOLD)
        .sortBy(({count}) => -count)  // the most common mismatch at the bottom
        .each(({count, base}) => {
          var misMatchObj = {position: 1 + pos, count, base};
          ctx.pushObject(misMatchObj);  // for debugging and click-tracking
  
          ctx.fillStyle = style.BASE_COLORS[base];
          var y = yScale(countSoFar);
          ctx.fillRect(barX1,
                       y,
                       Math.max(1, barX2 - barX1),  // min width of 1px
                       yScale(countSoFar + count) - y);
          countSoFar += count;
  
          ctx.popObject();
        });
      ctx.popObject();
    });
  }
  
  class CoverageTrack extends React.Component<VizProps<DataSource<Alignment | Feature>>, State> {
    props: VizProps<DataSource<Alignment | Feature >>;
    state: State; // no state, used to make flow happy
    cache: CoverageCache< Alignment | Feature >;
    tiles: CoverageTiledCanvas;
    ref: Object;
    static defaultOptions: Object;
    static getOptionsMenu: (options: Object) => any;
    static handleSelectOption: (item: Object, oldOptions: Object) => Object;
  
    constructor(props: VizProps<DataSource<Alignment | Feature>>) {
      super(props);
      this.ref = React.createRef();
    }
  
    render(): any {
      return <canvas ref={this.ref} onClick={this.handleClick.bind(this)} />;
    }
  
    getScale(): any {
      return d3utils.getTrackScale(this.props.range, this.props.width);
    }
  
    componentDidMount() {
      this.cache = new CoverageCache(this.props.referenceSource);
      this.tiles = new CoverageTiledCanvas(this.cache, this.props.height, this.props.options);
  
      this.props.source.on('newdata', range => {
        var oldMax = this.tiles.maxCoverage;
        this.props.source.getFeaturesInRange(range)
                         .forEach(read => this.cache.addItem(read));
        this.tiles.maxCoverage = this.cache.maxCoverageForRange(range);
  
        if (oldMax != this.tiles.maxCoverage) {
          this.tiles.invalidateAll();
        } else {
          this.tiles.invalidateRange(range);
        }
        this.visualizeCoverage();
      });
  
      this.props.referenceSource.on('newdata', range => {
        this.cache.updateMismatches(range);
        this.tiles.invalidateRange(range);
        this.visualizeCoverage();
      });
    }
  
    componentDidUpdate(prevProps: any, prevState: any) {
      if (!shallowEquals(this.props, prevProps) ||
          !shallowEquals(this.state, prevState)) {
        if (this.props.height != prevProps.height ||
            this.props.options != prevProps.options) {
          this.tiles.maxCoverage = this.cache.maxCoverageForRange(ContigInterval.fromGenomeRange(this.props.range));
          this.tiles.update(this.props.height, this.props.options);
          this.tiles.invalidateAll();
        }
        this.visualizeCoverage();
      }
    }
  
    getContext(): CanvasRenderingContext2D {
      const canvas = (this.ref.current : HTMLCanvasElement);
      // The typecast through `any` is because getContext could return a WebGL context.
      var ctx = ((canvas.getContext('2d') : any) : CanvasRenderingContext2D);
      return ctx;
    }
  
    // Draw three ticks on the left to set the scale for the user
    renderTicks(ctx: DataCanvasRenderingContext2D, yScale: any) {
      var axisMax = yScale.domain()[0];
      [0, Math.round(axisMax / 2), axisMax].forEach(tick => {
        // Draw a line indicating the tick
        ctx.pushObject({value: tick, type: 'tick'});
        var tickPosY = Math.round(yScale(tick));
        ctx.strokeStyle = style.COVERAGE_FONT_COLOR;
        canvasUtils.drawLine(ctx, 0, tickPosY, style.COVERAGE_TICK_LENGTH, tickPosY);
        ctx.popObject();
  
        var tickLabel = tick + 'X';
        ctx.pushObject({value: tick, label: tickLabel, type: 'label'});
        // Now print the coverage information
        ctx.font = style.COVERAGE_FONT_STYLE;
        var textPosX = style.COVERAGE_TICK_LENGTH + style.COVERAGE_TEXT_PADDING,
            textPosY = tickPosY + style.COVERAGE_TEXT_Y_OFFSET;
        // The stroke creates a border around the text to make it legible over the bars.
        ctx.strokeStyle = 'white';
        ctx.lineWidth = 2;
        ctx.strokeText(tickLabel, textPosX, textPosY);
        ctx.lineWidth = 1;
        ctx.fillStyle = style.COVERAGE_FONT_COLOR;
        ctx.fillText(tickLabel, textPosX, textPosY);
        ctx.popObject();
      });
    }
  
    visualizeCoverage() {
      const canvas = (this.ref.current : HTMLCanvasElement);
      var width = this.props.width,
          height = this.props.height,
          range = ContigInterval.fromGenomeRange(this.props.range);
  
      // Hold off until height & width are known.
      if (width === 0) return;
      d3utils.sizeCanvas(canvas, width, height);
  
      var ctx = dataCanvas.getDataContext(this.getContext());
      ctx.save();
      ctx.reset();
      ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  
      var yScale = this.tiles.yScaleForRef(10, 10);
  
      this.tiles.renderToScreen(ctx, range, this.getScale());
      this.renderTicks(ctx, yScale);
  
      ctx.restore();
    }
  
    handleClick(reactEvent: any) {
      var ev = reactEvent.nativeEvent,
          x = ev.offsetX;
  
      // It's simple to figure out which position was clicked using the x-scale.
      // No need to render the scene to determine what was clicked.
      var range = ContigInterval.fromGenomeRange(this.props.range),
          xScale = this.getScale(),
          bins = this.cache.binsForRef(range.contig),
          pos = Math.floor(xScale.invert(x)) - 1,
          bin = bins[pos];
  
      var alert = window.alert || console.log;
      if (bin) {
        var mmCount = bin.mismatches ? _.reduce(bin.mismatches, (a, b) => a + b) : 0;
        var ref = bin.ref || this.props.referenceSource.getRangeAsString(
            {contig: range.contig, start: pos, stop: pos});
  
        // Construct a JSON object to show the user.
        var messageObject = _.extend(
          {
            'position': range.contig + ':' + (1 + pos),
            'read depth': bin.count
          },
          bin.mismatches);
        messageObject[ref] = bin.count - mmCount;
        alert(JSON.stringify(messageObject, null, '  '));
      }
    }
  }
  
  CoverageTrack.displayName = 'coverage';
  CoverageTrack.defaultOptions = {
    // Color the reference base in the bar chart when the Variant Allele Fraction
    // exceeds this amount. When there are >=2 agreeing mismatches, they are
    // always rendered. But for mismatches below this threshold, the reference is
    // not colored in the bar chart. This draws attention to high-VAF mismatches.
    vafColorThreshold: 0.2,
    color: style.DEFAULT_COLORPICKER
  };
  
  CoverageTrack.getOptionsMenu = function(options: Object): any {
    return [
      {key: 'pick-color', label: 'Change track color', color: options.color}
    ];
  };
  
  CoverageTrack.handleSelectOption = function(item: Object, oldOptions: Object): Object {
      var opts = _.clone(oldOptions);
      if (item.key == "pick-color") {
        // This is all handled by the menu. Do nothing.
        opts.color = item.color;
        return opts;
      }
      return oldOptions;  // no change
  };
  
  
  module.exports = CoverageTrack;
  