var _ = require('underscore');
var Q = require('q');

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

var React = require('react');
var Errors = require('../util/errors');
var Sandbox = require('../sandbox/').Sandbox;
var GlobalStateActions = require('../actions/GlobalStateActions');
var LevelActions = require('../actions/LevelActions');
var LevelStore = require('../stores/LevelStore');
var Visualization = require('../visuals/visualization').Visualization;
var DisabledMap = require('../level/disabledMap').DisabledMap;
var GitShim = require('../git/gitShim').GitShim;
var Commands = require('../commands');

var MultiView = require('../views/multiView').MultiView;
var CanvasTerminalHolder = require('../views').CanvasTerminalHolder;
var ConfirmCancelTerminal = require('../views').ConfirmCancelTerminal;
var NextLevelConfirm = require('../views').NextLevelConfirm;
var LevelToolbarView = require('../react_views/LevelToolbarView.jsx');

var TreeCompare = require('../graph/treeCompare');

var regexMap = {
  'help level': /^help level$/,
  'start dialog': /^start dialog$/,
  'show goal': /^(show goal|goal|help goal)$/,
  'hide goal': /^hide goal$/,
  'show solution': /^show solution($|\s)/,
  'objective': /^(objective|assignment)$/
};

var parse = util.genParseCommand(regexMap, 'processLevelCommand');

var Level = Sandbox.extend({
  initialize: function(options) {
    options = options || {};
    options.level = options.level || {};

    this.level = options.level;

    this.gitCommandsIssued = [];
    this.solved = false;
    this.wasResetAfterSolved = false;

    this.initGoalData(options);
    this.initName(options);
    this.on('minimizeCanvas', this.minimizeGoal);
    this.on('resizeCanvas', this.resizeGoal);
    this.isGoalExpanded = false;

    Level.__super__.initialize.apply(this, [options]);
    this.startOffCommand();

    this.handleOpen(options.deferred);
  },

  getIsGoalExpanded: function() {
    return this.isGoalExpanded;
  },

  handleOpen: function(deferred) {
    deferred = deferred || Q.defer();

    // if there is a multiview in the beginning, open that
    // and let it resolve our deferred
    if (this.level.startDialog && !this.testOption('noIntroDialog')) {
      new MultiView(_.extend(
        {},
        intl.getStartDialog(this.level),
        { deferred: deferred }
      ));
      return;
    }

    // otherwise, resolve after a 700 second delay to allow
    // for us to animate easily
    setTimeout(function() {
      deferred.resolve();
    }, this.getAnimationTime() * 1.2);
  },

  objectiveDialog: function(command, deferred, levelObj) {
    levelObj = (levelObj === undefined) ? this.level : levelObj;

    if (!levelObj || !levelObj.startDialog) {
      command.set('error', new Errors.GitError({
        msg: intl.str('no-start-dialog')
      }));
      deferred.resolve();
      return;
    }

    var dialog = $.extend({}, intl.getStartDialog(levelObj));
    // grab the last slide only
    dialog.childViews = dialog.childViews.slice(-1);
    new MultiView(_.extend(
      dialog,
      { deferred: deferred }
    ));

    // when its closed we are done
    deferred.promise.then(function() {
      command.set('status', 'finished');
    });
  },

  startDialog: function(command, deferred) {
    if (!this.level.startDialog) {
      command.set('error', new Errors.GitError({
        msg: intl.str('no-start-dialog')
      }));
      deferred.resolve();
      return;
    }

    this.handleOpen(deferred);
    deferred.promise.then(function() {
      command.set('status', 'finished');
    });
  },

  getEnglishName: function() {
    return this.level.name.en_US;
  },

  initName: function() {
    var name = intl.getName(this.level);
    this.levelToolbar = React.createElement(
      LevelToolbarView,
      {
        name: name,
        onGoalClick: this.toggleGoal.bind(this),
        onObjectiveClick: this.toggleObjective.bind(this),
        parent: this
      }
    );
    React.render(
      this.levelToolbar,
      document.getElementById('levelToolbarMount')
    );
  },

  initGoalData: function(options) {
    if (!this.level.goalTreeString || !this.level.solutionCommand) {
      throw new Error('need goal tree and solution');
    }
  },

  takeControl: function() {
    Main.getEventBaton().stealBaton('processLevelCommand', this.processLevelCommand, this);

    Level.__super__.takeControl.apply(this);
  },

  releaseControl: function() {
    Main.getEventBaton().releaseBaton('processLevelCommand', this.processLevelCommand, this);

    Level.__super__.releaseControl.apply(this);
  },

  startOffCommand: function() {
    var method = this.options.command.get('method');
    if (!this.testOption('noStartCommand') && method !== 'importLevelNow') {
      Main.getEventBaton().trigger(
        'commandSubmitted',
        'hint; delay 2000; show goal'
      );
    }
  },

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

  initGoalVisualization: function() {
    var onlyMaster = TreeCompare.onlyMasterCompared(this.level);
    // first we make the goal visualization holder
    this.goalCanvasHolder = new CanvasTerminalHolder({
      text: (onlyMaster) ? intl.str('goal-only-master') : undefined,
      parent: this
    });

    // then we make a visualization. the "el" here is the element to
    // track for size information. the container is where the canvas will be placed
    this.goalVis = new Visualization({
      el: this.goalCanvasHolder.getCanvasLocation(),
      containerElement: this.goalCanvasHolder.getCanvasLocation(),
      treeString: this.level.goalTreeString,
      noKeyboardInput: true,
      smallCanvas: true,
      isGoalVis: true,
      levelBlob: this.level,
      noClick: true
    });

    // If the goal visualization gets dragged to the right side of the screen, then squeeze the main
    // repo visualization a bit to make room. This way, you could have the goal window hang out on
    // the right side of the screen and still see the repo visualization.
    this.goalVis.customEvents.on('drag', function(event, ui) {
      if (ui.position.left > 0.5 * $(window).width()) {
        if (!$('#goalPlaceholder').is(':visible')) {
          $('#goalPlaceholder').show();
          this.mainVis.myResize();
        }
      } else {
        if ($('#goalPlaceholder').is(':visible')) {
          $('#goalPlaceholder').hide();
          this.mainVis.myResize();
        }
      }
    }.bind(this));

    return this.goalCanvasHolder;
  },

  minimizeGoal: function (position, size) {
    this.isGoalExpanded = false;
    this.trigger('goalToggled');
    this.goalVis.hide();
    this.goalWindowPos = position;
    this.goalWindowSize = size;
    if ($('#goalPlaceholder').is(':visible')) {
      $('#goalPlaceholder').hide();
      this.mainVis.myResize();
    }
  },

  resizeGoal: function () {
    if (!this.goalVis) {
      return;
    }
    this.goalVis.myResize();
  },

  showSolution: function(command, deferred) {
    var toIssue = this.level.solutionCommand;
    var issueFunc = function() {
      this.isShowingSolution = true;
      Main.getEventBaton().trigger(
        'commandSubmitted',
        toIssue
      );
      log.showLevelSolution(this.getEnglishName());
    }.bind(this);

    var commandStr = command.get('rawStr');
    if (!this.testOptionOnString(commandStr, 'noReset')) {
      toIssue = 'reset --forSolution; ' + toIssue;
    }
    if (this.testOptionOnString(commandStr, 'force')) {
      issueFunc();
      command.finishWith(deferred);
      return;
    }

    // allow them for force the solution
    var confirmDefer = Q.defer();
    var dialog = intl.getDialog(require('../dialogs/confirmShowSolution'))[0];
    var confirmView = new ConfirmCancelTerminal({
      markdowns: dialog.options.markdowns,
      deferred: confirmDefer
    });

    confirmDefer.promise
    .then(issueFunc)
    .fail(function() {
      command.setResult("");
    })
    .done(function() {
     // either way we animate, so both options can share this logic
     setTimeout(function() {
        command.finishWith(deferred);
      }, confirmView.getAnimationTime());
    });
  },

  toggleObjective: function() {
    Main.getEventBaton().trigger(
      'commandSubmitted',
      'objective'
    );
  },

  toggleGoal: function () {
    if (this.goalCanvasHolder && this.goalCanvasHolder.inDom) {
      this.hideGoal();
    } else {
      this.showGoal();
    }
  },

  showGoal: function(command, defer) {
    this.isGoalExpanded = true;
    this.trigger('goalToggled');
    this.showSideVis(command, defer, this.goalCanvasHolder, this.initGoalVisualization);
    // show the squeezer again we are to the side
    if ($(this.goalVis.el).offset().left > 0.5 * $(window).width()) {
      $('#goalPlaceholder').show();
      this.mainVis.myResize();
    }
  },

  showSideVis: function(command, defer, canvasHolder, initMethod) {
    var safeFinish = function() {
      if (command) { command.finishWith(defer); }
    };
    if (!canvasHolder || !canvasHolder.inDom) {
      canvasHolder = initMethod.apply(this);
    }

    canvasHolder.restore(this.goalWindowPos, this.goalWindowSize);
    setTimeout(safeFinish, canvasHolder.getAnimationTime());
  },

  hideGoal: function(command, defer) {
    this.isGoalExpanded = false;
    this.trigger('goalToggled');
    this.hideSideVis(command, defer, this.goalCanvasHolder);
  },

  hideSideVis: function(command, defer, canvasHolder, vis) {
    var safeFinish = function() {
      if (command) { command.finishWith(defer); }
    };

    if (canvasHolder && canvasHolder.inDom) {
      canvasHolder.die();
      setTimeout(safeFinish, canvasHolder.getAnimationTime());
    } else {
      safeFinish();
    }
  },

  initParseWaterfall: function(options) {
    Level.__super__.initParseWaterfall.apply(this, [options]);

    // add our specific functionaity
    this.parseWaterfall.addFirst(
      'parseWaterfall',
      parse
    );

    this.parseWaterfall.addFirst(
      'instantWaterfall',
      this.getInstantCommands()
    );

    // if we want to disable certain commands...
    if (options.level.disabledMap) {
      // disable these other commands
      this.parseWaterfall.addFirst(
        'instantWaterfall',
        new DisabledMap({
          disabledMap: options.level.disabledMap
        }).getInstantCommands()
      );
    }
  },

  initGitShim: function(options) {
    // ok we definitely want a shim here
    this.gitShim = new GitShim({
      beforeCB: this.beforeCommandCB.bind(this),
      afterCB: this.afterCommandCB.bind(this),
      afterDeferHandler: this.afterCommandDefer.bind(this)
    });
  },

  undo: function() {
    this.gitCommandsIssued.pop();
    Level.__super__.undo.apply(this, arguments);
  },

  afterCommandCB: function(command) {
    if (command.get('error')) {
      // dont count errors towards our count
      return;
    }

    var matched = false;
    _.each(Commands.commands.getCommandsThatCount(), function(map) {
      _.each(map, function(regex) {
        matched = matched || regex.test(command.get('rawStr'));
      });
    });
    if (matched) {
      this.gitCommandsIssued.push(command.get('rawStr'));
    }
  },

  afterCommandDefer: function(defer, command) {
    if (this.solved) {
      command.addWarning(intl.str('already-solved'));
      defer.resolve();
      return;
    }

    var current = this.mainVis.gitEngine.printTree();
    var solved = TreeCompare.dispatchFromLevel(this.level, current);

    if (!solved) {
      defer.resolve();
      return;
    }

    // woohoo!!! they solved the level, lets animate and such
    this.levelSolved(defer);
  },

  getNumSolutionCommands: function() {
    // strip semicolons in bad places
    var toAnalyze = this.level.solutionCommand.replace(/^;|;$/g, '');
    return toAnalyze.split(';').length;
  },

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

  testOptionOnString: function(str, option) {
    return str && new RegExp('--' + option).test(str);
  },

  levelSolved: function(defer) {
    this.solved = true;
    if (!this.isShowingSolution) {
      LevelActions.setLevelSolved(this.level.id);
      log.levelSolved(this.getEnglishName());
    }

    this.hideGoal();

    var nextLevel = LevelStore.getNextLevel(this.level.id);
    var numCommands = this.gitCommandsIssued.length;
    var best = this.getNumSolutionCommands();

    var skipFinishDialog = this.testOption('noFinishDialog') ||
      this.wasResetAfterSolved;
    var skipFinishAnimation = this.wasResetAfterSolved;

    var finishAnimationChain = null;
    if (skipFinishAnimation) {
      var deferred = Q.defer();
      deferred.resolve();
      finishAnimationChain = deferred.promise;
      Main.getEventBaton().trigger(
        'commandSubmitted',
        'echo "level solved!"'
      );
    } else {
      GlobalStateActions.changeIsAnimating(true);
      finishAnimationChain = this.mainVis.gitVisuals.finishAnimation();
      if (this.mainVis.originVis) {
        finishAnimationChain = finishAnimationChain.then(
          this.mainVis.originVis.gitVisuals.finishAnimation()
        );
      }
    }

    if (!skipFinishDialog) {
      finishAnimationChain = finishAnimationChain.then(function() {
        // we want to ask if they will move onto the next level
        // while giving them their results...
        var nextDialog = new NextLevelConfirm({
          nextLevel: nextLevel,
          numCommands: numCommands,
          best: best
        });

        return nextDialog.getPromise();
      });
    }

    finishAnimationChain
    .then(function() {
      if (!skipFinishDialog && nextLevel) {
        log.choseNextLevel(nextLevel.id);
        Main.getEventBaton().trigger(
          'commandSubmitted',
          'level ' + nextLevel.id
        );
      }
    })
    .fail(function() {
      // nothing to do, we will just close
    })
    .done(function() {
      GlobalStateActions.changeIsAnimating(false);
      defer.resolve();
    });
  },

  die: function() {
    React.unmountComponentAtNode(
      document.getElementById('levelToolbarMount')
    );

    this.hideGoal();
    this.mainVis.die();
    this.releaseControl();

    this.clear();

    delete this.commandCollection;
    delete this.mainVis;
    delete this.goalVis;
    delete this.goalCanvasHolder;
  },

  getInstantCommands: function() {
    var getHint = function() {
      var hint = intl.getHint(this.level);
      if (!hint || !hint.length) {
        return intl.str('no-hint');
      }
      return hint;
    }.bind(this);

    return [
      [/^help$|^\?$/, function() {
        throw new Errors.CommandResult({
          msg: intl.str('help-vague-level')
        });
      }],
      [/^hint$/, function() {
        throw new Errors.CommandResult({
          msg: getHint()
        });
      }]
    ];
  },

  reset: function(command, deferred) {
    this.gitCommandsIssued = [];

    var commandStr = (command) ? command.get('rawStr') : '';
    if (!this.testOptionOnString(commandStr, 'forSolution')) {
      this.isShowingSolution = false;
    }
    if (this.solved) {
      this.wasResetAfterSolved = true;
    }
    this.solved = false;
    Level.__super__.reset.apply(this, arguments);
  },

  buildLevel: function(command, deferred) {
    this.exitLevel();
    setTimeout(function() {
      Main.getSandbox().buildLevel(command, deferred);
    }, this.getAnimationTime() * 1.5);
  },

  importLevel: function(command, deferred) {
    this.exitLevel();
    setTimeout(function() {
      Main.getSandbox().importLevel(command, deferred);
    }, this.getAnimationTime() * 1.5);
  },

  startLevel: function(command, deferred) {
    this.exitLevel();

    setTimeout(function() {
      Main.getSandbox().startLevel(command, deferred);
    }, this.getAnimationTime() * 1.5);
    // wow! that was simple :D
  },

  exitLevel: function(command, deferred) {
    this.die();

    if (!command || !deferred) {
      return;
    }

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

    // we need to fade in the sandbox
    Main.getEventBaton().trigger('levelExited');
  },

  processLevelCommand: function(command, defer) {
    var methodMap = {
      'show goal': this.showGoal,
      'hide goal': this.hideGoal,
      'show solution': this.showSolution,
      'start dialog': this.startDialog,
      'help level': this.startDialog,
      'objective': this.objectiveDialog
    };
    var method = methodMap[command.get('method')];
    if (!method) {
      throw new Error('woah we dont support that method yet', method);
    }

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

exports.Level = Level;
exports.regexMap = regexMap;
