text: /**
   * The visualization controller will works as a state machine.
   * See files under the `doc` folder for transition descriptions.
   * See https://github.com/jakesgordon/javascript-state-machine
   * for the document of the StateMachine module.
   */
  var Controller = StateMachine.create({
      initial: 'none',
      events: [
          {
              name: 'init',
              from: 'none',
              to:   'ready'
          },
          {
              name: 'search',
              from: 'starting',
              to:   'searching'
          },
          {
              name: 'pause',
              from: 'searching',
              to:   'paused'
          },
          {
              name: 'finish',
              from: 'searching',
              to:   'finished'
          },
          {
              name: 'resume',
              from: 'paused',
              to:   'searching'
          },
          {
              name: 'cancel',
              from: 'paused',
              to:   'ready'
          },
          {
              name: 'modify',
              from: 'finished',
              to:   'modified'
          },
          {
              name: 'reset',
              from: '*',
              to:   'ready'
          },
          {
              name: 'clear',
              from: ['finished', 'modified'],
              to:   'ready'
          },
          {
              name: 'start',
              from: ['ready', 'modified', 'restarting'],
              to:   'starting'
          },
          {
              name: 'restart',
              from: ['searching', 'finished'],
              to:   'restarting'
          },
          {
              name: 'dragStart',
              from: ['ready', 'finished'],
              to:   'draggingStart'
          },
          {
              name: 'dragEnd',
              from: ['ready', 'finished'],
              to:   'draggingEnd'
          },
          {
              name: 'drawWall',
              from: ['ready', 'finished'],
              to:   'drawingWall'
          },
          {
              name: 'eraseWall',
              from: ['ready', 'finished'],
              to:   'erasingWall'
          },
          {
              name: 'rest',
              from: ['draggingStart', 'draggingEnd', 'drawingWall', 'erasingWall'],
              to  : 'ready'
          },
      ],
  });
  
  $.extend(Controller, {
      gridSize: [64, 36], // number of nodes horizontally and vertically
      operationsPerSecond: 300,
  
      /**
       * Asynchronous transition from `none` state to `ready` state.
       */
      onleavenone: function() {
          var numCols = this.gridSize[0],
              numRows = this.gridSize[1];
  
          this.grid = new PF.Grid(numCols, numRows);
  
          View.init({
              numCols: numCols,
              numRows: numRows
          });
          View.generateGrid(function() {
              Controller.setDefaultStartEndPos();
              Controller.bindEvents();
              Controller.transition(); // transit to the next state (ready)
          });
  
          this.$buttons = $('.control_button');
  
          this.hookPathFinding();
  
          return StateMachine.ASYNC;
          // => ready
      },
      ondrawWall: function(event, from, to, gridX, gridY) {
          this.setWalkableAt(gridX, gridY, false);
          // => drawingWall
      },
      oneraseWall: function(event, from, to, gridX, gridY) {
          this.setWalkableAt(gridX, gridY, true);
          // => erasingWall
      },
      onsearch: function(event, from, to) {
          var grid,
              timeStart, timeEnd,
              finder = Panel.getFinder();
  
          timeStart = window.performance ? performance.now() : Date.now();
          grid = this.grid.clone();
          this.path = finder.findPath(
              grid.getNodeAt(this.startX,this.startY), grid.getNodeAt(this.endX, this.endY), grid.nodes
          );
          this.operationCount = this.operations.length;
          timeEnd = window.performance ? performance.now() : Date.now();
          this.timeSpent = (timeEnd - timeStart).toFixed(4);
  
          this.loop();
          // => searching
      },
      onrestart: function() {
          // When clearing the colorized nodes, there may be
          // nodes still animating, which is an asynchronous procedure.
          // Therefore, we have to defer the `abort` routine to make sure
          // that all the animations are done by the time we clear the colors.
          // The same reason applies for the `onreset` event handler.
          setTimeout(function() {
              Controller.clearOperations();
              Controller.clearFootprints();
              Controller.start();
          }, View.nodeColorizeEffect.duration * 1.2);
          // => restarting
      },
      onpause: function(event, from, to) {
          // => paused
      },
      onresume: function(event, from, to) {
          this.loop();
          // => searching
      },
      oncancel: function(event, from, to) {
          this.clearOperations();
          this.clearFootprints();
          // => ready
      },
      onfinish: function(event, from, to) {
          View.showStats({
              pathLength: PF.Util.pathLength(this.path),
              timeSpent:  this.timeSpent,
              operationCount: this.operationCount,
          });
          View.drawPath(this.path);
          // => finished
      },
      onclear: function(event, from, to) {
          this.clearOperations();
          this.clearFootprints();
          // => ready
      },
      onmodify: function(event, from, to) {
          // => modified
      },
      onreset: function(event, from, to) {
          setTimeout(function() {
              Controller.clearOperations();
              Controller.clearAll();
              Controller.buildNewGrid();
          }, View.nodeColorizeEffect.duration * 1.2);
          // => ready
      },
  
      /**
       * The following functions are called on entering states.
       */
  
      onready: function() {
          console.log('=> ready');
          this.setButtonStates({
              id: 1,
              text: 'Start Search',
              enabled: true,
              callback: $.proxy(this.start, this),
          }, {
              id: 2,
              text: 'Pause Search',
              enabled: false,
          }, {
              id: 3,
              text: 'Clear Walls',
              enabled: true,
              callback: $.proxy(this.reset, this),
          });
          // => [starting, draggingStart, draggingEnd, drawingStart, drawingEnd]
      },
      onstarting: function(event, from, to) {
          console.log('=> starting');
          // Clears any existing search progress
          this.clearFootprints();
          this.setButtonStates({
              id: 2,
              enabled: true,
          });
          this.search();
          // => searching
      },
      onsearching: function() {
          console.log('=> searching');
          this.setButtonStates({
              id: 1,
              text: 'Restart Search',
              enabled: true,
              callback: $.proxy(this.restart, this),
          }, {
              id: 2,
              text: 'Pause Search',
              enabled: true,
              callback: $.proxy(this.pause, this),
          });
          // => [paused, finished]
      },
      onpaused: function() {
          console.log('=> paused');
          this.setButtonStates({
              id: 1,
              text: 'Resume Search',
              enabled: true,
              callback: $.proxy(this.resume, this),
          }, {
              id: 2,
              text: 'Cancel Search',
              enabled: true,
              callback: $.proxy(this.cancel, this),
          });
          // => [searching, ready]
      },
      onfinished: function() {
          console.log('=> finished');
          this.setButtonStates({
              id: 1,
              text: 'Restart Search',
              enabled: true,
              callback: $.proxy(this.restart, this),
          }, {
              id: 2,
              text: 'Clear Path',
              enabled: true,
              callback: $.proxy(this.clear, this),
          });
      },
      onmodified: function() {
          console.log('=> modified');
          this.setButtonStates({
              id: 1,
              text: 'Start Search',
              enabled: true,
              callback: $.proxy(this.start, this),
          }, {
              id: 2,
              text: 'Clear Path',
              enabled: true,
              callback: $.proxy(this.clear, this),
          });
      },
  
      /**
       * Define setters and getters of PF.Node, then we can get the operations
       * of the pathfinding.
       */
      hookPathFinding: function() {
  
          PF.Node.prototype = {
              get opened() {
                  return this._opened;
              },
              set opened(v) {
                  this._opened = v;
                  Controller.operations.push({
                      x: this.x,
                      y: this.y,
                      z: this.z,
                      attr: 'opened',
                      value: v
                  });
              },
              get closed() {
                  return this._closed;
              },
              set closed(v) {
                  this._closed = v;
                  Controller.operations.push({
                      x: this.x,
                      y: this.y,
                      z: this.z,
                      attr: 'closed',
                      value: v
                  });
              },
              set jumptest(v) {
                  Controller.operations.push({
                      x: this.x,
                      y: this.y,
                      z: this.z,
                      attr: 'jumptest',
                      jumptested: true,
                      value: v
                  });
              },
          };
  
          this.operations = [];
      },
      bindEvents: function() {
          $('#draw_area').mousedown($.proxy(this.mousedown, this));
          $(window)
              .mousemove($.proxy(this.mousemove, this))
              .mouseup($.proxy(this.mouseup, this));
      },
      loop: function() {
          var interval = 1000 / this.operationsPerSecond;
          (function loop() {
              if (!Controller.is('searching')) {
                  return;
              }
              Controller.step();
              setTimeout(loop, interval);
          })();
      },
      step: function() {
          var operations = this.operations,
              op, isSupported;
  
          do {
              if (!operations.length) {
                  this.finish(); // transit to `finished` state
                  return;
              }
              op = operations.shift();
              isSupported = View.supportedOperations.indexOf(op.attr) !== -1;
          } while (!isSupported);
  
          View.setAttributeAt(op.x, op.y, op.attr, op.value);
      },
      clearOperations: function() {
          this.operations = [];
      },
      clearFootprints: function() {
          View.clearFootprints();
          View.clearPath();
      },
      clearAll: function() {
          this.clearFootprints();
          View.clearBlockedNodes();
      },
      buildNewGrid: function() {
          this.grid = new PF.Grid(this.gridSize[0], this.gridSize[1]);
      },
      mousedown: function (event) {
          var coord = View.toGridCoordinate(event.pageX, event.pageY),
              gridX = coord[0],
              gridY = coord[1],
              grid  = this.grid;
  
          if (this.can('dragStart') && this.isStartPos(gridX, gridY)) {
              this.dragStart();
              return;
          }
          if (this.can('dragEnd') && this.isEndPos(gridX, gridY)) {
              this.dragEnd();
              return;
          }
          if (this.can('drawWall') && grid.isWalkableAt(gridX, gridY)) {
              this.drawWall(gridX, gridY);
              return;
          }
          if (this.can('eraseWall') && !grid.isWalkableAt(gridX, gridY)) {
              this.eraseWall(gridX, gridY);
          }
      },
      mousemove: function(event) {
          var coord = View.toGridCoordinate(event.pageX, event.pageY),
              grid = this.grid,
              gridX = coord[0],
              gridY = coord[1];
  
          if (this.isStartOrEndPos(gridX, gridY)) {
              return;
          }
  
          switch (this.current) {
          case 'draggingStart':
              if (grid.isWalkableAt(gridX, gridY)) {
                  this.setStartPos(gridX, gridY);
              }
              break;
          case 'draggingEnd':
              if (grid.isWalkableAt(gridX, gridY)) {
                  this.setEndPos(gridX, gridY);
              }
              break;
          case 'drawingWall':
              this.setWalkableAt(gridX, gridY, false);
              break;
          case 'erasingWall':
              this.setWalkableAt(gridX, gridY, true);
              break;
          }
      },
      mouseup: function(event) {
          if (Controller.can('rest')) {
              Controller.rest();
          }
      },
      setButtonStates: function() {
          $.each(arguments, function(i, opt) {
              var $button = Controller.$buttons.eq(opt.id - 1);
              if (opt.text) {
                  $button.text(opt.text);
              }
              if (opt.callback) {
                  $button
                      .unbind('click')
                      .click(opt.callback);
              }
              if (opt.enabled === undefined) {
                  return;
              } else if (opt.enabled) {
                  $button.removeAttr('disabled');
              } else {
                  $button.attr({ disabled: 'disabled' });
              }
          });
      },
      /**
       * When initializing, this method will be called to set the positions
       * of start node and end node.
       * It will detect user's display size, and compute the best positions.
       */
      setDefaultStartEndPos: function() {
          var width, height,
              marginRight, availWidth,
              centerX, centerY,
              endX, endY,
              nodeSize = View.nodeSize;
  
          width  = $(window).width();
          height = $(window).height();
  
          marginRight = $('#algorithm_panel').width();
          availWidth = width - marginRight;
  
          centerX = Math.ceil(availWidth / 2 / nodeSize);
          centerY = Math.floor(height / 2 / nodeSize);
  
          this.setStartPos(centerX - 5, centerY);
          this.setEndPos(centerX + 5, centerY);
      },
      setStartPos: function(gridX, gridY) {
          this.startX = gridX;
          this.startY = gridY;
          View.setStartPos(gridX, gridY);
      },
      setEndPos: function(gridX, gridY) {
          this.endX = gridX;
          this.endY = gridY;
          View.setEndPos(gridX, gridY);
      },
      setWalkableAt: function(gridX, gridY, walkable) {
          this.grid.setWalkableAt(gridX, gridY, walkable);
          View.setAttributeAt(gridX, gridY, 'walkable', walkable);
      },
      isStartPos: function(gridX, gridY) {
          return gridX === this.startX && gridY === this.startY;
      },
      isEndPos: function(gridX, gridY) {
          return gridX === this.endX && gridY === this.endY;
      },
      isStartOrEndPos: function(gridX, gridY) {
          return this.isStartPos(gridX, gridY) || this.isEndPos(gridX, gridY);
      },
  });
  