text: /**
   * Copyright 2017 Google Inc.
   *
   * 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 {CachablePainting} from "../draw/CachablePainting.js"
  import {CircuitDefinition} from "../circuit/CircuitDefinition.js"
  import {CircuitStats} from "../circuit/CircuitStats.js"
  import {Config} from "../Config.js"
  import {DetailedError} from "../base/DetailedError.js"
  import {equate} from "../base/Equate.js"
  import {Format} from "../base/Format.js"
  import {GateColumn} from "../circuit/GateColumn.js"
  import {GateDrawParams} from "../draw/GateDrawParams.js"
  import {GatePainting} from "../draw/GatePainting.js"
  import {Hand} from "../ui/Hand.js"
  import {MathPainter} from "../draw/MathPainter.js"
  import {Point} from "../math/Point.js"
  import {Matrix} from "../math/Matrix.js"
  import {Rect} from "../math/Rect.js"
  import {Util} from "../base/Util.js"
  import {seq, Seq} from "../base/Seq.js"
  import {paintBlochSphereDisplay} from "../gates/BlochSphereDisplay.js"
  
  /** @type {!number} */
  let CIRCUIT_OP_HORIZONTAL_SPACING = 10;
  /** @type {!number} */
  let CIRCUIT_OP_LEFT_SPACING = 35;
  
  const SUPERPOSITION_GRID_LABEL_SPAN = 50;
  
  const EXTRA_COLS_FOR_SINGLE_QUBIT_DISPLAYS = 2;
  
  class DisplayedCircuit {
      /**
       *
       * @param {!number} top
       * @param {!CircuitDefinition} circuitDefinition
       * @param {undefined|!int} compressedColumnIndex
       * @param {undefined|!{col: !int, row: undefined|!int, resizeStyle: !boolean}} highlightedSlot
       * @param {undefined|!int} extraWireStartIndex
       * @private
       */
      constructor(top, circuitDefinition, compressedColumnIndex, highlightedSlot, extraWireStartIndex) {
          if (!Number.isFinite(top)) {
              throw new DetailedError("Bad top", {top, circuitDefinition});
          }
          if (!(circuitDefinition instanceof CircuitDefinition)) {
              throw new DetailedError("Bad circuitDefinition", {top, circuitDefinition});
          }
          /**
           * @type {!number}
           */
          this.top = top;
          /**
           * @type {!CircuitDefinition}
           */
          this.circuitDefinition = circuitDefinition;
          /**
           * @type {undefined|!int}
           * @private
           */
          this._compressedColumnIndex = compressedColumnIndex;
          /**
           * @type {undefined|!{col: !int, row: undefined|!int, resizeStyle: !boolean}}
           * @private
           */
          this._highlightedSlot = highlightedSlot;
          /**
           * @type {undefined|!int}
           * @private
           */
          this._extraWireStartIndex = extraWireStartIndex;
      }
  
      /**
       * @param {!number} top
       * @returns {!DisplayedCircuit}
       */
      static empty(top) {
          return new DisplayedCircuit(
              top,
              new CircuitDefinition(Config.MIN_WIRE_COUNT, []),
              undefined,
              undefined,
              undefined);
      }
  
      /**
       * @returns {!boolean}
       */
      isBeingEdited() {
          return this._extraWireStartIndex !== undefined;
      }
  
      /**
       * The number of wires that were in the circuit before picking up a gate, or the number that will be in the circuit
       * after dropping a gate; whichever is larger.
       * @returns {!int}
       * @private
       */
      _groundedWireCount() {
          let pseudoCount =
              this._extraWireStartIndex !== undefined && this._extraWireStartIndex !== Config.MAX_WIRE_COUNT ? 1 : 0;
  
          let n = Math.max(Config.MIN_WIRE_COUNT, this.circuitDefinition.numWires) - pseudoCount;
          return Math.max(n, this.circuitDefinition.minimumRequiredWireCount());
      }
  
      /**
       * @param {!boolean=true} forTooltip
       * @returns {!number}
       */
      desiredHeight(forTooltip=false) {
          if (forTooltip) {
              return this.circuitDefinition.numWires * Config.WIRE_SPACING;
          }
          return this._groundedWireCount() * Config.WIRE_SPACING + 55;
      }
  
      /**
       * @param {!boolean=true} forTooltip
       * @returns {!number}
       */
      desiredWidth(forTooltip=false) {
          if (forTooltip) {
              return this.opRect(this.circuitDefinition.columns.length - 1).right() + CIRCUIT_OP_LEFT_SPACING;
          }
          return this._rectForSuperpositionDisplay().right() + 101;
      }
  
      /**
       * @param {!int} wireIndex
       * @returns {!Rect}
       */
      wireRect(wireIndex) {
          if (wireIndex < 0) {
              throw new DetailedError("Bad wireIndex", {wireIndex});
          }
          return new Rect(0, this.top + Config.WIRE_SPACING * wireIndex, Infinity, Config.WIRE_SPACING);
      }
  
      /**
       * @param {!number} y
       * @returns {!int}
       */
      wireIndexAt(y) {
          return Math.floor((y - this.top) / Config.WIRE_SPACING);
      }
  
      //noinspection JSMethodCanBeStatic
      /**
       * @param {!number} x
       * @returns {!number} The continuous column-space coordinate corresponding to the given display-space coordinate.
       * @private
       */
      toColumnSpaceCoordinate(x) {
          let spacing = (CIRCUIT_OP_HORIZONTAL_SPACING + Config.GATE_RADIUS * 2);
          let left = CIRCUIT_OP_LEFT_SPACING - CIRCUIT_OP_HORIZONTAL_SPACING / 2;
          return (x - left) / spacing - 0.5;
      }
  
      /**
       * @param {!number} y
       * @returns {undefined|!int}
       */
      indexOfDisplayedRowAt(y) {
          let i = Math.floor((y - this.top) / Config.WIRE_SPACING);
          if (i < 0 || i >= this.circuitDefinition.numWires) {
              return undefined;
          }
          return i;
      }
  
      /**
       * @param {!number} x
       * @returns {undefined|!int}
       */
      indexOfDisplayedColumnAt(x) {
          let col = this.toColumnSpaceCoordinate(x);
          let i;
          if (this._compressedColumnIndex === undefined || col < this._compressedColumnIndex - 0.75) {
              i = Math.round(col);
          } else if (col < this._compressedColumnIndex - 0.25) {
              i = this._compressedColumnIndex;
          } else {
              i = Math.round(col) - 1;
          }
  
          if (i < 0 || i >= this.circuitDefinition.columns.length) {
              return undefined;
          }
  
          return i;
      }
  
      /**
       * @param {!Point} p
       * @returns {undefined|!number}
       */
      findOpHalfColumnAt(p) {
          if (p.x < 0 || p.y < this.top || p.y > this.top + this.desiredHeight()) {
              return undefined;
          }
  
          return Math.max(-0.5, Math.round(this.toColumnSpaceCoordinate(p.x) * 2) / 2);
      }
  
      /**
       * @param {!Hand} hand
       * @returns {undefined|!{col: !int, row: !int, halfColIndex: !number}}
       * @private
       */
      _findModificationIndex_helperColRow(hand) {
          if (hand.pos === undefined || hand.heldGate === undefined) {
              return undefined;
          }
          let pos = hand.pos.minus(hand.holdOffset).plus(new Point(Config.GATE_RADIUS, Config.GATE_RADIUS));
          let halfColIndex = this.findOpHalfColumnAt(pos);
          let row = this.indexOfDisplayedRowAt(pos.y);
          if (halfColIndex === undefined || row === undefined) {
              return undefined;
          }
          let col = Math.ceil(halfColIndex);
          return {col, row, halfColIndex};
      }
  
      /**
       * @param {!Hand} hand
       * @returns {?{ col : !number, row : !number, isInsert : !boolean }}
       */
      findModificationIndex(hand) {
          let loc = this._findModificationIndex_helperColRow(hand);
          if (loc === undefined) {
              return undefined;
          }
          let {col, row, halfColIndex} = loc;
  
          let isInsert = Math.abs(halfColIndex % 1) === 0.5;
          if (col >= this.circuitDefinition.columns.length) {
              return {col: col, row: row, isInsert: isInsert};
          }
  
          if (!isInsert) {
              let mustInsert = this.circuitDefinition.isSlotRectCoveredByGateInSameColumn(
                  col, row, hand.heldGate.height);
              if (mustInsert) {
                  let isAfter = hand.pos.x > this.opRect(col).center().x;
                  isInsert = true;
                  if (isAfter) {
                      col += 1;
                  }
              }
          }
  
          return {col: col, row: row, isInsert: isInsert};
      }
  
      /**
       * @param {!int} operationIndex
       * @returns {Rect!}
       */
      opRect(operationIndex) {
          let opWidth = Config.GATE_RADIUS * 2;
          let opSeparation = opWidth + CIRCUIT_OP_HORIZONTAL_SPACING;
          let tweak = 0;
          if (this._compressedColumnIndex !== undefined && operationIndex === this._compressedColumnIndex) {
              tweak = opSeparation / 2;
          }
          if (this._compressedColumnIndex !== undefined && operationIndex > this._compressedColumnIndex) {
              tweak = opSeparation;
          }
  
          let dx = opSeparation * operationIndex - tweak + CIRCUIT_OP_LEFT_SPACING;
          return new Rect(dx, this.top, opWidth, this.desiredHeight());
      }
  
      /**
       * @param {!int} wireIndex
       * @param {!int} operationIndex
       * @param {!int=} width
       * @param {!int=} height
       */
      gateRect(wireIndex, operationIndex, width=1, height=1) {
          let op = this.opRect(operationIndex);
          let wire = this.wireRect(wireIndex);
          let r = new Rect(
              op.center().x - Config.GATE_RADIUS,
              wire.center().y - Config.GATE_RADIUS,
              2*Config.GATE_RADIUS + (width-1)*Config.WIRE_SPACING,
              2*Config.GATE_RADIUS + (height-1)*Config.WIRE_SPACING);
  
          return new Rect(Math.round(r.x - 0.5) + 0.5, Math.round(r.y - 0.5) + 0.5, Math.round(r.w), Math.round(r.h));
      }
  
      /**
       * @returns {!DisplayedCircuit}
       */
      afterTidyingUp() {
          return this.
              withCircuit(this.circuitDefinition.
                  withUncoveredColumnsRemoved().
                  withHeightOverlapsFixed().
                  withWidthOverlapsFixed().
                  withUncoveredColumnsRemoved().
                  withTrailingSpacersIncluded()).
              _withCompressedColumnIndex(undefined).
              _withExtraWireStartIndex(undefined).
              _withHighlightedSlot(undefined);
      }
  
      /**
       * @param {!DisplayedCircuit|*} other
       * @returns {!boolean}
       */
      isEqualTo(other) {
          if (this === other) {
              return true;
          }
          return other instanceof DisplayedCircuit &&
              this.top === other.top &&
              this.circuitDefinition.isEqualTo(other.circuitDefinition) &&
              this._compressedColumnIndex === other._compressedColumnIndex &&
              this._extraWireStartIndex === other._extraWireStartIndex &&
              equate(this._highlightedSlot, other._highlightedSlot);
      }
  
      /**
       * @param {!Painter} painter
       * @param {!Hand} hand
       * @param {!CircuitStats} stats
       * @param {!boolean=false} forTooltip
       * @param {!boolean} showWires
       */
      paint(painter, hand, stats, forTooltip=false, showWires=true) {
          if (showWires) {
              this._drawWires(painter, !forTooltip, hand);
          }
  
          for (let col = 0; col < this.circuitDefinition.columns.length; col++) {
              this._drawColumn(painter, this.circuitDefinition.columns[col], col, hand, stats);
          }
  
          if (!forTooltip) {
              this._drawOutputDisplays(painter, stats, hand);
              this._drawHintLabels(painter, stats);
          }
  
          this._drawRowDragHighlight(painter);
      }
  
      /**
       * @param {!Painter} painter
       * @param {!boolean} showLabels
       * @param {!Hand} hand
       * @private
       */
      _drawWires(painter, showLabels, hand) {
          let drawnWireCount = Math.min(this.circuitDefinition.numWires, (this._extraWireStartIndex || Infinity) + 1);
  
          // Initial value labels
          if (showLabels) {
              for (let row = 0; row < drawnWireCount; row++) {
                  let wireRect = this.wireRect(row);
                  let y = wireRect.center().y;
                  let v = this.circuitDefinition.customInitialValues.get(row);
                  if (v === undefined) {
                      v = '0';
                  }
                  let rect = this._wireInitialStateClickableRect(row);
                  painter.noteTouchBlocker({rect, cursor: 'pointer'});
                  if (this._highlightedSlot === undefined && hand.pos !== undefined && rect.containsPoint(hand.pos)) {
                      painter.fillRect(rect, Config.HIGHLIGHTED_GATE_FILL_COLOR);
                  }
                  painter.print(`|${v}⟩`, 20, y, 'right', 'middle', 'black', '14px sans-serif', 20, Config.WIRE_SPACING);
              }
          }
  
          // Wires (doubled-up for measured sections).
          painter.ctx.save();
          for (let row = 0; row < drawnWireCount; row++) {
              if (row === this._extraWireStartIndex) {
                  painter.ctx.globalAlpha *= 0.5;
              }
              painter.trace(trace => {
                  let wireRect = this.wireRect(row);
                  let y = Math.round(wireRect.center().y - 0.5) + 0.5;
                  let lastX = showLabels ? 25 : 5;
                  //noinspection ForLoopThatDoesntUseLoopVariableJS
                  for (let col = 0;
                          showLabels ? lastX < painter.canvas.width : col <= this.circuitDefinition.columns.length;
                          col++) {
                      let x = this.opRect(col).center().x;
                      if (this.circuitDefinition.locIsMeasured(new Point(col, row))) {
                          // Measured wire.
                          trace.line(lastX, y-1, x, y-1);
                          trace.line(lastX, y+1, x, y+1);
                      } else {
                          // Unmeasured wire.
                          trace.line(lastX, y, x, y);
                      }
                      lastX = x;
                  }
              }).thenStroke('black');
          }
          painter.ctx.restore();
          if (this._extraWireStartIndex !== undefined && this.circuitDefinition.numWires === Config.MAX_WIRE_COUNT) {
              painter.print(
                  `(Max wires. Qubit limit is ${Config.MAX_WIRE_COUNT}.)`,
                  5,
                  this.wireRect(Config.MAX_WIRE_COUNT).y,
                  'left',
                  'top',
                  'red',
                  '16px bold monospace',
                  400,
                  Config.WIRE_SPACING);
          }
      }
  
      /**
       * @param {!int} col
       * @param {!int} row
       * @param {!Array.<!Point>} focusPosPts
       * @returns {!{isHighlighted: !boolean, isResizeShowing: !boolean, isResizeHighlighted: !boolean}}
       * @private
       */
      _highlightStatusAt(col, row, focusPosPts) {
          if (this._highlightedSlot !== undefined) {
              if (this._highlightedSlot.col === col && this._highlightedSlot.row === row) {
                  return {
                      isResizeShowing: true,
                      isResizeHighlighted: this._highlightedSlot.resizeStyle,
                      isHighlighted: !this._highlightedSlot.resizeStyle
                  };
              }
          }
  
          let gate = this.circuitDefinition.gateInSlot(col, row);
          if (gate === undefined || this._highlightedSlot !== undefined) {
              return {
                  isResizeShowing: false,
                  isResizeHighlighted: false,
                  isHighlighted: false
              };
          }
  
          let gateRect = this.gateRect(row, col, gate.width, gate.height);
          let resizeTabRect = GatePainting.rectForResizeTab(gateRect);
  
          let isOverGate = pos => {
              let overGate = this.findGateOverlappingPos(pos);
              return overGate !== undefined && overGate.col === col && overGate.row === row;
          };
          let isNotCoveredAt = pos => {
              let g = this.findGateOverlappingPos(pos);
              return g === undefined || (g.col === col && g.row === row);
          };
          let isOverGateResizeTab = pos => isNotCoveredAt(pos) && resizeTabRect.containsPoint(pos);
  
          let isResizeHighlighted = gate.canChangeInSize() && seq(focusPosPts).any(isOverGateResizeTab);
          let isHighlighted = !isResizeHighlighted && seq(focusPosPts).any(isOverGate);
          let isResizeShowing = gate.canChangeInSize() && (isResizeHighlighted || isHighlighted);
  
          return {isHighlighted, isResizeShowing, isResizeHighlighted};
      }
  
      /**
       * @param {!Painter} painter
       * @param {!int} col
       * @param {!int} row
       * @param {!Rect} gateRect
       * @param {!boolean} isHighlighted
       * @private
       */
      _drawGate_disabledReason(painter, col, row, gateRect, isHighlighted) {
          let isDisabledReason = this.circuitDefinition.gateAtLocIsDisabledReason(col, row);
          if (isDisabledReason === undefined) {
              return;
          }
  
          painter.ctx.save();
          if (isHighlighted) {
              painter.ctx.globalAlpha *= 0.3;
          }
          painter.ctx.globalAlpha *= 0.5;
          painter.fillRect(gateRect.paddedBy(5), 'yellow');
          painter.ctx.globalAlpha *= 2;
          painter.strokeLine(gateRect.topLeft(), gateRect.bottomRight(), 'orange', 3);
          let r = painter.printParagraph(isDisabledReason, gateRect.paddedBy(5), new Point(0.5, 0.5), 'red');
          painter.ctx.globalAlpha *= 0.5;
          painter.fillRect(r.paddedBy(2), 'yellow');
          painter.ctx.globalAlpha *= 2;
          painter.printParagraph(isDisabledReason, gateRect.paddedBy(5), new Point(0.5, 0.5), 'red');
          painter.ctx.restore()
      }
  
      /**
       * @param {!Painter} painter
       * @param {!GateColumn} gateColumn
       * @param {!int} col
       * @param {!Hand} hand
       * @param {!CircuitStats} stats
       * @private
       */
      _drawColumn(painter, gateColumn, col, hand, stats) {
          this._drawColumnControlWires(painter, col);
          this._drawColumnDragHighlight(painter, col);
  
          for (let row = 0; row < this.circuitDefinition.numWires; row++) {
              if (gateColumn.gates[row] === undefined) {
                  continue;
              }
              let gate = gateColumn.gates[row];
              let gateRect = this.gateRect(row, col, gate.width, gate.height);
  
              let {isHighlighted, isResizeShowing, isResizeHighlighted} =
                  this._highlightStatusAt(col, row, hand.hoverPoints());
  
              let drawer = gate.customDrawer || GatePainting.DEFAULT_DRAWER;
              painter.noteTouchBlocker({rect: gateRect, cursor: 'pointer'});
              if (gate.canChangeInSize()) {
                  painter.noteTouchBlocker({rect: GatePainting.rectForResizeTab(gateRect), cursor: 'ns-resize'});
              }
              drawer(new GateDrawParams(
                  painter,
                  hand,
                  false,
                  isHighlighted && !isResizeHighlighted,
                  isResizeShowing,
                  isResizeHighlighted,
                  gateRect,
                  gate,
                  stats,
                  {row, col},
                  this._highlightedSlot === undefined ? hand.hoverPoints() : [],
                  stats.customStatsForSlot(col, row)));
  
              this._drawGate_disabledReason(painter, col, row, gateRect, isHighlighted);
          }
  
          this._drawColumnSurvivalRate(painter, gateColumn, col, stats);
      }
  
      /**
       * @param {!Painter} painter
       * @param {!GateColumn} gateColumn
       * @param {!int} col
       * @param {!CircuitStats} stats
       * @private
       */
      _drawColumnSurvivalRate(painter, gateColumn, col, stats) {
          if (gateColumn.indexOfNonUnitaryGate() === undefined) {
              return;
          }
  
          let preRate = stats.survivalRate(col - 1);
          let postRate = stats.survivalRate(col);
  
          let marginalRate = (postRate - preRate) / preRate;
          if (isNaN(marginalRate) || Math.abs(marginalRate) <= 0.005) {
              return;
          }
  
          let descAmount;
          let descCategory;
          if (marginalRate < 0) {
              let rate = Math.round(-marginalRate * 100);
              let rateDesc = marginalRate === -1 ? "100" : rate < 100 ? rate : ">99";
              descAmount = `${rateDesc}%`;
              descCategory = 'omits';
          } else {
              let factor = Math.round(marginalRate * 100 + 100);
              descAmount = `${factor}%`;
              descCategory = 'gains';
          }
  
          let pt = this.opRect(col).bottomCenter();
          painter.print(
              descCategory,
              pt.x,
              pt.y - 28,
              'center',
              'bottom',
              'red',
              '14px sans-serif',
              800,
              50);
          painter.print(
              descAmount,
              pt.x,
              pt.y - 13,
              'center',
              'bottom',
              'red',
              '14px sans-serif',
              800,
              50);
      }
  
      _drawColumnDragHighlight(painter, col) {
          if (this._highlightedSlot !== undefined &&
              this._highlightedSlot.col === col &&
              this._highlightedSlot.row === undefined) {
              let rect = this.gateRect(0, col, 1, this._groundedWireCount()).paddedBy(3);
              painter.fillRect(rect, 'rgba(255, 196, 112, 0.7)');
              painter.strokeRect(rect, 'black');
          }
      }
  
      /**
       * @param {!Painter} painter
       * @private
       */
      _drawRowDragHighlight(painter) {
          if (this._highlightedSlot !== undefined &&
                  this._highlightedSlot.col === undefined &&
                  this._highlightedSlot.row !== undefined) {
  
              let row = this._highlightedSlot.row;
              let w = this.gateRect(row, this.clampedCircuitColCount() + 1).x;
              let rect = this.wireRect(row).takeLeft(w);
              painter.fillRect(rect, 'rgba(255, 196, 112, 0.7)');
              painter.strokeRect(rect, 'black');
          }
      }
  
      /**
       * @param {!Painter} painter
       * @param {!int} columnIndex
       * @private
       */
      _drawColumnControlWires(painter, columnIndex) {
          let x = Math.round(this.opRect(columnIndex).center().x - 0.5) + 0.5;
  
          // Dashed line indicates effects from non-unitary gates may affect, or appear to affect, other wires.
          if (this.circuitDefinition.columns[columnIndex].hasGatesWithGlobalEffects()) {
              painter.ctx.save();
              painter.ctx.setLineDash([1, 4]);
              painter.strokeLine(
                  new Point(x, this.gateRect(0, 0).y),
                  new Point(x, this.opRect(0).bottom() - 40));
              painter.ctx.restore();
          }
  
          for (let {first, last, measured} of this.circuitDefinition.controlLinesRanges(columnIndex)) {
              let y1 =  this.wireRect(first).center().y;
              let y2 = this.wireRect(last).center().y;
              if (measured) {
                  painter.strokeLine(new Point(x+1, y1), new Point(x+1, y2));
                  painter.strokeLine(new Point(x-1, y1), new Point(x-1, y2));
              } else {
                  painter.strokeLine(new Point(x, y1), new Point(x, y2));
              }
          }
      }
  
      /**
       * @param {!Hand} hand
       * @returns {!DisplayedCircuit}
       */
      previewDrop(hand) {
          return hand.heldRow !== undefined ? this._previewDropMovedRow(hand) :
              hand.heldColumn !== undefined ? this._previewDropMovedGateColumn(hand) :
              hand.heldGate !== undefined ? this._previewDropMovedGate(hand) :
              this._previewResizedGate(hand);
      }
  
      /**
       * @param {!Hand} hand
       * @returns {!DisplayedCircuit}
       * @private
       */
      _previewDropMovedRow(hand) {
          if (hand.pos === undefined) {
              return this;
          }
          let handWire = this.wireIndexAt(hand.pos.y);
          if (handWire < 0 || handWire >= this.circuitDefinition.numWires) {
              // Dragged the row out of the circuit.
              return this;
          }
  
          let heldRowHeight = seq(hand.heldRow.gates).map(g => g === undefined ? 1 : g.height).max(1);
          handWire = Math.min(handWire, this.circuitDefinition.numWires - heldRowHeight);
  
          let newCols = [];
          for (let c = 0; c < this.circuitDefinition.columns.length; c++) {
              let gates = [...this.circuitDefinition.columns[c].gates];
              gates.splice(handWire, 0, hand.heldRow.gates[c]);
              gates.pop();
              newCols.push(new GateColumn(gates));
          }
  
          let newInitialStates = seq(this.circuitDefinition.customInitialValues.entries()).
              map(([k, v]) => [k + (k >= handWire ? 1 : 0), v]).
              toMap(([k, _]) => k, ([_, v]) => v);
          if (hand.heldRow.initialState !== undefined) {
              newInitialStates.set(handWire, hand.heldRow.initialState);
          }
          let newCircuitDef = this.circuitDefinition.withColumns(newCols).withInitialStates(newInitialStates);
  
          return this.withCircuit(newCircuitDef).
              _withHighlightedSlot({row: handWire, col: undefined, resizeStyle: false});
      }
  
      /**
       * @param {!Hand} hand
       * @returns {!DisplayedCircuit}
       * @private
       */
      _previewDropMovedGateColumn(hand) {
          if (hand.pos === undefined) {
              return this;
          }
          let handWire = this.wireIndexAt(hand.pos.y);
          if (handWire < 0 || handWire >= Config.MAX_WIRE_COUNT || hand.pos.x <= 1) {
              // Dragged the gate column out of the circuit.
              return this;
          }
  
  
          let halfCol = this.findOpHalfColumnAt(new Point(hand.pos.x, this.top));
          let mustInsert = halfCol % 1 === 0 &&
              this.circuitDefinition.columns[halfCol] !== undefined &&
              !this.circuitDefinition.columns[halfCol].isEmpty();
          if (mustInsert) {
              let isAfter = hand.pos.x > this.opRect(halfCol).center().x;
              halfCol += isAfter ? 0.5 : -0.5;
          }
  
          let col = Math.ceil(halfCol);
          let isInsert = halfCol % 1 !== 0;
  
          let rowShift = Math.round((hand.pos.y - hand.holdOffset.y - this.top) / Config.WIRE_SPACING);
          let newCircuitDef = this._shiftAndSpliceColumn(rowShift, [...hand.heldColumn.gates], col, isInsert);
  
          return this.withCircuit(newCircuitDef).
              _withHighlightedSlot({row: undefined, col, resizeStyle: false}).
              _withCompressedColumnIndex(isInsert ? col : undefined);
      }
  
      _shiftAndSpliceColumn(rowShift, gatesOfCol, insertCol, isInsert) {
          // Move gates upward.
          while (rowShift < 0 && gatesOfCol[0] === undefined) {
              gatesOfCol.shift();
              gatesOfCol.push(undefined);
              rowShift += 1;
          }
  
          // Shift gates downward.
          while (rowShift > 0 && new GateColumn(gatesOfCol).minimumRequiredWireCount() < Config.MAX_WIRE_COUNT) {
              gatesOfCol.unshift(undefined);
              if (new GateColumn(gatesOfCol).minimumRequiredWireCount() < gatesOfCol.length) {
                  gatesOfCol.pop();
              }
              rowShift -= 1;
          }
  
          let expandedCircuit = this.circuitDefinition.withWireCount(gatesOfCol.length);
          let newCols = [...expandedCircuit.columns];
  
          // Move displays rightward.
          while (newCols.length < insertCol) {
              newCols.push(GateColumn.empty(expandedCircuit.numWires));
          }
  
          newCols.splice(insertCol, isInsert ? 0 : 1, new GateColumn(gatesOfCol));
          return expandedCircuit.withColumns(newCols).withTrailingSpacersIncluded();
      }
  
      /**
       * @param {!Hand} hand
       * @returns {!DisplayedCircuit}
       * @private
       */
      _previewDropMovedGate(hand) {
          let modificationPoint = this.findModificationIndex(hand);
          if (modificationPoint === undefined) {
              return this;
          }
  
          // Use the grab offset instead of the gate height so that tall gates are 'sticky' when dragging downward: they
          // aren't removed until the hand actually leaves the circuit area.
          let handRowOffset = Math.floor(hand.holdOffset.y/Config.WIRE_SPACING);
          if (modificationPoint.row + handRowOffset >= this.circuitDefinition.numWires) {
              return this;
          }
  
          let addedGate = hand.heldGate;
  
          let emptyCol = GateColumn.empty(this.circuitDefinition.numWires);
          let i = modificationPoint.col;
          let isInserting = modificationPoint.isInsert;
          let row = Math.min(modificationPoint.row, Math.max(0, Config.MAX_WIRE_COUNT - addedGate.height));
          let newCols = seq(this.circuitDefinition.columns).
              padded(i, emptyCol).
              ifThen(isInserting, s => s.withInsertedItem(i, emptyCol)).
              padded(i + addedGate.width, emptyCol).
              withTransformedItem(i, c => c.withGatesAdded(row, new GateColumn([addedGate]))).
              toArray();
          let newWireCount = Math.max(
              this._extraWireStartIndex || 0,
              Math.max(
                  this.circuitDefinition.numWires,
                  addedGate.height + row));
          if (newWireCount > Config.MAX_WIRE_COUNT) {
              return this;
          }
  
          let newCircuitDef = this.circuitDefinition.
              withColumns(newCols).
              withWireCount(newWireCount);
          return this.withCircuit(newCircuitDef).
              _withHighlightedSlot({row, col: modificationPoint.col, resizeStyle: false}).
              _withCompressedColumnIndex(isInserting ? i : undefined).
              _withFallbackExtraWireStartIndex(this.circuitDefinition.numWires);
      }
  
      /**
       * @param {!Hand} hand
       * @returns {!DisplayedCircuit}
       * @private
       */
      _previewResizedGate(hand) {
          if (hand.resizingGateSlot === undefined || hand.pos === undefined) {
              return this;
          }
          let gate = this.circuitDefinition.gateInSlot(hand.resizingGateSlot.x, hand.resizingGateSlot.y);
          if (gate === undefined) {
              return this;
          }
          let row = Math.min(
              this.wireIndexAt(hand.pos.y - hand.holdOffset.y),
              Config.MAX_WIRE_COUNT - 1);
          let newGate = seq(gate.gateFamily).minBy(g => Math.abs(g.height - (row - hand.resizingGateSlot.y + 1)));
          let newWireCount = Math.min(Config.MAX_WIRE_COUNT,
              Math.max(this.circuitDefinition.numWires, newGate.height + hand.resizingGateSlot.y));
          let newCols = seq(this.circuitDefinition.columns).
              withTransformedItem(hand.resizingGateSlot.x,
                  colObj => new GateColumn(seq(colObj.gates).
                      withOverlayedItem(hand.resizingGateSlot.y, newGate).
                      toArray())).
              toArray();
  
          let newCircuitWithoutOverlapFix = this.circuitDefinition.withColumns(newCols).withWireCount(newWireCount);
          let newCircuitWithOverlapFix = newCircuitWithoutOverlapFix.withHeightOverlapsFixed();
          let newCircuit = newCircuitWithOverlapFix.withTrailingSpacersIncluded();
          return this.withCircuit(newCircuit).
              _withHighlightedSlot(this._highlightedSlot).
              _withCompressedColumnIndex(newCircuitWithoutOverlapFix.isEqualTo(newCircuitWithOverlapFix) ?
                  undefined :
                  hand.resizingGateSlot.x + 1).
              _withFallbackExtraWireStartIndex(this.circuitDefinition.numWires);
      }
  
      /**
       * @param {!Hand} hand
       * @returns {!DisplayedCircuit}
       */
      afterDropping(hand) {
          return this.previewDrop(hand)._withCompressedColumnIndex(undefined);
      }
  
      /**
       * @param {!CircuitDefinition} circuitDefinition
       * @returns {!DisplayedCircuit}
       */
      withCircuit(circuitDefinition) {
          return new DisplayedCircuit(
              this.top,
              circuitDefinition,
              this._compressedColumnIndex,
              this._highlightedSlot,
              this._extraWireStartIndex);
      }
  
      /**
       * @param {undefined|!int} compressedColumnIndex
       * @returns {!DisplayedCircuit}
       * @private
       */
      _withCompressedColumnIndex(compressedColumnIndex) {
          return new DisplayedCircuit(
              this.top,
              this.circuitDefinition,
              compressedColumnIndex,
              this._highlightedSlot,
              this._extraWireStartIndex);
      }
  
      /**
       * @param {undefined|!{col: undefined|!int, row: undefined|!int, resizeStyle: !boolean}} slot
       * @returns {!DisplayedCircuit}
       * @private
       */
      _withHighlightedSlot(slot) {
          return new DisplayedCircuit(
              this.top,
              this.circuitDefinition,
              this._compressedColumnIndex,
              slot,
              this._extraWireStartIndex);
      }
  
      /**
       * @param {undefined|!int} extraWireStartIndex
       * @returns {!DisplayedCircuit}
       * @private
       */
      _withExtraWireStartIndex(extraWireStartIndex) {
          return new DisplayedCircuit(
              this.top,
              this.circuitDefinition,
              this._compressedColumnIndex,
              this._highlightedSlot,
              extraWireStartIndex);
      }
  
      /**
       * @param {undefined|!int} fallbackExtraWireStartIndex
       * @returns {!DisplayedCircuit}
       * @private
       */
      _withFallbackExtraWireStartIndex(fallbackExtraWireStartIndex) {
          return this._withExtraWireStartIndex(this._extraWireStartIndex || fallbackExtraWireStartIndex);
      }
  
      /**
       * @param {!Hand} hand
       * @param {!int} extraWireCount
       * @returns {!DisplayedCircuit}
       */
      withJustEnoughWires(hand, extraWireCount) {
          let neededWireCountForPlacement = hand.heldGate !== undefined ? hand.heldGate.height : 0;
          let desiredWireCount = this.circuitDefinition.minimumRequiredWireCount();
          let clampedWireCount = Math.min(
              Config.MAX_WIRE_COUNT,
              Math.max(
                  Math.min(1, neededWireCountForPlacement),
                  Math.max(Config.MIN_WIRE_COUNT, desiredWireCount) + extraWireCount));
          return this.withCircuit(this.circuitDefinition.withWireCount(clampedWireCount)).
              _withExtraWireStartIndex(extraWireCount === 0 ? undefined : this.circuitDefinition.numWires);
      }
  
      /**
       * @param {!Point} pos
       * @returns {undefined|!{col: !int, row: !int, offset: !Point}}
       */
      findGateOverlappingPos(pos) {
          let col = this.indexOfDisplayedColumnAt(pos.x);
          let row = this.indexOfDisplayedRowAt(pos.y);
          if (col === undefined || row === undefined) {
              return undefined;
          }
  
          let target = this.circuitDefinition.findGateCoveringSlot(col, row);
          if (target === undefined) {
              return undefined;
          }
  
          let gateRect = this.gateRect(target.row, target.col, target.gate.width, target.gate.height);
          if (!gateRect.containsPoint(pos)) {
              return undefined;
          }
  
          return {col: target.col, row: target.row, offset: pos.minus(gateRect.topLeft())};
      }
  
      /**
       * @param {!Point} pos
       * @returns {undefined|!{col: !int, row: !int, gate: !Gate}}
       */
      findGateWithButtonContaining(pos) {
          let foundPt = this.findGateOverlappingPos(pos);
          if (foundPt === undefined) {
              return undefined;
          }
  
          let gate = this.circuitDefinition.gateInSlot(foundPt.col, foundPt.row);
          if (gate.onClickGateFunc === undefined) {
              return undefined;
          }
  
          let buttonRect = GatePainting.gateButtonRect(this.gateRect(foundPt.row, foundPt.col, gate.width, gate.height));
          if (!buttonRect.containsPoint(pos)) {
              return undefined;
          }
  
          return {col: foundPt.col, row: foundPt.row, gate};
      }
  
      /**
       * @param {!int} wire
       * @returns {!Rect}
       * @private
       */
      _wireInitialStateClickableRect(wire) {
          let r = this.wireRect(wire);
          r.x = 0;
          r.y += 5;
          r.w = 30;
          r.h -= 10;
          return r;
      }
  
      /**
       * @param {!Point} pt
       * @returns {undefined|!int}
       */
      findWireWithInitialStateAreaContaining(pt) {
          // Is it in the right vertical band; the one at the start of the circuit?
          if (pt.x < 0 || pt.x > 30) {
              return undefined;
          }
  
          // Which wire is it? Is it one that's actually in the circuit?
          let wire = this.wireIndexAt(pt.y);
          if (wire < 0 || wire >= this.circuitDefinition.numWires) {
              return undefined;
          }
  
          // Is it inside the intended click area, instead of just off to the side?
          let r = this._wireInitialStateClickableRect(wire);
          if (!r.containsPoint(pt)) {
              return undefined;
          }
  
          // Good to go.
          return wire;
      }
  
      /**
       * @param {!Hand} hand
       * @returns {undefined|!DisplayedCircuit}
       */
      tryClick(hand) {
          if (hand.pos === undefined || hand.heldGate !== undefined) {
              return undefined;
          }
  
          let clickedInitialStateWire = this.findWireWithInitialStateAreaContaining(hand.pos);
          if (clickedInitialStateWire !== undefined) {
              return this.withCircuit(this.circuitDefinition.withSwitchedInitialStateOn(clickedInitialStateWire))
          }
  
          let found = this.findGateWithButtonContaining(hand.pos);
          if (found === undefined) {
              return undefined;
          }
  
          let newGate = found.gate.onClickGateFunc(found.gate);
          let cols = [...this.circuitDefinition.columns];
          let col = cols[found.col];
          let gates = [...col.gates];
          gates.splice(found.row, 1, newGate);
          cols.splice(found.col, 1, new GateColumn(gates));
          return this.withCircuit(this.circuitDefinition.withColumns(cols));
      }
  
      /**
       * @param {!Hand} hand
       * @param {!boolean=false} duplicate
       * @param {!boolean=false} wholeColumn
       * @param {!boolean=false} ignoreResizeTabs
       * @param {!boolean=false} alt Whether or not to replace grabbed gates with their alternates.
       * @returns {!{newCircuit: !DisplayedCircuit, newHand: !Hand}}
       */
      tryGrab(hand, duplicate=false, wholeColumn=false, ignoreResizeTabs=false, alt=false) {
          if (wholeColumn) {
              let grabRowResult = this._tryGrabRow(hand, alt);
              if (grabRowResult !== undefined) {
                  return grabRowResult;
              }
              return this._tryGrabWholeColumn(hand, duplicate, alt) || {newCircuit: this, newHand: hand};
          }
  
          let newHand = hand;
          let newCircuit = this;
          if (!ignoreResizeTabs) {
              let resizing = this._tryGrabResizeTab(hand);
              if (resizing !== undefined) {
                  newHand = resizing.newHand;
                  newCircuit = resizing.newCircuit;
              }
          }
  
          return newCircuit._tryGrabGate(newHand, duplicate, alt) || {newCircuit, newHand};
      }
  
      /**
       * @param {!Hand} hand
       * @param {!boolean} alt
       * @returns {undefined|!{newCircuit: !DisplayedCircuit, newHand: !Hand}}
       */
      _tryGrabRow(hand, alt) {
          if (hand.pos === undefined) {
              return undefined;
          }
  
          // Which wire is it? Is it one that's actually in the circuit?
          let wire = this.wireIndexAt(hand.pos.y);
          if (wire < 0 || wire >= this.circuitDefinition.numWires) {
              return undefined;
          }
  
          // Is it inside the intended click area, instead of just off to the side?
          let r = this._wireInitialStateClickableRect(wire);
          if (!r.containsPoint(hand.pos)) {
              return undefined;
          }
  
          let {newCircuit, initialState, rowGates} = this._cutRow(wire);
          let holdOffset = new Point(0, hand.pos.y - r.y);
          if (alt) {
              rowGates = rowGates.map(e => e === undefined ? e : e.alternate);
          }
          return {
              newCircuit: this.withCircuit(newCircuit),
              newHand: hand.withHeldRow({initialState, gates: rowGates}, holdOffset)
          };
      }
  
      /**
       * @param {!int} row
       * @returns {!{newCircuit: !CircuitDefinition, rowGates: !Array.<undefined|!Gate>, initialState: *}}
       * @private
       */
      _cutRow(row) {
          let row_gates = [];
          let cols = [];
          for (let i = 0; i < this.circuitDefinition.columns.length; i++) {
              let col_gates = [...this.circuitDefinition.columns[i].gates];
              row_gates.push(col_gates[row]);
              col_gates.splice(row, 1);
              col_gates.push(undefined);
              cols.push(new GateColumn(col_gates));
          }
          let newInitialStates = seq(this.circuitDefinition.customInitialValues.entries()).
              filter(([k, _]) => k !== row).
              map(([k, v]) => [k - (k > row ? 1 : 0), v]).
              toMap(([k, _]) => k, ([_, v]) => v);
          return {
              newCircuit: this.circuitDefinition.withColumns(cols).withInitialStates(newInitialStates),
              rowGates: row_gates,
              initialState: this.circuitDefinition.customInitialValues.get(row)
          };
      }
  
      /**
       * @param {!Hand} hand
       * @param {!boolean} duplicate
       * @param {!boolean} alt
       * @returns {undefined|!{newCircuit: !DisplayedCircuit, newHand: !Hand}}
       */
      _tryGrabGate(hand, duplicate, alt) {
          if (hand.isBusy() || hand.pos === undefined) {
              return undefined;
          }
  
          let foundPt = this.findGateOverlappingPos(hand.pos);
          if (foundPt === undefined) {
              return undefined;
          }
  
          let {col, row, offset} = foundPt;
          let gate = this.circuitDefinition.columns[col].gates[row];
          if (alt) {
              gate = gate.alternate;
          }
  
          let remainingGates = seq(this.circuitDefinition.columns[col].gates).toArray();
          if (!duplicate) {
              remainingGates[row] = undefined;
          }
  
          let newCols = seq(this.circuitDefinition.columns).
              withOverlayedItem(col, new GateColumn(remainingGates)).
              toArray();
          return {
              newCircuit: new DisplayedCircuit(
                  this.top,
                  this.circuitDefinition.withColumns(newCols),
                  undefined,
                  undefined,
                  this._extraWireStartIndex),
              newHand: hand.withHeldGate(gate, offset)
          };
      }
  
      /**
       * @param {!Hand} hand
       * @returns {!{newCircuit: !DisplayedCircuit, newHand: !Hand}}
       */
      _tryGrabResizeTab(hand) {
          if (hand.isBusy() || hand.pos === undefined) {
              return undefined;
          }
  
          for (let col = 0; col < this.circuitDefinition.columns.length; col++) {
              for (let row = 0; row < this.circuitDefinition.numWires; row++) {
                  let gate = this.circuitDefinition.columns[col].gates[row];
                  if (gate === undefined) {
                      continue;
                  }
                  let {isResizeHighlighted} =
                      this._highlightStatusAt(col, row, hand.hoverPoints());
                  if (isResizeHighlighted) {
                      let offset = hand.pos.minus(this.gateRect(row + gate.height - 1, col, 1, 1).center());
                      return {
                          newCircuit: this._withHighlightedSlot({col, row, resizeStyle: true}),
                          newHand: hand.withResizeSlot(new Point(col, row), offset)
                      };
                  }
              }
          }
          return undefined;
      }
  
      /**
       * @param {!Hand} hand
       * @param {!boolean} duplicate
       * @param {!boolean} alt Whether or not to replace grabbed gates with their alternates.
       * @returns {undefined|!{newCircuit: !DisplayedCircuit, newHand: !Hand}}
       * @private
       */
      _tryGrabWholeColumn(hand, duplicate, alt) {
          if (hand.isBusy() || hand.pos === undefined) {
              return undefined;
          }
  
          let col = Math.round(this.toColumnSpaceCoordinate(hand.pos.x));
          if (col < 0 || col >= this.circuitDefinition.columns.length || this.circuitDefinition.columns[col].isEmpty()) {
              return undefined;
          }
  
          let newCols = [...this.circuitDefinition.columns];
          if (!duplicate) {
              newCols.splice(col, 1, GateColumn.empty(this.circuitDefinition.numWires));
          }
  
          let holdOffset = new Point(0, this.wireIndexAt(hand.pos.y) * Config.WIRE_SPACING + Config.WIRE_SPACING/2);
          let grabbedGates = this.circuitDefinition.columns[col];
          if (alt) {
              grabbedGates = new GateColumn(grabbedGates.gates.map(e => e === undefined ? e : e.alternate));
          }
          return {
              newCircuit: this.withCircuit(this.circuitDefinition.withColumns(newCols)),
              newHand: hand.withHeldGateColumn(grabbedGates, holdOffset)
          };
      }
  
      /**
       * @returns {Infinity|!number}
       */
      stableDuration() {
          return this.circuitDefinition.stableDuration();
      }
  
      /**
       * @returns {!int}
       */
      importantWireCount() {
          return Math.max(
              this.circuitDefinition.numWires - (this._extraWireStartIndex === Config.MAX_WIRE_COUNT ? 0 : 1),
              Config.MIN_WIRE_COUNT,
              this.circuitDefinition.minimumRequiredWireCount());
      }
  
      /**
       * Draws a peek gate on each wire at the right-hand side of the circuit.
       *
       * @param {!Painter} painter
       * @param {!CircuitStats} stats
       * @param {!Hand} hand
       * @private
       */
      _drawOutputDisplays(painter, stats, hand) {
          let chanceCol = this.clampedCircuitColCount() + 1;
          let blochCol = chanceCol + 1;
          let numWire = this.importantWireCount();
  
          for (let i = 0; i < numWire; i++) {
              let p = stats.controlledWireProbabilityJustAfter(i, Infinity);
              MathPainter.paintProbabilityBox(painter, p, this.gateRect(i, chanceCol), hand.hoverPoints());
              let m = stats.qubitDensityMatrix(Infinity, i);
              if (m !== undefined) {
                  paintBlochSphereDisplay(painter, m, this.gateRect(i, blochCol), hand.hoverPoints());
              }
          }
  
          let bottom = this.wireRect(numWire-1).bottom();
          let x = this.opRect(chanceCol - 1).x;
          painter.printParagraph(
              "Local wire states\n(Chance/Bloch)",
              new Rect(x, bottom+4, 190, 40),
              new Point(0.5, 0),
              'gray');
  
          this._drawOutputSuperpositionDisplay(painter, stats, hand);
      }
  
      /**
       * @returns {!number} The number of columns used for drawing the circuit, before the output display.
       */
      clampedCircuitColCount() {
          return Math.max(
              this.circuitDefinition.columns.length,
              Config.MIN_COL_COUNT + (this._compressedColumnIndex !== undefined ? 1 : 0));
      }
  
      /**
       * Draws a peek gate on each wire at the right-hand side of the circuit.
       *
       * @param {!Painter} painter
       * @param {!CircuitStats} stats
       * @param {!Hand} hand
       * @private
       */
      _drawOutputSuperpositionDisplay(painter, stats, hand) {
          let amplitudeGrid = this._outputStateAsMatrix(stats);
          let gridRect = this._rectForSuperpositionDisplay();
  
          let numWire = this.importantWireCount();
          MathPainter.paintMatrix(
              painter,
              amplitudeGrid,
              gridRect,
              numWire < Config.SIMPLE_SUPERPOSITION_DRAWING_WIRE_THRESHOLD ? Config.SUPERPOSITION_MID_COLOR : undefined,
              'black',
              numWire < Config.SIMPLE_SUPERPOSITION_DRAWING_WIRE_THRESHOLD ? Config.SUPERPOSITION_FORE_COLOR : undefined,
              Config.SUPERPOSITION_BACK_COLOR);
          let forceSign = v => (v >= 0 ? '+' : '') + v.toFixed(2);
          MathPainter.paintMatrixTooltip(painter, amplitudeGrid, gridRect, hand.hoverPoints(),
              (c, r) => `Amplitude of |${Util.bin(r*amplitudeGrid.width() + c, numWire)}⟩ (decimal ${r*amplitudeGrid.width() + c})`,
              (c, r, v) => 'val:' + v.toString(new Format(false, 0, 5, ", ")),
              (c, r, v) => `mag²:${(v.norm2()*100).toFixed(4)}%, phase:${forceSign(v.phase() * 180 / Math.PI)}°`);
  
          this._drawOutputSuperpositionDisplay_labels(painter);
      }
  
      /**
       * @param {!Painter} painter
       * @private
       */
      _drawOutputSuperpositionDisplay_labels(painter) {
          let gridRect = this._rectForSuperpositionDisplay();
          let numWire = this.importantWireCount();
          _cachedRowLabelDrawer.paint(gridRect.right(), gridRect.y, painter, numWire);
          _cachedColLabelDrawer.paint(gridRect.x, gridRect.bottom(), painter, numWire);
      }
  
      /**
       * @param {!CircuitStats} stats
       * @returns {!Matrix}
       * @private
       */
      _outputStateAsMatrix(stats) {
          let numWire = this.importantWireCount();
          let buf = stats.finalState.rawBuffer();
          if (stats.circuitDefinition.numWires !== numWire) {
              let r = new Float32Array(2 << numWire);
              r.set(buf.slice(0, r.length));
              buf = r;
          }
  
          let [colWires, rowWires] = [Math.floor(numWire/2), Math.ceil(numWire/2)];
          let [colCount, rowCount] = [1 << colWires, 1 << rowWires];
          //noinspection JSCheckFunctionSignatures
          return new Matrix(colCount, rowCount, buf);
      }
  
      /**
       * @returns {!Rect}
       * @private
       */
      _rectForSuperpositionDisplay() {
          let col = this.clampedCircuitColCount() + EXTRA_COLS_FOR_SINGLE_QUBIT_DISPLAYS + 1;
          let numWire = this.importantWireCount();
          let [colWires, rowWires] = [Math.floor(numWire/2), Math.ceil(numWire/2)];
          let [colCount, rowCount] = [1 << colWires, 1 << rowWires];
          let topRect = this.gateRect(0, col);
          let bottomRect = this.gateRect(numWire-1, col);
          let gridRect = new Rect(topRect.x, topRect.y, 0, bottomRect.bottom() - topRect.y);
          return gridRect.withW(gridRect.h * (colCount/rowCount));
      }
  
      /**
       * Draws a peek gate on each wire at the right-hand side of the circuit.
       *
       * @param {!Painter} painter
       * @param {!CircuitStats} stats
       * @private
       */
      _drawHintLabels(painter, stats) {
          let gridRect = this._rectForSuperpositionDisplay();
  
          // Amplitude hint.
          painter.print(
              'Final amplitudes',
              gridRect.right() + 3,
              gridRect.bottom() + 3,
              'left',
              'top',
              'gray',
              '12px sans-serif',
              100,
              20);
  
          // Deferred measurement warning.
          if (this.circuitDefinition.colIsMeasuredMask(Infinity) !== 0) {
              painter.printParagraph(
                  "(assuming measurement deferred)",
                  new Rect(
                      gridRect.right() + 3,
                      gridRect.bottom() + 20,
                      100,
                      75),
                  new Point(0.5, 0),
                  'red');
          }
  
          // Discard rate warning.
          let survivalRate = stats.survivalRate(Infinity);
          if (Math.abs(survivalRate - 1) > 0.01) {
              let desc;
              if (survivalRate < 1) {
                  let rate = Math.round(survivalRate * 100);
                  let rateDesc = survivalRate === 0 ? "0" :
                      rate > 0 ? rate :
                      "<1";
                  desc = `kept: ${rateDesc}%`;
              } else {
                  let factor = Math.round(survivalRate * 100);
                  desc = `over-unity: ${factor}%`;
              }
              painter.print(
                  desc,
                  this._rectForSuperpositionDisplay().x - 5,
                  gridRect.bottom() + SUPERPOSITION_GRID_LABEL_SPAN,
                  'right',
                  'bottom',
                  'red',
                  '14px sans-serif',
                  800,
                  50);
          }
      }
  
      /**
       * Parses a text diagram of a circuit, with positions marked by numbers, into a displayed circuit and a list of the
       * positions of the marked points.
       *
       * Note: All lines should start with a pipe (|).
       * Note: Follow a number with a carat (^) to indicate a position above the carat.
       * Note: Separate wires with blank lines. Also start and end with blank lines.
       * Note: Hyphens (-) mark wires, but can't be used at gate locations. Use a char mapped to undefined for that.
       *
       * Example diagram, which could be used to seed a drag of the X gate from after the control to under the control:
       *    |
       *    |-H-C-X-
       *    |    0^
       *    |-+-1-+-
       *    |
       *
       * @param {!string} diagramText
       * @param {!Map<!string, undefined|!Gate>} gateMap
       * @returns {!{circuit: !DisplayedCircuit, pts: !Array.<!Point>}}
       */
      static fromTextDiagram(gateMap, diagramText) {
          let lines = diagramText.split('\n').map(e => {
              let p = e.split('|');
              if (p.length !== 2) {
                  throw new DetailedError('Bad diagram', {diagramText, gateMap});
              }
              return p[1];
          });
          let circuitDiagramSubset = seq(lines).
              skip(1).
              stride(2).
              map(line => seq(line).skip(1).stride(2).join("")).
              join('\n');
          let top = 10;
          let circuit = new DisplayedCircuit(
              top,
              CircuitDefinition.fromTextDiagram(gateMap, circuitDiagramSubset),
              undefined,
              undefined,
              undefined);
          let pts = Seq.naturals().
              takeWhile(k => diagramText.indexOf(k) !== -1).
              map(k => {
                  let pos = seq(lines).mapWithIndex((line, row) => ({row, col: line.indexOf(k)})).
                      filter(e => e.col !== -1).
                      single();
                  if (lines[pos.row][pos.col + 1] === '^') {
                      pos.row -= 1;
                      pos.col += 1;
                  }
                  return new Point(
                      pos.col * Config.WIRE_SPACING / 2 + 35.5,
                      pos.row * Config.WIRE_SPACING / 2 + 10.5);
              }).toArray();
          return {circuit, pts};
      }
  }
  
  /**
   * @param {!Painter} painter
   * @param {!CircuitDefinition} circuitDefinition
   * @param {!Rect} rect
   * @param {!boolean} showWires
   * @param {!number} time
   * @returns {!{maxW: !number, maxH: !number}}
   */
  function drawCircuitTooltip(painter, circuitDefinition, rect, showWires, time) {
      let displayed = new DisplayedCircuit(
          0,
          circuitDefinition,
          undefined,
          undefined,
          undefined);
      let neededWidth = displayed.desiredWidth(true);
      let neededHeight = displayed.desiredHeight(true);
      let scaleX = rect.w / neededWidth;
      let scaleY = rect.h / neededHeight;
      if (showWires) {
          let s = Math.min(scaleX, scaleY);
          scaleX = s;
          scaleY = s;
      }
      let stats = CircuitStats.withNanDataFromCircuitAtTime(circuitDefinition, time);
      try {
          painter.ctx.save();
          painter.ctx.translate(rect.x, rect.y);
          painter.ctx.scale(Math.min(1, scaleX), Math.min(1, scaleY));
          painter.ctx.translate(0, 0);
          displayed.paint(
              painter,
              Hand.EMPTY,
              stats,
              true,
              showWires);
      } finally {
          painter.ctx.restore();
      }
      return {maxW: neededWidth*scaleX, maxH: neededHeight*scaleY};
  }
  
  /**
   * @param {!GateDrawParams} args
   */
  let GATE_CIRCUIT_DRAWER = args => {
      let circuit = args.gate.knownCircuit;
      if (circuit === undefined || args.gate.symbol !== '') {
          if (args.gate.stableDuration() === Infinity) {
              GatePainting.DEFAULT_DRAWER(args);
          } else {
              GatePainting.makeCycleDrawer()(args);
          }
          return;
      }
  
      let toolboxColor = args.gate.stableDuration() === Infinity ?
          Config.GATE_FILL_COLOR :
          Config.TIME_DEPENDENT_HIGHLIGHT_COLOR;
      GatePainting.paintBackground(args, toolboxColor);
      drawCircuitTooltip(args.painter, args.gate.knownCircuitNested, args.rect, false, args.stats.time);
      GatePainting.paintOutline(args);
      if (args.isHighlighted) {
          args.painter.ctx.save();
          args.painter.ctx.globalAlpha *= 0.9;
          args.painter.fillRect(args.rect, Config.HIGHLIGHTED_GATE_FILL_COLOR);
          args.painter.ctx.restore();
      }
      GatePainting.paintOutline(args);
  };
  
  /**
   * @param {!Painter} painter
   * @param {!number} dy
   * @param {!int} n
   * @param {!function(!int) : !String} labeller
   * @param {!number} boundingWidth
   * @private
   */
  function _drawLabelsReasonablyFast(painter, dy, n, labeller, boundingWidth) {
      let ctx = painter.ctx;
      ctx.save();
      ctx.textAlign = 'left';
      ctx.textBaseline = 'middle';
      painter.ctx.font = '12px monospace';
      let w = Math.max(
          painter.ctx.measureText(labeller(0)).width,
          painter.ctx.measureText(labeller(n-1)).width);
      let h = ctx.measureText("0").width * 2.5;
      let scale = Math.min(Math.min((boundingWidth-2) / w, dy / h), 1);
  
      // Row labels.
      let step = dy/scale;
      let pad = 1/scale;
      ctx.scale(scale, scale);
      ctx.translate(0, dy*0.5/scale - h*0.5);
      ctx.fillStyle = 'lightgray';
      if (h < step*0.95) {
          for (let i = 0; i < n; i++) {
              ctx.fillRect(0, step*i, w + 2*pad, h);
          }
      } else {
          ctx.fillRect(0, 0, w + 2*pad, step*n);
      }
      ctx.fillStyle = 'black';
      for (let i = 0; i < n; i++) {
          ctx.fillText(labeller(i), pad, h*0.5 + step*i);
      }
      ctx.restore();
  }
  
  let _cachedRowLabelDrawer = new CachablePainting(
      numWire => ({
          width: SUPERPOSITION_GRID_LABEL_SPAN,
          height: (numWire - 1) * Config.WIRE_SPACING + Config.GATE_RADIUS * 2
      }),
      (painter, numWire) => {
          let [colWires, rowWires] = [Math.floor(numWire/2), Math.ceil(numWire/2)];
          let rowCount = 1 << rowWires;
          //noinspection JSCheckFunctionSignatures
          let suffix = colWires < 4 ? "_".repeat(colWires) : "_..";
          //noinspection JSCheckFunctionSignatures
          _drawLabelsReasonablyFast(
              painter,
              painter.canvas.height / rowCount,
              rowCount,
              i => Util.bin(i, rowWires) + suffix,
              SUPERPOSITION_GRID_LABEL_SPAN);
      });
  
  let _cachedColLabelDrawer = new CachablePainting(
      numWire => {
          let [colWires, rowWires] = [Math.floor(numWire/2), Math.ceil(numWire/2)];
          let [colCount, rowCount] = [1 << colWires, 1 << rowWires];
          let total_height = (numWire - 1) * Config.WIRE_SPACING + Config.GATE_RADIUS * 2;
          let cellDiameter = total_height / rowCount;
          return {
              width: colCount * cellDiameter,
              height: SUPERPOSITION_GRID_LABEL_SPAN
          }
      },
      (painter, numWire) => {
          let [colWires, rowWires] = [Math.floor(numWire/2), Math.ceil(numWire/2)];
          let colCount = 1 << colWires;
          let dw = painter.canvas.width / colCount;
  
          painter.ctx.translate(colCount*dw, 0);
          painter.ctx.rotate(Math.PI/2);
          //noinspection JSCheckFunctionSignatures
          let prefix = rowWires < 4 ? "_".repeat(rowWires) : ".._";
          //noinspection JSCheckFunctionSignatures
          _drawLabelsReasonablyFast(
              painter,
              dw,
              colCount,
              i => prefix + Util.bin(colCount-1-i, colWires),
              SUPERPOSITION_GRID_LABEL_SPAN);
      });
  
  export {DisplayedCircuit, drawCircuitTooltip, GATE_CIRCUIT_DRAWER}
  