exports.dialog = {
  'en_US': [{
    type: 'ModalAlert',
    options: {
      markdowns: [
        '## Welcome to the level builder!',
        '',
        'Here are the main steps:',
        '',
        '  * Set up the initial environment with git commands',
        '  * Define the starting tree with ```define start```',
        '  * Enter the series of git commands that compose the (optimal) solution',
        '  * Define the goal tree with ```define goal```. Defining the goal also defines the solution',
        '  * Optionally define a hint with ```define hint```',
        '  * Edit the name with ```define name```',
        '  * Optionally define a nice start dialog with ```edit dialog```',
        '  * Enter the command ```finish``` to output your level JSON!'
      ]
    }
  }],
  'de_DE': [{
    type: 'ModalAlert',
    options: {
      markdowns: [
        '## Willkommen zum Level-Editor!',
        '',
        'So funktioniert\'s:',
        '',
        '  * Stelle mit Git-Befehlen die Ausganssituation her',
        '  * Leg den Startpunkt mit ```define start``` fest',
        '  * Gib eine Abfolge von Git-Befehlen ein, welche die (optimale) Lösung darstellen',
        '  * Leg den Ziel-Baum mit ```define goal``` fest. Damit markierst du den Endpunkt der Lösung',
        '  * Gib einen Hinweis mittels ```define hint``` an, wenn du willst',
        '  * Änder den Namen mittels ```define name```',
        '  * Wenn du magst, erstelle einen schönene Einführungsdialog mit ```edit dialog```',
        '  * Gib das Kommando ```finish``` ein um deinen Level als JSON auszugeben'
      ]
    }
  }],
  'zh_CN': [{
    type: 'ModalAlert',
    options: {
      markdowns: [
        '## 欢迎使用关卡生成器！',
        '',
        '主要步骤如下：',
        '',
        '  * 使用 git 命令布置好初始环境',
        '  * 使用 ```define start``` 命令定义起始树',
        '  * 输入一系列 git 命令，编好答案',
        '  * 使用 ```define goal``` 命令定义目标树。定义目标的同时定义答案',
        '  * 还可以用 ```define hint``` 命令定义一个提示',
        '  * 用 ```define name``` 修改名称',
        '  * 还可以用 ```edit dialog``` 定义一个漂亮的开始对话框',
        '  * 输入 ```finish``` 就可以输出你的关卡数据（JSON）了！'
      ]
    }
  }],
  'fr_FR': [{
    type: 'ModalAlert',
    options: {
      markdowns: [
        '## Bienvenue dans l\'éditeur niveaux !',
        '',
        'Voici les étapes principales :',
        '',
        '  * Mettez en place l\'environnement initial avec des commandes git',
        '  * Définissez l\'arbre de départ avec ```define start```',
        '  * Saisissez la série de commandes git qui composent la solution (optimale)',
        '  * Définissez l\'arbre cible avec ```define goal```. Cela définit aussi la solution',
        '  * Optionnellement, définissez un indice avec ```define hint```',
        '  * Changez le nom avec ```define name```',
        '  * Optionellement, definissez un joli dialogue de départ avec ```edit dialog```',
        '  * Entrez la commande ```finish``` pour délivrer votre niveau JSON!'
      ]
    }
  }]
};


TreeCompare.compareAllBranchesWithinTreesAndHEAD = function(treeA, treeB) {

// also compare tags!! for just one level

return treeA.HEAD.target == treeB.HEAD.target &&

TreeCompare.compareAllBranchesWithinTrees = function(treeA, treeB) {

keep keep keep keep keep replace keep keep keep keep keep

return treeA.HEAD.target === treeB.HEAD.target &&

var TreeCompare = require('../src/js/graph/treeCompare');
var _ = require('underscore');

var loadTree = function(treeString) {
  return TreeCompare.convertTreeSafe(treeString);
};

var testMethod = function(compareMethod, goalTreeString, cases, options) {
  cases = cases || {};
  options = options || {};
  if (!options.dontCompareGoal) {
    // always expect the goal to compare to itself correctly
    cases[goalTreeString] = true;
  }

  _.each(cases, function(value, actualTree) {
    var isEqual = TreeCompare.dispatch(compareMethod, goalTreeString, actualTree);
    if (isEqual !== value) {
      console.log('this goal tree', loadTree(goalTreeString));
      console.log('did not match this tree', loadTree(actualTree));
      console.log('for this value', value);
    }
    expect(isEqual).toBe(value);
  });
};

describe('Tree Compare', function() {
  it('will return false early if goal missing origin', function() {
    testMethod(
      {}, // checked for all methods
      '{}', // no origin tree
      {
        '{"originTree":{}}': false
      }, {
        dontCompareGoal: true
      }
    );
  });

  it('will return false early if current missing origin', function() {
    testMethod(
      {}, // checked for all methods
      '{"originTree":{}}',
      {
        '{}': false
      }, {
        dontCompareGoal: true
      }
    );
  });

  it('deep compares on origin tree', function() {
    testMethod(
      {}, // checked for all methods so this doesnt matter
      // state with originTree
      '{"branches":{"master":{"target":"C1","id":"master"},"o/master":{"target":"C1","id":"o/master"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"}},"HEAD":{"target":"master","id":"HEAD"},"originTree":{"branches":{"master":{"remoteTrackingBranch":null,"remote":false,"target":"C1","id":"master","type":"branch"}},"commits":{"C0":{"type":"commit","parents":[],"author":"Peter Cottle","createTime":"Wed Jul 24 2013 09:58:50 GMT-0700 (PDT)","commitMessage":"Quick commit. Go Bears!","id":"C0","rootCommit":true},"C1":{"type":"commit","parents":["C0"],"author":"Peter Cottle","createTime":"Wed Jul 24 2013 09:58:50 GMT-0700 (PDT)","commitMessage":"Quick commit. Go Bears!","id":"C1"}},"HEAD":{"target":"master","id":"HEAD","type":"general ref"}}}',
      {
        // one extra commit in origin
        '{"branches":{"master":{"target":"C1","id":"master"},"o/master":{"target":"C1","id":"o/master"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"}},"HEAD":{"target":"master","id":"HEAD"},"originTree":{"branches":{"master":{"remoteTrackingBranch":null,"remote":false,"target":"C2","id":"master","type":"branch"}},"commits":{"C0":{"type":"commit","parents":[],"author":"Peter Cottle","createTime":"Wed Jul 24 2013 10:24:50 GMT-0700 (PDT)","commitMessage":"Quick commit. Go Bears!","id":"C0","rootCommit":true},"C1":{"type":"commit","parents":["C0"],"author":"Peter Cottle","createTime":"Wed Jul 24 2013 10:24:50 GMT-0700 (PDT)","commitMessage":"Quick commit. Go Bears!","id":"C1"},"C2":{"type":"commit","parents":["C1"],"author":"Peter Cottle","createTime":"Wed Jul 24 2013 10:24:55 GMT-0700 (PDT)","commitMessage":"Quick commit. Go Bears!","id":"C2"}},"HEAD":{"target":"master","id":"HEAD","type":"general ref"}}}': false,
        // extra commit local
        '{"branches":{"master":{"target":"C2","id":"master"},"o/master":{"target":"C1","id":"o/master"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"}},"HEAD":{"target":"master","id":"HEAD"},"originTree":{"branches":{"master":{"remoteTrackingBranch":null,"remote":false,"target":"C1","id":"master","type":"branch"}},"commits":{"C0":{"type":"commit","parents":[],"author":"Peter Cottle","createTime":"Wed Jul 24 2013 12:31:20 GMT-0700 (PDT)","commitMessage":"Quick commit. Go Bears!","id":"C0","rootCommit":true},"C1":{"type":"commit","parents":["C0"],"author":"Peter Cottle","createTime":"Wed Jul 24 2013 12:31:20 GMT-0700 (PDT)","commitMessage":"Quick commit. Go Bears!","id":"C1"}},"HEAD":{"target":"master","id":"HEAD","type":"general ref"}}}': false
      }
    );
  });

  it('uses originCompare for the origin comparison results', function() {
    testMethod({
        // compare all branches and head locally, but only master remotely
        originCompare: {
          compareOnlyMaster: true
        }
      },
      // start with a local / origin with two branches
      '{"branches":{"master":{"target":"C3","id":"master"},"side":{"target":"C2","id":"side"},"o/master":{"target":"C3","id":"o/master"},"o/side":{"target":"C2","id":"o/side"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"},"C3":{"parents":["C1"],"id":"C3"}},"HEAD":{"target":"master","id":"HEAD"},"originTree":{"branches":{"master":{"remoteTrackingBranch":null,"remote":false,"target":"C3","id":"master","type":"branch"},"side":{"remoteTrackingBranch":null,"remote":false,"target":"C2","id":"side","type":"branch"}},"commits":{"C0":{"type":"commit","parents":[],"author":"Peter Cottle","createTime":"Wed Jul 24 2013 12:34:45 GMT-0700 (PDT)","commitMessage":"Quick commit. Go Bears!","id":"C0","rootCommit":true},"C1":{"type":"commit","parents":["C0"],"author":"Peter Cottle","createTime":"Wed Jul 24 2013 12:34:45 GMT-0700 (PDT)","commitMessage":"Quick commit. Go Bears!","id":"C1"},"C2":{"type":"commit","parents":["C1"],"author":"Peter Cottle","createTime":"Wed Jul 24 2013 12:34:45 GMT-0700 (PDT)","commitMessage":"Quick commit. Go Bears!","id":"C2"},"C3":{"type":"commit","parents":["C1"],"author":"Peter Cottle","createTime":"Wed Jul 24 2013 12:34:45 GMT-0700 (PDT)","commitMessage":"Quick commit. Go Bears!","id":"C3"}},"HEAD":{"target":"master","id":"HEAD","type":"general ref"}}}',
      {
        // committing remotely on other branch is fine
        '{"branches":{"master":{"target":"C3","id":"master"},"side":{"target":"C2","id":"side"},"o/master":{"target":"C3","id":"o/master"},"o/side":{"target":"C2","id":"o/side"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"},"C3":{"parents":["C1"],"id":"C3"}},"HEAD":{"target":"master","id":"HEAD"},"originTree":{"branches":{"master":{"remoteTrackingBranch":null,"remote":false,"target":"C3","id":"master","type":"branch"},"side":{"remoteTrackingBranch":null,"remote":false,"target":"C4","id":"side","type":"branch"}},"commits":{"C0":{"type":"commit","parents":[],"author":"Peter Cottle","createTime":"Wed Jul 24 2013 12:34:45 GMT-0700 (PDT)","commitMessage":"Quick commit. Go Bears!","id":"C0","rootCommit":true},"C1":{"type":"commit","parents":["C0"],"author":"Peter Cottle","createTime":"Wed Jul 24 2013 12:34:45 GMT-0700 (PDT)","commitMessage":"Quick commit. Go Bears!","id":"C1"},"C2":{"type":"commit","parents":["C1"],"author":"Peter Cottle","createTime":"Wed Jul 24 2013 12:34:45 GMT-0700 (PDT)","commitMessage":"Quick commit. Go Bears!","id":"C2"},"C3":{"type":"commit","parents":["C1"],"author":"Peter Cottle","createTime":"Wed Jul 24 2013 12:34:45 GMT-0700 (PDT)","commitMessage":"Quick commit. Go Bears!","id":"C3"},"C4":{"type":"commit","parents":["C2"],"author":"Peter Cottle","createTime":"Wed Jul 24 2013 12:35:38 GMT-0700 (PDT)","commitMessage":"Quick commit. Go Bears!","id":"C4"}},"HEAD":{"target":"side","id":"HEAD","type":"general ref"}}}': true,
        // but committing on master is not ok
        '{"branches":{"master":{"target":"C3","id":"master"},"side":{"target":"C2","id":"side"},"o/master":{"target":"C3","id":"o/master"},"o/side":{"target":"C2","id":"o/side"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"},"C3":{"parents":["C1"],"id":"C3"}},"HEAD":{"target":"master","id":"HEAD"},"originTree":{"branches":{"master":{"remoteTrackingBranch":null,"remote":false,"target":"C5","id":"master","type":"branch"},"side":{"remoteTrackingBranch":null,"remote":false,"target":"C4","id":"side","type":"branch"}},"commits":{"C0":{"type":"commit","parents":[],"author":"Peter Cottle","createTime":"Wed Jul 24 2013 12:34:45 GMT-0700 (PDT)","commitMessage":"Quick commit. Go Bears!","id":"C0","rootCommit":true},"C1":{"type":"commit","parents":["C0"],"author":"Peter Cottle","createTime":"Wed Jul 24 2013 12:34:45 GMT-0700 (PDT)","commitMessage":"Quick commit. Go Bears!","id":"C1"},"C2":{"type":"commit","parents":["C1"],"author":"Peter Cottle","createTime":"Wed Jul 24 2013 12:34:45 GMT-0700 (PDT)","commitMessage":"Quick commit. Go Bears!","id":"C2"},"C3":{"type":"commit","parents":["C1"],"author":"Peter Cottle","createTime":"Wed Jul 24 2013 12:34:45 GMT-0700 (PDT)","commitMessage":"Quick commit. Go Bears!","id":"C3"},"C4":{"type":"commit","parents":["C2"],"author":"Peter Cottle","createTime":"Wed Jul 24 2013 12:35:38 GMT-0700 (PDT)","commitMessage":"Quick commit. Go Bears!","id":"C4"},"C5":{"type":"commit","parents":["C3"],"author":"Peter Cottle","createTime":"Wed Jul 24 2013 12:37:44 GMT-0700 (PDT)","commitMessage":"Quick commit. Go Bears!","id":"C5"}},"HEAD":{"target":"master","id":"HEAD","type":"general ref"}}}': false
      }
    );
  });

  it('has default behavior to check all branches and master', function() {
    testMethod(
      {}, // default method, which is compare all branches and HEAD
      "{\"branches\":{\"master\":{\"target\":\"C3\",\"id\":\"master\"}},\"commits\":{\"C0\":{\"parents\":[],\"id\":\"C0\",\"rootCommit\":true},\"C1\":{\"parents\":[\"C0\"],\"id\":\"C1\"},\"C2\":{\"parents\":[\"C1\"],\"id\":\"C2\"},\"C3\":{\"parents\":[\"C2\"],\"id\":\"C3\"}},\"HEAD\":{\"target\":\"master\",\"id\":\"HEAD\"}}",
      {
        // side branch that is checked out
        '{"branches":{"master":{"target":"C2","id":"master"},"side":{"target":"C3","id":"side"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"},"C3":{"parents":["C2"],"id":"C3"}},"HEAD":{"target":"side","id":"HEAD"}}': false,
        // head detached
        '{"branches":{"master":{"target":"C3","id":"master"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"},"C3":{"parents":["C2"],"id":"C3"}},"HEAD":{"target":"C2","id":"HEAD"}}': false
      }
    );
  });

  it('compares only master', function() {
    testMethod(
      { compareOnlyMaster: true },
      "{\"branches\":{\"master\":{\"target\":\"C3\",\"id\":\"master\"}},\"commits\":{\"C0\":{\"parents\":[],\"id\":\"C0\",\"rootCommit\":true},\"C1\":{\"parents\":[\"C0\"],\"id\":\"C1\"},\"C2\":{\"parents\":[\"C1\"],\"id\":\"C2\"},\"C3\":{\"parents\":[\"C2\"],\"id\":\"C3\"}},\"HEAD\":{\"target\":\"master\",\"id\":\"HEAD\"}}",
      {
        // side branch that is checked out
        '{"branches":{"master":{"target":"C3","id":"master"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"},"C3":{"parents":["C2"],"id":"C3"}},"HEAD":{"target":"master","id":"HEAD"}}': true,
        // head detached
        '{"branches":{"master":{"target":"C3","id":"master"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"},"C3":{"parents":["C2"],"id":"C3"}},"HEAD":{"target":"C2","id":"HEAD"}}': true
      }
    );
  });

  it('compares all branches with hash agnostic', function() {
    var selectiveRebaseGoal = require('../src/levels/rebase/selectiveRebase').level.goalTreeString;
    testMethod(
      { compareAllBranchesHashAgnostic: true },
      selectiveRebaseGoal,
      {
        // almost done with level
        '%7B%22branches%22%3A%7B%22master%22%3A%7B%22target%22%3A%22C5%22%2C%22id%22%3A%22master%22%7D%2C%22one%22%3A%7B%22target%22%3A%22C2%27%5E4%22%2C%22id%22%3A%22one%22%7D%2C%22two%22%3A%7B%22target%22%3A%22C3%27%27%22%2C%22id%22%3A%22two%22%7D%2C%22three%22%3A%7B%22target%22%3A%22C2%27%27%22%2C%22id%22%3A%22three%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22parents%22%3A%5B%5D%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C1%22%7D%2C%22C2%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%22%7D%2C%22C3%22%3A%7B%22parents%22%3A%5B%22C2%22%5D%2C%22id%22%3A%22C3%22%7D%2C%22C4%22%3A%7B%22parents%22%3A%5B%22C3%22%5D%2C%22id%22%3A%22C4%22%7D%2C%22C5%22%3A%7B%22parents%22%3A%5B%22C4%22%5D%2C%22id%22%3A%22C5%22%7D%2C%22C2%27%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%27%22%7D%2C%22C2%27%27%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%27%27%22%7D%2C%22C4%27%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C4%27%22%7D%2C%22C3%27%22%3A%7B%22parents%22%3A%5B%22C4%27%22%5D%2C%22id%22%3A%22C3%27%22%7D%2C%22C2%27%27%27%22%3A%7B%22parents%22%3A%5B%22C3%27%22%5D%2C%22id%22%3A%22C2%27%27%27%22%7D%2C%22C2%27%5E4%22%3A%7B%22parents%22%3A%5B%22C3%27%22%5D%2C%22id%22%3A%22C2%27%5E4%22%7D%2C%22C5%27%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C5%27%22%7D%2C%22C4%27%27%22%3A%7B%22parents%22%3A%5B%22C5%27%22%5D%2C%22id%22%3A%22C4%27%27%22%7D%2C%22C3%27%27%22%3A%7B%22parents%22%3A%5B%22C4%27%27%22%5D%2C%22id%22%3A%22C3%27%27%22%7D%7D%2C%22HEAD%22%3A%7B%22target%22%3A%22two%22%2C%22id%22%3A%22HEAD%22%7D%7D': false,
        // done with level, tons of extra ammends
        '%7B%22branches%22%3A%7B%22master%22%3A%7B%22target%22%3A%22C5%22%2C%22id%22%3A%22master%22%7D%2C%22one%22%3A%7B%22target%22%3A%22C2%27%5E4%22%2C%22id%22%3A%22one%22%7D%2C%22two%22%3A%7B%22target%22%3A%22C2%27%5E5%22%2C%22id%22%3A%22two%22%7D%2C%22three%22%3A%7B%22target%22%3A%22C2%27%27%22%2C%22id%22%3A%22three%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22parents%22%3A%5B%5D%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C1%22%7D%2C%22C2%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%22%7D%2C%22C3%22%3A%7B%22parents%22%3A%5B%22C2%22%5D%2C%22id%22%3A%22C3%22%7D%2C%22C4%22%3A%7B%22parents%22%3A%5B%22C3%22%5D%2C%22id%22%3A%22C4%22%7D%2C%22C5%22%3A%7B%22parents%22%3A%5B%22C4%22%5D%2C%22id%22%3A%22C5%22%7D%2C%22C2%27%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%27%22%7D%2C%22C2%27%27%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%27%27%22%7D%2C%22C4%27%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C4%27%22%7D%2C%22C3%27%22%3A%7B%22parents%22%3A%5B%22C4%27%22%5D%2C%22id%22%3A%22C3%27%22%7D%2C%22C2%27%27%27%22%3A%7B%22parents%22%3A%5B%22C3%27%22%5D%2C%22id%22%3A%22C2%27%27%27%22%7D%2C%22C2%27%5E4%22%3A%7B%22parents%22%3A%5B%22C3%27%22%5D%2C%22id%22%3A%22C2%27%5E4%22%7D%2C%22C5%27%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C5%27%22%7D%2C%22C4%27%27%22%3A%7B%22parents%22%3A%5B%22C5%27%22%5D%2C%22id%22%3A%22C4%27%27%22%7D%2C%22C3%27%27%22%3A%7B%22parents%22%3A%5B%22C4%27%27%22%5D%2C%22id%22%3A%22C3%27%27%22%7D%2C%22C2%27%5E5%22%3A%7B%22parents%22%3A%5B%22C3%27%27%22%5D%2C%22id%22%3A%22C2%27%5E5%22%7D%7D%2C%22HEAD%22%3A%7B%22target%22%3A%22two%22%2C%22id%22%3A%22HEAD%22%7D%7D': true,
        // head checked out
        '%7B%22branches%22%3A%7B%22master%22%3A%7B%22target%22%3A%22C5%22%2C%22id%22%3A%22master%22%7D%2C%22one%22%3A%7B%22target%22%3A%22C2%27%5E4%22%2C%22id%22%3A%22one%22%7D%2C%22two%22%3A%7B%22target%22%3A%22C2%27%5E5%22%2C%22id%22%3A%22two%22%7D%2C%22three%22%3A%7B%22target%22%3A%22C2%27%27%22%2C%22id%22%3A%22three%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22parents%22%3A%5B%5D%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C1%22%7D%2C%22C2%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%22%7D%2C%22C3%22%3A%7B%22parents%22%3A%5B%22C2%22%5D%2C%22id%22%3A%22C3%22%7D%2C%22C4%22%3A%7B%22parents%22%3A%5B%22C3%22%5D%2C%22id%22%3A%22C4%22%7D%2C%22C5%22%3A%7B%22parents%22%3A%5B%22C4%22%5D%2C%22id%22%3A%22C5%22%7D%2C%22C2%27%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%27%22%7D%2C%22C2%27%27%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%27%27%22%7D%2C%22C4%27%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C4%27%22%7D%2C%22C3%27%22%3A%7B%22parents%22%3A%5B%22C4%27%22%5D%2C%22id%22%3A%22C3%27%22%7D%2C%22C2%27%27%27%22%3A%7B%22parents%22%3A%5B%22C3%27%22%5D%2C%22id%22%3A%22C2%27%27%27%22%7D%2C%22C2%27%5E4%22%3A%7B%22parents%22%3A%5B%22C3%27%22%5D%2C%22id%22%3A%22C2%27%5E4%22%7D%2C%22C5%27%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C5%27%22%7D%2C%22C4%27%27%22%3A%7B%22parents%22%3A%5B%22C5%27%22%5D%2C%22id%22%3A%22C4%27%27%22%7D%2C%22C3%27%27%22%3A%7B%22parents%22%3A%5B%22C4%27%27%22%5D%2C%22id%22%3A%22C3%27%27%22%7D%2C%22C2%27%5E5%22%3A%7B%22parents%22%3A%5B%22C3%27%27%22%5D%2C%22id%22%3A%22C2%27%5E5%22%7D%7D%2C%22HEAD%22%3A%7B%22target%22%3A%22C3%22%2C%22id%22%3A%22HEAD%22%7D%7D': true,
        // head checked out and master forced one up
        '%7B%22branches%22%3A%7B%22master%22%3A%7B%22target%22%3A%22C4%22%2C%22id%22%3A%22master%22%7D%2C%22one%22%3A%7B%22target%22%3A%22C2%27%5E4%22%2C%22id%22%3A%22one%22%7D%2C%22two%22%3A%7B%22target%22%3A%22C2%27%5E5%22%2C%22id%22%3A%22two%22%7D%2C%22three%22%3A%7B%22target%22%3A%22C2%27%27%22%2C%22id%22%3A%22three%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22parents%22%3A%5B%5D%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C1%22%7D%2C%22C2%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%22%7D%2C%22C3%22%3A%7B%22parents%22%3A%5B%22C2%22%5D%2C%22id%22%3A%22C3%22%7D%2C%22C4%22%3A%7B%22parents%22%3A%5B%22C3%22%5D%2C%22id%22%3A%22C4%22%7D%2C%22C5%22%3A%7B%22parents%22%3A%5B%22C4%22%5D%2C%22id%22%3A%22C5%22%7D%2C%22C2%27%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%27%22%7D%2C%22C2%27%27%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%27%27%22%7D%2C%22C4%27%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C4%27%22%7D%2C%22C3%27%22%3A%7B%22parents%22%3A%5B%22C4%27%22%5D%2C%22id%22%3A%22C3%27%22%7D%2C%22C2%27%27%27%22%3A%7B%22parents%22%3A%5B%22C3%27%22%5D%2C%22id%22%3A%22C2%27%27%27%22%7D%2C%22C2%27%5E4%22%3A%7B%22parents%22%3A%5B%22C3%27%22%5D%2C%22id%22%3A%22C2%27%5E4%22%7D%2C%22C5%27%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C5%27%22%7D%2C%22C4%27%27%22%3A%7B%22parents%22%3A%5B%22C5%27%22%5D%2C%22id%22%3A%22C4%27%27%22%7D%2C%22C3%27%27%22%3A%7B%22parents%22%3A%5B%22C4%27%27%22%5D%2C%22id%22%3A%22C3%27%27%22%7D%2C%22C2%27%5E5%22%3A%7B%22parents%22%3A%5B%22C3%27%27%22%5D%2C%22id%22%3A%22C2%27%5E5%22%7D%7D%2C%22HEAD%22%3A%7B%22target%22%3A%22C3%22%2C%22id%22%3A%22HEAD%22%7D%7D': false
      }
    );
  });

  it('compares only master with hash agnostic', function() {
    var manyRebaseGoal = require('../src/levels/rebase/manyRebases').level.goalTreeString;
    testMethod(
      { compareOnlyMasterHashAgnostic: true },
      manyRebaseGoal,
      {
        // almost done with level
        '%7B%22branches%22%3A%7B%22master%22%3A%7B%22target%22%3A%22C6%27%22%2C%22id%22%3A%22master%22%7D%2C%22bugFix%22%3A%7B%22target%22%3A%22C3%22%2C%22id%22%3A%22bugFix%22%7D%2C%22side%22%3A%7B%22target%22%3A%22C6%22%2C%22id%22%3A%22side%22%7D%2C%22another%22%3A%7B%22target%22%3A%22C7%22%2C%22id%22%3A%22another%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22parents%22%3A%5B%5D%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C1%22%7D%2C%22C2%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%22%7D%2C%22C3%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C3%22%7D%2C%22C4%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C4%22%7D%2C%22C5%22%3A%7B%22parents%22%3A%5B%22C4%22%5D%2C%22id%22%3A%22C5%22%7D%2C%22C6%22%3A%7B%22parents%22%3A%5B%22C5%22%5D%2C%22id%22%3A%22C6%22%7D%2C%22C7%22%3A%7B%22parents%22%3A%5B%22C5%22%5D%2C%22id%22%3A%22C7%22%7D%2C%22C3%27%22%3A%7B%22parents%22%3A%5B%22C2%22%5D%2C%22id%22%3A%22C3%27%22%7D%2C%22C4%27%22%3A%7B%22parents%22%3A%5B%22C3%27%22%5D%2C%22id%22%3A%22C4%27%22%7D%2C%22C5%27%22%3A%7B%22parents%22%3A%5B%22C4%27%22%5D%2C%22id%22%3A%22C5%27%22%7D%2C%22C6%27%22%3A%7B%22parents%22%3A%5B%22C5%27%22%5D%2C%22id%22%3A%22C6%27%22%7D%7D%2C%22HEAD%22%3A%7B%22target%22%3A%22master%22%2C%22id%22%3A%22HEAD%22%7D%7D': false,
        // right form but master not FF-ed
        '%7B%22branches%22%3A%7B%22master%22%3A%7B%22target%22%3A%22C6%27%22%2C%22id%22%3A%22master%22%7D%2C%22bugFix%22%3A%7B%22target%22%3A%22C3%22%2C%22id%22%3A%22bugFix%22%7D%2C%22side%22%3A%7B%22target%22%3A%22C6%22%2C%22id%22%3A%22side%22%7D%2C%22another%22%3A%7B%22target%22%3A%22C7%27%22%2C%22id%22%3A%22another%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22parents%22%3A%5B%5D%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C1%22%7D%2C%22C2%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%22%7D%2C%22C3%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C3%22%7D%2C%22C4%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C4%22%7D%2C%22C5%22%3A%7B%22parents%22%3A%5B%22C4%22%5D%2C%22id%22%3A%22C5%22%7D%2C%22C6%22%3A%7B%22parents%22%3A%5B%22C5%22%5D%2C%22id%22%3A%22C6%22%7D%2C%22C7%22%3A%7B%22parents%22%3A%5B%22C5%22%5D%2C%22id%22%3A%22C7%22%7D%2C%22C3%27%22%3A%7B%22parents%22%3A%5B%22C2%22%5D%2C%22id%22%3A%22C3%27%22%7D%2C%22C4%27%22%3A%7B%22parents%22%3A%5B%22C3%27%22%5D%2C%22id%22%3A%22C4%27%22%7D%2C%22C5%27%22%3A%7B%22parents%22%3A%5B%22C4%27%22%5D%2C%22id%22%3A%22C5%27%22%7D%2C%22C6%27%22%3A%7B%22parents%22%3A%5B%22C5%27%22%5D%2C%22id%22%3A%22C6%27%22%7D%2C%22C7%27%22%3A%7B%22parents%22%3A%5B%22C6%27%22%5D%2C%22id%22%3A%22C7%27%22%7D%7D%2C%22HEAD%22%3A%7B%22target%22%3A%22another%22%2C%22id%22%3A%22HEAD%22%7D%7D': false,
        // master FF-ed and solved
        '%7B%22branches%22%3A%7B%22master%22%3A%7B%22target%22%3A%22C7%27%22%2C%22id%22%3A%22master%22%7D%2C%22bugFix%22%3A%7B%22target%22%3A%22C3%22%2C%22id%22%3A%22bugFix%22%7D%2C%22side%22%3A%7B%22target%22%3A%22C6%22%2C%22id%22%3A%22side%22%7D%2C%22another%22%3A%7B%22target%22%3A%22C7%27%22%2C%22id%22%3A%22another%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22parents%22%3A%5B%5D%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C1%22%7D%2C%22C2%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%22%7D%2C%22C3%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C3%22%7D%2C%22C4%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C4%22%7D%2C%22C5%22%3A%7B%22parents%22%3A%5B%22C4%22%5D%2C%22id%22%3A%22C5%22%7D%2C%22C6%22%3A%7B%22parents%22%3A%5B%22C5%22%5D%2C%22id%22%3A%22C6%22%7D%2C%22C7%22%3A%7B%22parents%22%3A%5B%22C5%22%5D%2C%22id%22%3A%22C7%22%7D%2C%22C3%27%22%3A%7B%22parents%22%3A%5B%22C2%22%5D%2C%22id%22%3A%22C3%27%22%7D%2C%22C4%27%22%3A%7B%22parents%22%3A%5B%22C3%27%22%5D%2C%22id%22%3A%22C4%27%22%7D%2C%22C5%27%22%3A%7B%22parents%22%3A%5B%22C4%27%22%5D%2C%22id%22%3A%22C5%27%22%7D%2C%22C6%27%22%3A%7B%22parents%22%3A%5B%22C5%27%22%5D%2C%22id%22%3A%22C6%27%22%7D%2C%22C7%27%22%3A%7B%22parents%22%3A%5B%22C6%27%22%5D%2C%22id%22%3A%22C7%27%22%7D%7D%2C%22HEAD%22%3A%7B%22target%22%3A%22master%22%2C%22id%22%3A%22HEAD%22%7D%7D': true,
        // HEAD checked out after solve
        '%7B%22branches%22%3A%7B%22master%22%3A%7B%22target%22%3A%22C7%27%22%2C%22id%22%3A%22master%22%7D%2C%22bugFix%22%3A%7B%22target%22%3A%22C3%22%2C%22id%22%3A%22bugFix%22%7D%2C%22side%22%3A%7B%22target%22%3A%22C6%22%2C%22id%22%3A%22side%22%7D%2C%22another%22%3A%7B%22target%22%3A%22C7%27%22%2C%22id%22%3A%22another%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22parents%22%3A%5B%5D%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C1%22%7D%2C%22C2%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%22%7D%2C%22C3%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C3%22%7D%2C%22C4%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C4%22%7D%2C%22C5%22%3A%7B%22parents%22%3A%5B%22C4%22%5D%2C%22id%22%3A%22C5%22%7D%2C%22C6%22%3A%7B%22parents%22%3A%5B%22C5%22%5D%2C%22id%22%3A%22C6%22%7D%2C%22C7%22%3A%7B%22parents%22%3A%5B%22C5%22%5D%2C%22id%22%3A%22C7%22%7D%2C%22C3%27%22%3A%7B%22parents%22%3A%5B%22C2%22%5D%2C%22id%22%3A%22C3%27%22%7D%2C%22C4%27%22%3A%7B%22parents%22%3A%5B%22C3%27%22%5D%2C%22id%22%3A%22C4%27%22%7D%2C%22C5%27%22%3A%7B%22parents%22%3A%5B%22C4%27%22%5D%2C%22id%22%3A%22C5%27%22%7D%2C%22C6%27%22%3A%7B%22parents%22%3A%5B%22C5%27%22%5D%2C%22id%22%3A%22C6%27%22%7D%2C%22C7%27%22%3A%7B%22parents%22%3A%5B%22C6%27%22%5D%2C%22id%22%3A%22C7%27%22%7D%7D%2C%22HEAD%22%3A%7B%22target%22%3A%22C6%22%2C%22id%22%3A%22HEAD%22%7D%7D': true
      }
    );
  });

  it('compares only master with hash agnostic and asserts', function() {
    var jugglingGoal = require('../src/levels/mixed/jugglingCommits').level.goalTreeString;
    testMethod(
      {
        compareOnlyMasterHashAgnosticWithAsserts: true,
        'goalAsserts': {
          'master': [
            function(data) {
              return data.C2 > data.C3;
            },
            function(data) {
              return data.C2 > data.C1;
            }
          ]
        }
      },
      jugglingGoal,
      {
        // level solved with a bunch of extra dangling commits
        '%7B%22branches%22%3A%7B%22master%22%3A%7B%22target%22%3A%22C3%27%27%22%2C%22id%22%3A%22master%22%7D%2C%22newImage%22%3A%7B%22target%22%3A%22C2%22%2C%22id%22%3A%22newImage%22%7D%2C%22caption%22%3A%7B%22target%22%3A%22C3%22%2C%22id%22%3A%22caption%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22parents%22%3A%5B%5D%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C1%22%7D%2C%22C2%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%22%7D%2C%22C3%22%3A%7B%22parents%22%3A%5B%22C2%22%5D%2C%22id%22%3A%22C3%22%7D%2C%22C3%27%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C3%27%22%7D%2C%22C2%27%22%3A%7B%22parents%22%3A%5B%22C3%27%22%5D%2C%22id%22%3A%22C2%27%22%7D%2C%22C2%27%27%22%3A%7B%22parents%22%3A%5B%22C3%27%22%5D%2C%22id%22%3A%22C2%27%27%22%7D%2C%22C2%27%27%27%22%3A%7B%22parents%22%3A%5B%22C3%27%22%5D%2C%22id%22%3A%22C2%27%27%27%22%7D%2C%22C2%27%5E4%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%27%5E4%22%7D%2C%22C3%27%27%22%3A%7B%22parents%22%3A%5B%22C2%27%5E4%22%5D%2C%22id%22%3A%22C3%27%27%22%7D%7D%2C%22HEAD%22%3A%7B%22target%22%3A%22master%22%2C%22id%22%3A%22HEAD%22%7D%7D': true,
        // level with the number of hashes being equal when we want more
        '%7B%22branches%22%3A%7B%22master%22%3A%7B%22target%22%3A%22C3%27%5E7%22%2C%22id%22%3A%22master%22%7D%2C%22newImage%22%3A%7B%22target%22%3A%22C2%22%2C%22id%22%3A%22newImage%22%7D%2C%22caption%22%3A%7B%22target%22%3A%22C3%22%2C%22id%22%3A%22caption%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22parents%22%3A%5B%5D%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22parents%22%3A%5B%22C0%22%5D%2C%22id%22%3A%22C1%22%7D%2C%22C2%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%22%7D%2C%22C3%22%3A%7B%22parents%22%3A%5B%22C2%22%5D%2C%22id%22%3A%22C3%22%7D%2C%22C3%27%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C3%27%22%7D%2C%22C2%27%22%3A%7B%22parents%22%3A%5B%22C3%27%22%5D%2C%22id%22%3A%22C2%27%22%7D%2C%22C2%27%27%22%3A%7B%22parents%22%3A%5B%22C3%27%22%5D%2C%22id%22%3A%22C2%27%27%22%7D%2C%22C2%27%27%27%22%3A%7B%22parents%22%3A%5B%22C3%27%22%5D%2C%22id%22%3A%22C2%27%27%27%22%7D%2C%22C2%27%5E4%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%27%5E4%22%7D%2C%22C3%27%27%22%3A%7B%22parents%22%3A%5B%22C2%27%5E4%22%5D%2C%22id%22%3A%22C3%27%27%22%7D%2C%22C3%27%27%27%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C3%27%27%27%22%7D%2C%22C2%27%5E5%22%3A%7B%22parents%22%3A%5B%22C3%27%27%27%22%5D%2C%22id%22%3A%22C2%27%5E5%22%7D%2C%22C2%27%5E6%22%3A%7B%22parents%22%3A%5B%22C3%27%27%27%22%5D%2C%22id%22%3A%22C2%27%5E6%22%7D%2C%22C2%27%5E7%22%3A%7B%22parents%22%3A%5B%22C1%22%5D%2C%22id%22%3A%22C2%27%5E7%22%7D%2C%22C3%27%5E4%22%3A%7B%22parents%22%3A%5B%22C2%27%5E7%22%5D%2C%22id%22%3A%22C3%27%5E4%22%7D%2C%22C3%27%5E5%22%3A%7B%22parents%22%3A%5B%22C2%27%5E7%22%5D%2C%22id%22%3A%22C3%27%5E5%22%7D%2C%22C3%27%5E6%22%3A%7B%22parents%22%3A%5B%22C2%27%5E7%22%5D%2C%22id%22%3A%22C3%27%5E6%22%7D%2C%22C3%27%5E7%22%3A%7B%22parents%22%3A%5B%22C2%27%5E7%22%5D%2C%22id%22%3A%22C3%27%5E7%22%7D%2C%22C3%27%5E8%22%3A%7B%22parents%22%3A%5B%22C2%27%5E7%22%5D%2C%22id%22%3A%22C3%27%5E8%22%7D%7D%2C%22HEAD%22%3A%7B%22target%22%3A%22master%22%2C%22id%22%3A%22HEAD%22%7D%7D': false
      }
    );
  });
});


var _ = require('underscore');
var Q = require('q');
// horrible hack to get localStorage Backbone plugin
var Backbone = (!require('../util').isBrowser()) ? require('backbone') : window.Backbone;

var util = require('../util');
var intl = require('../intl');
var Main = require('../app');
var Errors = require('../util/errors');

var Visualization = require('../visuals/visualization').Visualization;
var ParseWaterfall = require('../level/parseWaterfall').ParseWaterfall;
var DisabledMap = require('../level/disabledMap').DisabledMap;
var Command = require('../models/commandModel').Command;
var GitShim = require('../git/gitShim').GitShim;

var Views = require('../views');
var ModalTerminal = Views.ModalTerminal;
var ModalAlert = Views.ModalAlert;
var BuilderViews = require('../views/builderViews');
var MultiView = require('../views/multiView').MultiView;

var Sandbox = Backbone.View.extend({
  // tag name here is purely vestigial. I made this a view
  // simply to use inheritance and have a nice event system in place
  tagName: 'div',
  initialize: function(options) {
    options = options || {};
    this.options = options;

    this.initVisualization(options);
    this.initCommandCollection(options);
    this.initParseWaterfall(options);
    this.initGitShim(options);
    this.initUndoStack(options);

    if (!options.wait) {
      this.takeControl();
    }
  },

  getDefaultVisEl: function() {
    return $('#mainVisSpace')[0];
  },

  getAnimationTime: function() { return 700 * 1.5; },

  initVisualization: function(options) {
    this.mainVis = new Visualization({
      el: options.el || this.getDefaultVisEl()
    });
  },

  initUndoStack: function(options) {
    this.undoStack = [];
  },

  initCommandCollection: function(options) {
    // don't add it to just any collection -- adding to the
    // CommandUI collection will put in history
    this.commandCollection = Main.getCommandUI().commandCollection;
  },

  initParseWaterfall: function(options) {
    this.parseWaterfall = new ParseWaterfall();
  },

  initGitShim: function(options) {
    this.gitShim = new GitShim({
      beforeCB: _.bind(this.beforeCommandCB, this)
    });
  },

  takeControl: function() {
    // we will be handling commands that are submitted, mainly to add the sanadbox
    // functionality (which is included by default in ParseWaterfall())
    Main.getEventBaton().stealBaton('commandSubmitted', this.commandSubmitted, this);
    // we obviously take care of sandbox commands
    Main.getEventBaton().stealBaton('processSandboxCommand', this.processSandboxCommand, this);

    // a few things to help transition between levels and sandbox
    Main.getEventBaton().stealBaton('levelExited', this.levelExited, this);

    this.insertGitShim();
  },

  releaseControl: function() {
    // we will be handling commands that are submitted, mainly to add the sanadbox
    // functionality (which is included by default in ParseWaterfall())
    Main.getEventBaton().releaseBaton('commandSubmitted', this.commandSubmitted, this);
    // we obviously take care of sandbox commands
    Main.getEventBaton().releaseBaton('processSandboxCommand', this.processSandboxCommand, this);
    // a few things to help transition between levels and sandbox
    Main.getEventBaton().releaseBaton('levelExited', this.levelExited, this);

    this.releaseGitShim();
  },

  releaseGitShim: function() {
    if (this.gitShim) {
      this.gitShim.removeShim();
    }
  },

  insertGitShim: function() {
    // and our git shim goes in after the git engine is ready so it doesn't steal the baton
    // too early
    if (this.gitShim) {
      this.mainVis.customEvents.on('gitEngineReady', function() {
          this.gitShim.insertShim();
      },this);
    }
  },

  beforeCommandCB: function(command) {
    this.pushUndo();
  },

  pushUndo: function() {
    // go ahead and push the three onto the stack
    this.undoStack.push(this.mainVis.gitEngine.printTree());
  },

  undo: function(command, deferred) {
    var toRestore = this.undoStack.pop();
    if (!toRestore) {
      command.set('error', new Errors.GitError({
        msg: intl.str('undo-stack-empty')
      }));
      deferred.resolve();
      return;
    }

    this.mainVis.reset(toRestore);
    setTimeout(function() {
      command.finishWith(deferred);
    }, this.mainVis.getAnimationTime());
  },

  commandSubmitted: function(value) {
    // allow other things to see this command (aka command history on terminal)
    Main.getEvents().trigger('commandSubmittedPassive', value);

    util.splitTextCommand(value, function(command) {
      this.commandCollection.add(new Command({
        rawStr: command,
        parseWaterfall: this.parseWaterfall
      }));
    }, this);
  },

  startLevel: function(command, deferred) {
    var regexResults = command.get('regexResults') || [];
    var desiredID = regexResults[1] || '';
    var levelJSON = Main.getLevelArbiter().getLevel(desiredID);

    // handle the case where that level is not found...
    if (!levelJSON) {
      command.addWarning(
        intl.str(
          'level-no-id',
          { id: desiredID }
        )
      );
      Main.getEventBaton().trigger('commandSubmitted', 'levels');

      command.set('status', 'error');
      deferred.resolve();
      return;
    }

    // we are good to go!! lets prep a bit visually
    this.hide();
    this.clear();

    // we don't even need a reference to this,
    // everything will be handled via event baton :DDDDDDDDD
    var whenLevelOpen = Q.defer();
    var Level = require('../level').Level;

    this.currentLevel = new Level({
      level: levelJSON,
      deferred: whenLevelOpen,
      command: command
    });

    whenLevelOpen.promise.then(function() {
      command.finishWith(deferred);
    });
  },

  buildLevel: function(command, deferred) {
    this.hide();
    this.clear();

    var whenBuilderOpen = Q.defer();

    var LevelBuilder = require('../level/builder').LevelBuilder;
    this.levelBuilder = new LevelBuilder({
      deferred: whenBuilderOpen
    });

    whenBuilderOpen.promise.then(function() {
      command.finishWith(deferred);
    });
  },

  exitLevel: function(command, deferred) {
    command.addWarning(
      intl.str('level-cant-exit')
    );
    command.set('status', 'error');
    deferred.resolve();
  },

  showLevels: function(command, deferred) {
    var whenClosed = Q.defer();
    Main.getLevelDropdown().show(whenClosed, command);
    whenClosed.promise.done(function() {
      command.finishWith(deferred);
    });
  },

  resetSolved: function(command, deferred) {
    Main.getLevelArbiter().resetSolvedMap();
    command.addWarning(
      intl.str('solved-map-reset')
    );
    command.finishWith(deferred);
  },

  processSandboxCommand: function(command, deferred) {
    // I'm tempted to do camcel case conversion, but there are
    // some exceptions to the rule
    var commandMap = {
      'reset solved': this.resetSolved,
      'undo': this.undo,
      'help general': this.helpDialog,
      'help': this.helpDialog,
      'reset': this.reset,
      'delay': this.delay,
      'clear': this.clear,
      'exit level': this.exitLevel,
      'level': this.startLevel,
      'sandbox': this.exitLevel,
      'levels': this.showLevels,
      'mobileAlert': this.mobileAlert,
      'build level': this.buildLevel,
      'export tree': this.exportTree,
      'import tree': this.importTree,
      'importTreeNow': this.importTreeNow,
      'import level': this.importLevel
    };

    var method = commandMap[command.get('method')];
    if (!method) { throw new Error('no method for that wut'); }

    method.apply(this, [command, deferred]);
  },

  hide: function() {
    this.mainVis.hide();
  },

  levelExited: function() {
    this.show();
  },

  show: function() {
    this.mainVis.show();
  },

  importTreeNow: function(command, deferred) {
    var options = command.get('regexResults') || [];
    if (options.length < 2) {
      command.set('error', new Errors.GitError({
        msg: intl.str('git-error-options')
      }));
    } else {
      var string = options.input.replace(/importTreeNow\s+/g, '');
      try {
        this.mainVis.gitEngine.loadTreeFromString(string);
      } catch (e) {
        command.set('error', new Errors.GitError({
          msg: String(e)
        }));
      }
    }

    command.finishWith(deferred);
  },

  importTree: function(command, deferred) {
    var jsonGrabber = new BuilderViews.MarkdownPresenter({
      previewText: intl.str('paste-json'),
      fillerText: ' '
    });
    jsonGrabber.deferred.promise
    .then(_.bind(function(treeJSON) {
      try {
        this.mainVis.gitEngine.loadTree(JSON.parse(treeJSON));
      } catch(e) {
        this.mainVis.reset();
        new MultiView({
          childViews: [{
            type: 'ModalAlert',
            options: {
              markdowns: [
                '## Error!',
                '',
                'Something is wrong with that JSON! Here is the error:',
                '',
                String(e)
              ]
            }
          }]
        });
      }
    }, this))
    .fail(function() { })
    .done(function() {
      command.finishWith(deferred);
    });
  },

  importLevel: function(command, deferred) {
    var jsonGrabber = new BuilderViews.MarkdownPresenter({
      previewText: intl.str('paste-json'),
      fillerText: ' '
    });

    jsonGrabber.deferred.promise
    .then(_.bind(function(inputText) {
      var Level = require('../level').Level;
      try {
        var levelJSON = JSON.parse(inputText);
        var whenLevelOpen = Q.defer();
        this.currentLevel = new Level({
          level: levelJSON,
          deferred: whenLevelOpen,
          command: command
        });
        this.hide();

        whenLevelOpen.promise.then(function() {
          command.finishWith(deferred);
        });
      } catch(e) {
        new MultiView({
          childViews: [{
            type: 'ModalAlert',
            options: {
              markdowns: [
                '## Error!',
                '',
                'Something is wrong with that level JSON, this happened:',
                '',
                String(e)
              ]
            }
          }]
        });
        command.finishWith(deferred);
      }
    }, this))
    .fail(function() {
      command.finishWith(deferred);
    })
    .done();
  },

  exportTree: function(command, deferred) {
    var treeJSON = JSON.stringify(this.mainVis.gitEngine.exportTree(), null, 2);

    var showJSON = new MultiView({
      childViews: [{
        type: 'MarkdownPresenter',
        options: {
          previewText: intl.str('share-tree'),
          fillerText: treeJSON,
          noConfirmCancel: true
        }
      }]
    });
    showJSON.getPromise()
    .then(function() {
      command.finishWith(deferred);
    })
    .done();
  },

  clear: function(command, deferred) {
    Main.getEvents().trigger('clearOldCommands');
    if (command && deferred) {
      command.finishWith(deferred);
    }
  },

  mobileAlert: function(command, deferred) {
    alert(intl.str('mobile-alert'));
    command.finishWith(deferred);
  },

  delay: function(command, deferred) {
    var amount = parseInt(command.get('regexResults')[1], 10);
    setTimeout(function() {
      command.finishWith(deferred);
    }, amount);
  },

  reset: function(command, deferred) {
    this.mainVis.reset();
    this.initUndoStack();

    setTimeout(function() {
      command.finishWith(deferred);
    }, this.mainVis.getAnimationTime());
  },

  helpDialog: function(command, deferred) {
    var helpDialog = new MultiView({
      childViews: intl.getDialog(require('../dialogs/sandbox'))
    });
    helpDialog.getPromise().then(_.bind(function() {
      // the view has been closed, lets go ahead and resolve our command
      command.finishWith(deferred);
    }, this))
    .done();
  }
});

exports.Sandbox = Sandbox;


var _ = require('underscore');
var Q = require('q');
// horrible hack to get localStorage Backbone plugin
var Backbone = (!require('../util').isBrowser()) ? require('backbone') : window.Backbone;

var util = require('../util');
var intl = require('../intl');
var log = require('../log');
var KeyboardListener = require('../util/keyboard').KeyboardListener;
var Main = require('../app');

var ModalTerminal = require('../views').ModalTerminal;
var ContainedBase = require('../views').ContainedBase;
var BaseView = require('../views').BaseView;

var LEVELS = require('../../levels');

var LevelDropdownView = ContainedBase.extend({
  tagName: 'div',
  className: 'levelDropdownView box vertical',
  template: _.template($('#level-dropdown-view').html()),
  events: {
    'click div.levelDropdownTab': 'onTabClick'
  },

  initialize: function(options) {
    options = options || {};
    var queryParams = util.parseQueryString(
      window.location.href
    );
    this.JSON = {
      selectedTab: queryParams.defaultTab || 'main',
      tabs: [{
        id: 'main',
        name: intl.todo('Main')
      }, {
        id: 'remote',
        name: intl.todo('Remote')
      }]
    };

    this.navEvents = _.clone(Backbone.Events);
    this.navEvents.on('clickedID', _.debounce(
      _.bind(this.loadLevelID, this),
      300,
      true
    ));
    this.navEvents.on('negative', this.negative, this);
    this.navEvents.on('positive', this.positive, this);
    this.navEvents.on('left', this.left, this);
    this.navEvents.on('right', this.right, this);
    this.navEvents.on('up', this.up, this);
    this.navEvents.on('down', this.down, this);

    this.keyboardListener = new KeyboardListener({
      events: this.navEvents,
      aliasMap: {
        esc: 'negative',
        enter: 'positive'
      },
      wait: true
    });

    this.sequences = Main.getLevelArbiter().getSequences();
    this.sequenceToLevels = Main.getLevelArbiter().getSequenceToLevels();

    this.container = new ModalTerminal({
      title: intl.str('select-a-level')
    });

    this.render();

    Main.getEvents().on('resetMapSolved', this.render, this);
    Main.getEvents().on('localeChanged', this.render, this);

    if (!options.wait) {
      this.show();
    }
  },

  render: function() {
    LevelDropdownView.__super__.render.apply(this, arguments);
    this.buildSequences();
  },

  onTabClick: function(ev) {
    var srcElement = ev.target || ev.srcElement;
    var id = $(srcElement).attr('data-id');
    if (id === this.JSON.selectedTab) {
      return;
    }
    this.updateTabTo(id);
  },

  updateTabTo: function(id) {
    this.JSON.selectedTab = id;
    this.render();
    if (this.selectedID) {
      this.selectedSequence = this.getSequencesOnTab()[0];
      this.selectedIndex = 0;
      this.updateSelectedIcon();
    }
  },

  positive: function() {
    if (!this.selectedID) {
      return;
    }
    this.loadLevelID(this.selectedID);
  },

  left: function() {
    if (this.turnOnKeyboardSelection()) {
      return;
    }
    this.leftOrRight(-1);
  },

  updateSelectedIcon: function() {
    this.selectedID = this.getSelectedID();
    this.selectIconByID(this.selectedID);
  },

  leftOrRight: function(delta) {
    this.deselectIconByID(this.selectedID);
    var index = this.selectedIndex + delta;

    var sequence = this.getCurrentSequence();
    var tabs = this.JSON.tabs;
    // switch tabs now if needed / possible
    if (index >= sequence.length &&
        this.getTabIndex() + 1 < tabs.length) {
      this.switchToTabIndex(this.getTabIndex() + 1);
      this.selectedIndex = 0;
    } else if (index < 0 &&
               this.getTabIndex() - 1 >= 0) {
      this.switchToTabIndex(this.getTabIndex() - 1);
      this.selectedIndex = 0;
    } else {
      this.selectedIndex = this.wrapIndex(
        this.selectedIndex + delta, this.getCurrentSequence()
      );
    }
    this.updateSelectedIcon();
  },

  right: function() {
    if (this.turnOnKeyboardSelection()) {
      return;
    }
    this.leftOrRight(1);
  },

  up: function() {
    if (this.turnOnKeyboardSelection()) {
      return;
    }
    this.selectedSequence = this.getPreviousSequence();
    this.downOrUp();
  },

  down: function() {
    if (this.turnOnKeyboardSelection()) {
      return;
    }
    this.selectedSequence = this.getNextSequence();
    this.downOrUp();
  },

  downOrUp: function() {
    this.selectedIndex = this.boundIndex(this.selectedIndex, this.getCurrentSequence());
    this.deselectIconByID(this.selectedID);
    this.updateSelectedIcon();
  },

  turnOnKeyboardSelection: function() {
    if (!this.selectedID) {
      this.selectFirst();
      return true;
    }
    return false;
  },

  turnOffKeyboardSelection: function() {
    if (!this.selectedID) { return; }
    this.deselectIconByID(this.selectedID);
    this.selectedID = undefined;
    this.selectedIndex = undefined;
    this.selectedSequence = undefined;
  },

  getTabIndex: function() {
    var ids = _.map(this.JSON.tabs, function(tab) {
      return tab.id;
    });
    return ids.indexOf(this.JSON.selectedTab);
  },

  switchToTabIndex: function(index) {
    var tabID = this.JSON.tabs[index].id;
    this.updateTabTo(tabID);
  },

  wrapIndex: function(index, arr) {
    index = (index >= arr.length) ? 0 : index;
    index = (index < 0) ? arr.length - 1 : index;
    return index;
  },

  boundIndex: function(index, arr) {
    index = (index >= arr.length) ? arr.length - 1 : index;
    index = (index < 0) ? 0 : index;
    return index;
  },

  getSequencesOnTab: function() {
    return _.filter(this.sequences, function(sequenceName) {
      var tab = LEVELS.getTabForSequence(sequenceName);
      return tab === this.JSON.selectedTab;
    }, this);
  },

  getNextSequence: function() {
    var current = this.getSequenceIndex(this.selectedSequence);
    var desired = this.wrapIndex(current + 1, this.getSequencesOnTab());
    return this.getSequencesOnTab()[desired];
  },

  getPreviousSequence: function() {
    var current = this.getSequenceIndex(this.selectedSequence);
    var desired = this.wrapIndex(current - 1, this.getSequencesOnTab());
    return this.getSequencesOnTab()[desired];
  },

  getSequenceIndex: function(name) {
    var index = this.getSequencesOnTab().indexOf(name);
    if (index < 0) { throw new Error('didnt find'); }
    return index;
  },

  getIndexForID: function(id) {
    return Main.getLevelArbiter().getLevel(id).index;
  },

  selectFirst: function() {
    var firstID = this.sequenceToLevels[this.getSequencesOnTab()[0]][0].id;
    this.selectIconByID(firstID);
    this.selectedIndex = 0;
    this.selectedSequence = this.getSequencesOnTab()[0];
  },

  getCurrentSequence: function() {
    return this.sequenceToLevels[this.selectedSequence];
  },

  getSelectedID: function() {
    return this.sequenceToLevels[this.selectedSequence][this.selectedIndex].id;
  },

  selectIconByID: function(id) {
    this.toggleIconSelect(id, true);
  },

  deselectIconByID: function(id) {
    this.toggleIconSelect(id, false);
  },

  toggleIconSelect: function(id, value) {
    this.selectedID = id;
    var selector = '#levelIcon-' + id;
    $(selector).toggleClass('selected', value);

    // also go find the series and update the about
    _.each(this.seriesViews, function(view) {
      if (view.levelIDs.indexOf(id) === -1) {
        view.resetAbout();
        return;
      }
      view.updateAboutForLevelID(id);
    }, this);
  },

  negative: function() {
    this.hide();
  },

  testOption: function(str) {
    return this.currentCommand && new RegExp('--' + str).test(this.currentCommand.get('rawStr'));
  },

  show: function(deferred, command) {
    this.currentCommand = command;
    // doing the update on show will allow us to fade which will be nice
    this.updateSolvedStatus();

    this.showDeferred = deferred;
    this.keyboardListener.listen();
    LevelDropdownView.__super__.show.apply(this);
  },

  hide: function() {
    if (this.showDeferred) {
      this.showDeferred.resolve();
    }
    this.showDeferred = undefined;
    this.keyboardListener.mute();
    this.turnOffKeyboardSelection();

    LevelDropdownView.__super__.hide.apply(this);
  },

  loadLevelID: function(id) {
    if (!this.testOption('noOutput')) {
      Main.getEventBaton().trigger(
        'commandSubmitted',
        'level ' + id
      );
      var level = Main.getLevelArbiter().getLevel(id);
      var name = level.name.en_US;
      log.levelSelected(name);
    }
    this.hide();
  },

  updateSolvedStatus: function() {
    _.each(this.seriesViews, function(view) {
      view.updateSolvedStatus();
    }, this);
  },

  buildSequences: function() {
    this.seriesViews = [];
    _.each(this.getSequencesOnTab(), function(sequenceName) {
      this.seriesViews.push(new SeriesView({
        destination: this.$el,
        name: sequenceName,
        navEvents: this.navEvents
      }));
    }, this);
  }
});

var SeriesView = BaseView.extend({
  tagName: 'div',
  className: 'seriesView box flex1 vertical',
  template: _.template($('#series-view').html()),
  events: {
    'click div.levelIcon': 'click',
    'mouseenter div.levelIcon': 'enterIcon',
    'mouseleave div.levelIcon': 'leaveIcon'
  },

  initialize: function(options) {
    this.name = options.name || 'intro';
    this.navEvents = options.navEvents;
    this.info = Main.getLevelArbiter().getSequenceInfo(this.name);
    this.levels = Main.getLevelArbiter().getLevelsInSequence(this.name);

    this.levelIDs = [];
    _.each(this.levels, function(level) {
      this.levelIDs.push(level.id);
    }, this);

    this.destination = options.destination;
    // use a non-breaking space to prevent the level from bouncing around
    // from missing strings
    this.JSON = {
      displayName: intl.getIntlKey(this.info, 'displayName'),
      about: intl.getIntlKey(this.info, 'about') || " ",
      ids: this.levelIDs
    };

    this.render();
    this.updateSolvedStatus();
  },

  updateSolvedStatus: function() {
    // this is a bit hacky, it really should be some nice model
    // property changing but it's the 11th hour...
    var toLoop = this.$('div.levelIcon').each(function(index, el) {
      var id = $(el).attr('data-id');
      $(el).toggleClass('solved', Main.getLevelArbiter().isLevelSolved(id));
    });
  },

  getEventID: function(ev) {
    var element = ev.target;
    return $(element).attr('data-id');
  },

  resetAbout: function() {
    this.$('p.about').text(intl.getIntlKey(this.info, 'about'))
      .css('font-style', 'inherit');
  },

  setAbout: function(content) {
    this.$('p.about').text(content).css('font-style', 'italic');
  },

  enterIcon: function(ev) {
    var id = this.getEventID(ev);
    this.updateAboutForLevelID(id);
  },

  updateAboutForLevelID: function(id) {
    var level = Main.getLevelArbiter().getLevel(id);
    this.setAbout(intl.getName(level));
  },

  leaveIcon: function() {
    this.resetAbout();
  },

  click: function(ev) {
    var id = this.getEventID(ev);
    this.navEvents.trigger('clickedID', id);
  }
});

exports.LevelDropdownView = LevelDropdownView;


var _ = require('underscore');
var fs = require('fs');

// Haha, this is so tricky. so we have a template for index.html to stick
// in the hashed JS and style files -- that template also contains
// templates used in the app. in order to avoid evaluating those
// templates, we change the regexes so we can effectively nest templates
_.templateSettings.interpolate = /\{\{(.+?)\}\}/g;
_.templateSettings.escape = /\{\{\{(.*?)\}\}\}/g;
_.templateSettings.evaluate = /\{\{-(.*?)\}\}/g;

// precompile for speed
var indexFile = fs.readFileSync('src/template.index.html').toString();
var indexTemplate = _.template(indexFile);

/*global module:false*/
module.exports = function(grunt) {
  // eventually have sound...?
  grunt.registerTask('compliment', 'Stay motivated!', function() {
    var defaults = ['Awesome!!'];

    var compliments = grunt.config('compliment.compliments') || defaults;
    var index = Math.floor(Math.random() * compliments.length);

    grunt.log.writeln(compliments[index]);
  });

  grunt.registerTask('lintStrings', 'Find if an INTL string doesnt exist', function() {
    var child_process = require('child_process');
    var output = child_process.exec('node src/js/intl/checkStrings', function(err, output) {
      grunt.log.writeln(output);
    });
  });

  grunt.registerTask('buildIndex', 'stick in hashed resources', function() {
    grunt.log.writeln('Building index...');

    // first find the one in here that we want
    var buildFiles = fs.readdirSync('build');

    var hashedMinFile;
    if (buildFiles.length == 2) {
      grunt.log.writeln('Assuming debug mode wanted');
      hashedMinFile = 'bundle.js';
    }
    var jsRegex = /bundle\.min\.\w+\.js/;
    _.each(buildFiles, function(jsFile) {
      if (jsRegex.test(jsFile)) {
        if (hashedMinFile) {
          throw new Error('more than one hashed file: ' + jsFile + hashedMinFile);
        }
        hashedMinFile = jsFile;
      }
    });
    if (!hashedMinFile) { throw new Error('no hashed min file found!'); }

    grunt.log.writeln('Found hashed js file: ' + hashedMinFile);

    var styleRegex = /main\.\w+\.css/;
    var hashedStyleFile;
    _.each(buildFiles, function(styleFile) {
      if (styleRegex.test(styleFile)) {
        if (hashedStyleFile) {
          throw new Error('more than one hashed style: ' + styleFile + hashedStyleFile);
        }
        hashedStyleFile = styleFile;
      }
    });
    if (!hashedStyleFile) { throw new Error('no style found'); }

    grunt.log.writeln('Found hashed style file: ' + hashedStyleFile);

    // output these filenames to our index template
    var outputIndex = indexTemplate({
      jsFile: hashedMinFile,
      styleFile: hashedStyleFile
    });
    fs.writeFileSync('index.html', outputIndex);
  });

  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    jshint: {
      all: [
        'Gruntfile.js',
        'spec/*.js',
        'src/js/**/*.js',
        'src/js/**/**/*.js',
        'src/levels/**/*.js',
      ],
      options: {
        curly: true,
        // sometimes triple equality is just redundant and unnecessary
        eqeqeq: false,
        // i know my regular expressions
        regexp: false,
        // i think it's super weird to not use new on a constructor
        nonew: false,
        // these latedefs are just annoying -- no pollution of global scope
        latedef: false,
        // use this in mocks
        forin: false,
        ///////////////////////////////
        // All others are true
        //////////////////////////////
        immed: true,
        newcap: true,
        noarg: true,
        bitwise: true,
        sub: true,
        undef: true,
        unused: false,
        trailing: true,
        devel: true,
        jquery: true,
        nonstandard: true,
        boss: true,
        eqnull: true,
        browser: true,
        debug: true,
        globals: {
          Raphael: true,
          require: true,
          console: true,
          describe: true,
          expect: true,
          it: true,
          runs: true,
          waitsFor: true,
          exports: true,
          module: true,
          prompt: true,
          process: true
        }
      },
    },
    compliment: {
      compliments: [
        "Wow peter great work!",
        "Such a professional dev environment",
        "Can't stop the TRAIN",
        "git raging"
      ]
    },
    hash: {
      js: {
        src: 'build/bundle.min.js',
        dest: 'build/'
      },
      css: {
        src: 'src/style/main.css',
        dest: 'build/'
      }
    },
    watch: {
      files: '<config:lint.files>',
      tasks: 'watching'
    },
    uglify: {
      build: {
        src: ['build/bundle.js'],
        dest: 'build/bundle.min.js'
      }
    },
    clean: ['build/*'],
    shell: {
      gitAdd: {
        command: 'git add build/'
      }
    },
    jasmine_node: {
      specNameMatcher: 'spec',
      projectRoot: '.',
      forceExit: true,
      verbose: true,
      requirejs: false
    },
    browserify: {
      dist: {
        files: {
          'build/bundle.js': ['src/**/*.js', 'src/js/**/*.js'],
        },
      }
    }
  });

  // all my npm helpers
  grunt.loadNpmTasks('grunt-contrib-jshint');
  grunt.loadNpmTasks('grunt-browserify');
  grunt.loadNpmTasks('grunt-hash');
  grunt.loadNpmTasks('grunt-contrib-clean');
  grunt.loadNpmTasks('grunt-shell-spawn');
  grunt.loadNpmTasks('grunt-jasmine-node');
  grunt.loadNpmTasks('grunt-contrib-uglify');

  grunt.registerTask('build',
    ['clean', 'browserify', 'uglify', 'hash', 'buildIndex', 'shell', 'jasmine_node', 'jshint', 'lintStrings', 'compliment']
  );
  grunt.registerTask('lint', ['jshint', 'compliment']);
  grunt.registerTask('fastBuild', ['clean', 'browserify', 'hash', 'buildIndex', 'jshint']);
  grunt.registerTask('watching', ['fastBuild', 'jasmine_node', 'jshint', 'lintStrings']);

  grunt.registerTask('default', ['build']);
  grunt.registerTask('test', ['jasmine_node']);
};


TreeCompare.convertTreeSafe = function(tree) {

TreeCompare.lowercaseTree = function(tree) {

var HeadlessGit = require('../src/js/git/headless').HeadlessGit;
var TreeCompare = require('../src/js/graph/treeCompare.js');

var loadTree = function(json) {
  return JSON.parse(unescape(json));
};

var compareLevelTree = function(headless, levelBlob) {
  var actualTree = headless.gitEngine.exportTree();
  return TreeCompare.dispatchFromLevel(levelBlob, actualTree);
};

var compareAnswer = function(headless, expectedJSON) {
  var expectedTree = loadTree(expectedJSON);
  var actualTree = headless.gitEngine.exportTree();

  return TreeCompare.compareTrees(expectedTree, actualTree);
};

var getHeadlessSummary = function(headless) {
  var tree = headless.gitEngine.exportTree();
  TreeCompare.reduceTreeFields([tree]);
  return tree;
};

var expectLevelAsync = function(headless, levelBlob) {
  var command = levelBlob.solutionCommand;
  if (command.indexOf('git rebase -i') !== -1) {
    // dont do interactive rebase levels
    return;
  }

  var start;
  runs(function() {
    start = Date.now();
    headless.sendCommand(command);
  });
  waitsFor(function() {
    var diff = (Date.now() - start);
    if (diff > TIME - 10) {
      console.log('not going to match', command);
    }
    var result = compareLevelTree(headless, levelBlob);
    if (result) {
      console.log('solved level ' + levelBlob.name.en_US);
    }
    return result;
  }, 'trees should be equal', TIME);
};

var expectTreeAsync = function(command, expectedJSON, startJSON) {
  var headless = new HeadlessGit();
  var start = Date.now();
  var haveReported = false;

  if (startJSON) {
    headless.gitEngine.loadTreeFromString(startJSON);
  }

  runs(function() {
    headless.sendCommand(command);
  });
  waitsFor(function() {
    var diff = (Date.now() - start);
    if (diff > TIME - 40 && !haveReported) {
      haveReported = true;
      var expected = loadTree(expectedJSON);
      console.log('not going to match', command);
      console.log('expected\n>>>>>>>>\n', expected);
      console.log('\n<<<<<<<<<<<\nactual', getHeadlessSummary(headless));
      console.log('\n<<<<ORIGIN>>>>>\n');
      if (expected.originTree) {
        console.log('expected origin tree:');
        console.log(expected.originTree);
        console.log('\n=========\n');
        console.log('actual origin tree');
        console.log(getHeadlessSummary(headless).originTree);
      }
      console.log(expectedJSON);
      console.log(JSON.stringify(getHeadlessSummary(headless)));
    }
    return compareAnswer(headless, expectedJSON);
  }, 'trees should be equal', 100);
};

var expectLevelSolved = function(levelBlob) {
  var headless = new HeadlessGit();
  if (levelBlob.startTree) {
    headless.gitEngine.loadTreeFromString(levelBlob.startTree);
  }
  expectLevelAsync(headless, levelBlob);
};

var TIME = 150;
// useful for throwing garbage and then expecting one commit
var ONE_COMMIT_TREE = '{"branches":{"master":{"target":"C2","id":"master"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"}},"HEAD":{"target":"master","id":"HEAD"}}';

module.exports = {
  loadTree: loadTree,
  compareAnswer: compareAnswer,
  TIME: TIME,
  expectTreeAsync: expectTreeAsync,
  expectLevelSolved: expectLevelSolved,
  ONE_COMMIT_TREE: ONE_COMMIT_TREE
};


var _ = require('underscore');
// horrible hack to get localStorage Backbone plugin
var Backbone = (!require('../util').isBrowser()) ? Backbone = require('backbone') : Backbone = window.Backbone;

var CommandEntryCollection = require('../models/collections').CommandEntryCollection;
var Main = require('../app');
var Command = require('../models/commandModel').Command;
var CommandEntry = require('../models/commandModel').CommandEntry;

var Errors = require('../util/errors');
var Warning = Errors.Warning;

var util = require('../util');
var log = require('../log');
var keyboard = require('../util/keyboard');

var CommandPromptView = Backbone.View.extend({
  initialize: function(options) {
    Main.getEvents().on('commandSubmittedPassive', this.addToCommandHistory, this);

    // uses local storage
    this.commands = new CommandEntryCollection();
    this.commands.fetch({
      success: _.bind(function() {
        // reverse the commands. this is ugly but needs to be done...
        var commands = [];
        this.commands.each(function(c) {
          commands.push(c);
        });

        commands.reverse();
        this.commands.reset();

        _.each(commands, function(c) {
          this.commands.add(c);
        }, this);
      }, this)
    });

    this.index = -1;
    this.commandParagraph = this.$('#prompt p.command')[0];
    this.commandCursor = this.$('#prompt span.cursor')[0];
    this.focus();

    Main.getEvents().on('rollupCommands', this.rollupCommands, this);

    Main.getEventBaton().stealBaton('keydown', this.onKeyDown, this);
    Main.getEventBaton().stealBaton('keyup', this.onKeyUp, this);
  },

  events: {
    'blur #commandTextField': 'hideCursor',
    'focus #commandTextField': 'showCursor'
  },

  blur: function() {
    this.hideCursor();
  },

  focus: function() {
    this.$('#commandTextField').focus();
    this.showCursor();
  },

  hideCursor: function() {
    this.toggleCursor(false);
  },

  showCursor: function() {
    this.toggleCursor(true);
  },

  toggleCursor: function(state) {
    $(this.commandCursor).toggleClass('shown', state);
  },

  onKeyDown: function(e) {
    var el = e.target;
    this.updatePrompt(el);
  },

  onKeyUp: function(e) {
    this.onKeyDown(e);

    // we need to capture some of these events.
    var keyToFuncMap = {
      enter: _.bind(function() {
        this.submit();
      }, this),
      up: _.bind(function() {
        this.commandSelectChange(1);
      }, this),
      down: _.bind(function() {
        this.commandSelectChange(-1);
      }, this)
    };

    var key = keyboard.mapKeycodeToKey(e.which || e.keyCode);
    if (keyToFuncMap[key] !== undefined) {
      e.preventDefault();
      keyToFuncMap[key]();
      this.onKeyDown(e);
    }
  },

  badHtmlEncode: function(text) {
    return text.replace(/&/g,'&')
      .replace(/</g,'<')
      .replace(/</g,'<')
      .replace(/ /g,' ')
      .replace(/\n/g,'');
  },

  updatePrompt: function(el) {
    el = el || {};  // firefox
    // i WEEEPPPPPPpppppppppppp that this reflow takes so long. it adds this
    // super annoying delay to every keystroke... I have tried everything
    // to make this more performant. getting the srcElement from the event,
    // getting the value directly from the dom, etc etc. yet still,
    // there's a very annoying and sightly noticeable command delay.
    // try.github.com also has this, so I'm assuming those engineers gave up as
    // well...
    var text = $('#commandTextField').val();
    var val = this.badHtmlEncode(text);
    this.commandParagraph.innerHTML = val;

    // now mutate the cursor...
    this.cursorUpdate(text.length, el.selectionStart, el.selectionEnd);
    // and scroll down due to some weird bug
    Main.getEvents().trigger('commandScrollDown');
  },

  cursorUpdate: function(commandLength, selectionStart, selectionEnd) {
    if (selectionStart === undefined || selectionEnd === undefined) {
      selectionStart = Math.max(commandLength - 1, 0);
      selectionEnd = commandLength;
    }

    // 10px for monospaced font at "1" zoom
    var zoom = require('../util/zoomLevel').detectZoom();
    var widthPerChar = 10 * zoom;
    var heightPerRow = 22 * zoom;

    var widthOfParagraph = $(this.commandParagraph).width();
    var numCharsPerLine = widthOfParagraph / widthPerChar;

    var numCharsSelected = Math.min(Math.max(1, selectionEnd - selectionStart), numCharsPerLine);
    var widthOfSelection = String(numCharsSelected * widthPerChar) + 'px';

    // now for positioning
    var leftOffset = String(widthPerChar * (selectionStart % numCharsPerLine)) + 'px';
    var topOffset = String(Math.floor(selectionStart / numCharsPerLine) * heightPerRow) + 'px';

    // one reflow? :D
    $(this.commandCursor).css({
      width: widthOfSelection,
      left: leftOffset,
      top: topOffset
    });
  },

  commandSelectChange: function(delta) {
    this.index += delta;

    // if we are over / under, display blank line. yes this eliminates your
    // partially edited command, but i doubt that is much in this demo
    if (this.index >= this.commands.length || this.index < 0) {
      this.clear();
      this.index = -1;
      return;
    }

    // yay! we actually can display something
    var commandEntry = this.commands.toArray()[this.index].get('text');
    this.setTextField(commandEntry);
  },

  clearLocalStorage: function() {
    this.commands.each(function(c) {
      Backbone.sync('delete', c, function() { });
    }, this);
  },

  setTextField: function(value) {
    this.$('#commandTextField').val(value);
  },

  clear: function() {
    this.setTextField('');
  },

  submit: function() {
    var value = this.$('#commandTextField').val().replace('\n', '');
    this.clear();

    this.submitCommand(value);
    this.index = -1;
  },

  rollupCommands: function(numBack) {
    var which = this.commands.toArray().slice(1, Number(numBack) + 1);
    which.reverse();

    var str = '';
    _.each(which, function(commandEntry) {
      str += commandEntry.get('text') + ';';
    }, this);

    var rolled = new CommandEntry({text: str});
    this.commands.unshift(rolled);
    Backbone.sync('create', rolled, function() { });
  },

  addToCommandHistory: function(value) {
    // we should add the command to our local storage history
    // if it's not a blank line and this is a new command...
    // or if we edited the command in place in history
    var shouldAdd = (value.length && this.index === -1) ||
      ((value.length && this.index !== -1 &&
      this.commands.toArray()[this.index].get('text') !== value));

    if (!shouldAdd) {
      return;
    }

    var commandEntry = new CommandEntry({text: value});
    this.commands.unshift(commandEntry);

    // store to local storage
    Backbone.sync('create', commandEntry, function() { });

    // if our length is too egregious, reset
    if (this.commands.length > 100) {
      this.clearLocalStorage();
    }
    log.commandEntered(value);
  },

  submitCommand: function(value) {
    Main.getEventBaton().trigger('commandSubmitted', value);
  }
});

// This is the view for all commands -- it will represent
// their status (inqueue, processing, finished, error),
// their value ("git commit --amend"),
// and the result (either errors or warnings or whatever)
var CommandView = Backbone.View.extend({
  tagName: 'div',
  model: Command,
  template: _.template($('#command-template').html()),

  events: {
    'click': 'clicked'
  },

  clicked: function(e) {
  },

  initialize: function() {
    this.model.bind('change', this.wasChanged, this);
    this.model.bind('destroy', this.remove, this);
  },

  wasChanged: function(model, changeEvent) {
    // for changes that are just comestic, we actually only want to toggle classes
    // with jquery rather than brutally delete a html. doing so allows us
    // to nicely fade things
    var changes = changeEvent.changes;
    var changeKeys = _.keys(changes);
    if (_.difference(changeKeys, ['status']).length === 0) {
      this.updateStatus();
    } else {
      this.render();
    }
  },

  updateStatus: function() {
    var statuses = ['inqueue', 'processing', 'finished'];
    var toggleMap = {};
    _.each(statuses, function(status) {
      toggleMap[status] = false;
    });
    toggleMap[this.model.get('status')] = true;

    var query = this.$('p.commandLine');

    _.each(toggleMap, function(value, key) {
      query.toggleClass(key, value);
    });
  },

  render: function() {
    var json = _.extend(
      {
        resultType: '',
        result: '',
        formattedWarnings: this.model.getFormattedWarnings()
      },
      this.model.toJSON()
    );

    this.$el.html(this.template(json));
    return this;
  },

  remove: function() {
    $(this.el).hide();
  }
});

var CommandLineHistoryView = Backbone.View.extend({
  initialize: function(options) {
    this.collection = options.collection;

    this.collection.on('add', this.addOne, this);
    this.collection.on('reset', this.addAll, this);
    this.collection.on('all', this.render, this);

    this.collection.on('change', this.scrollDown, this);
    Main.getEvents().on('commandScrollDown', this.scrollDown, this);
    Main.getEvents().on('clearOldCommands', this.clearOldCommands, this);
  },

  addWarning: function(msg) {
    var err = new Warning({
      msg: msg
    });

    var command = new Command({
      error: err,
      rawStr: 'Warning:'
    });

    this.collection.add(command);
  },

  clearOldCommands: function() {
    // go through and get rid of every command that is "processed" or done
    var toDestroy = [];

    this.collection.each(function(command) {
      if (command.get('status') !== 'inqueue' &&
          command.get('status') !== 'processing') {
        toDestroy.push(command);
      }
    }, this);

    _.each(toDestroy, function(command) {
      command.destroy();
    }, this);
    this.scrollDown();
  },

  scrollDown: function() {
    // if commandDisplay is ever bigger than #terminal, we need to
    // add overflow-y to terminal and scroll down
    var cD = $('#commandDisplay')[0];
    var t = $('#terminal')[0];

    // firefox hack
    var shouldScroll = (cD.clientHeight > t.clientHeight) ||
      ($(window).height() < cD.clientHeight);
    $(t).toggleClass('scrolling', shouldScroll);
    if (shouldScroll) {
      t.scrollTop = t.scrollHeight;
    }
  },

  addOne: function(command) {
    var view = new CommandView({
      model: command
    });
    this.$('#commandDisplay').append(view.render().el);
    this.scrollDown();
  },

  addAll: function() {
    this.collection.each(this.addOne);
  }
});

exports.CommandPromptView = CommandPromptView;
exports.CommandLineHistoryView = CommandLineHistoryView;


exports.dialog = {
  'en_US': [{
    type: 'ModalAlert',
    options: {
      markdowns: [
        '## Are you sure you want to see the solution?',
        '',
        'I believe in you! You can do it'
      ]
    }
  }],
  'de_DE': [{
    type: 'ModalAlert',
    options: {
      markdowns: [
        '## Bist du sicher, dass du die Auflösung sehen willst?',
        '',
        'Ich glaube an dich! Du schaffst das!'
      ]
    }
  }],
  'zh_CN': [{
    type: 'ModalAlert',
    options: {
      markdowns: [
        '## 确定要看答案吗？',
        '',
        '哥相信你！你可以的'
      ]
    }
  }],
  'fr_FR': [{
    type: 'ModalAlert',
    options: {
      markdowns: [
        '## Êtes-vous sûr de vouloir voir la solution ?',
        '',
        'Je crois en vous ! Vous pouvez le faire'
      ]
    }
  }]
};


var _ = require('underscore');
var Backbone = require('backbone');

// Each level is part of a "sequence;" levels within
// a sequence proceed in order.
var levelSequences = require('../../levels').levelSequences;
var sequenceInfo = require('../../levels').sequenceInfo;

var Main = require('../app');

function LevelArbiter() {
  this.levelMap = {};
  this.levelSequences = levelSequences;
  this.sequences = [];
  this.init();

  var solvedMap;
  try {
    solvedMap = JSON.parse(localStorage.getItem('solvedMap') || '{}');
  } catch (e) {
    console.warn('local storage failed', e);
    // throw e;
  }
  this.solvedMap = solvedMap || {};

  Main.getEvents().on('levelSolved', this.levelSolved, this);
}

LevelArbiter.prototype.init = function() {
  var previousLevelID;
  _.each(this.levelSequences, function(levels, levelSequenceName) {
    this.sequences.push(levelSequenceName);
    if (!levels || !levels.length) {
      throw new Error('no empty sequences allowed');
    }

    // for this particular sequence...
    _.each(levels, function(level, index) {
      this.validateLevel(level);

      var id = levelSequenceName + String(index + 1);
      var compiledLevel = _.extend(
        {},
        level,
        {
          index: index,
          id: id,
          sequenceName: levelSequenceName
        }
      );

      // update our internal data
      this.levelMap[id] = compiledLevel;
      this.levelSequences[levelSequenceName][index] = compiledLevel;
    }, this);
  }, this);
};

LevelArbiter.prototype.isLevelSolved = function(id) {
  if (!this.levelMap[id]) {
    throw new Error('that level doesnt exist!');
  }
  return Boolean(this.solvedMap[id]);
};

LevelArbiter.prototype.levelSolved = function(id) {
  // called without an id when we reset solved status
  if (!id) { return; }

  this.solvedMap[id] = true;
  this.syncToStorage();
};

LevelArbiter.prototype.resetSolvedMap = function() {
  this.solvedMap = {};
  this.syncToStorage();
  Main.getEvents().trigger('levelSolved');
};

LevelArbiter.prototype.syncToStorage = function() {
  try {
    localStorage.setItem('solvedMap', JSON.stringify(this.solvedMap));
  } catch (e) {
    console.warn('local storage fialed on set', e);
  }
};

LevelArbiter.prototype.validateLevel = function(level) {
  level = level || {};
  var requiredFields = [
    'name',
    'goalTreeString',
    //'description',
    'solutionCommand'
  ];

  var optionalFields = [
    'hint',
    'disabledMap',
    'startTree'
  ];

  _.each(requiredFields, function(field) {
    if (level[field] === undefined) {
      console.log(level);
      throw new Error('I need this field for a level: ' + field);
    }
  });
};

LevelArbiter.prototype.getSequenceToLevels = function() {
  return this.levelSequences;
};

LevelArbiter.prototype.getSequences = function() {
  return _.keys(this.levelSequences);
};

LevelArbiter.prototype.getLevelsInSequence = function(sequenceName) {
  if (!this.levelSequences[sequenceName]) {
    throw new Error('that sequecne name ' + sequenceName + 'does not exist');
  }
  return this.levelSequences[sequenceName];
};

LevelArbiter.prototype.getSequenceInfo = function(sequenceName) {
  return sequenceInfo[sequenceName];
};

LevelArbiter.prototype.getLevel = function(id) {
  return this.levelMap[id];
};

LevelArbiter.prototype.getNextLevel = function(id) {
  if (!this.levelMap[id]) {
    console.warn('that level doesnt exist!!!');
    return null;
  }

  // meh, this method could be better. It's a tradeoff between
  // having the sequence structure be really simple JSON
  // and having no connectivity information between levels, which means
  // you have to build that up yourself on every query
  var level = this.levelMap[id];
  var sequenceName = level.sequenceName;
  var sequence = this.levelSequences[sequenceName];

  var nextIndex = level.index + 1;
  if (nextIndex < sequence.length) {
    return sequence[nextIndex];
  }

  var nextSequenceIndex = this.sequences.indexOf(sequenceName) + 1;
  if (nextSequenceIndex < this.sequences.length) {
    var nextSequenceName = this.sequences[nextSequenceIndex];
    return this.levelSequences[nextSequenceName][0];
  }

  // they finished the last level!
  return null;
};

exports.LevelArbiter = LevelArbiter;


keep replace replace keep keep keep keep

// ok we are almost done -- but we need to case insensitive

// certain fields. so go ahead and do that.