var TEAM_PATTERN = '^[a-zA-Z][a-zA-Z0-9]+$';
var ROBOT_PATTERN = '^[a-zA-Z][a-zA-Z0-9]{3,29}$';

$.fn.clipboardCopy = function() { 
  if (zeroClipboardSupported) {
    (new ZeroClipboard($(this)));
    return true;
  }

  this.hide();
  return false;
};

var zeroClipboardSupported = true;
ZeroClipboard.config({
  'swfPath': 'static/lib/ZeroClipboard.swf'
});

ZeroClipboard.on("error", function(e) {
  zeroClipboardSupported = false;  
});

ZeroClipboard.on('aftercopy', function(e) {
  var container = e.target.parentNode.parentNode.parentNode;
  var message = $(container).find('.clipboard-copied-message')[0];

  // Resets the animation.
  var elem = message;
  elem.style.display = 'none';
  elem.classList.remove('animated');

  // Show the notification.
  setTimeout(function() {
    elem.style.display = 'inline-block';
    elem.classList.add('animated');
  }, 10);

  // Reset the notification.
  setTimeout(function() {
    elem.style.display = 'none';
  }, 5000);
});

function getRestUrl(args) {
  var url = '';
  for (var i = 0; i < arguments.length; ++i) {
    if (i > 0) {
       url += '/';
    }
    url += encodeURI(arguments[i])
  }
  return url;
}

function clickElement(el){
  // From: http://stackoverflow.com/questions/16802795/click-not-working-in-mocha-phantomjs-on-certain-elements
  var ev = document.createEvent("MouseEvent");
  ev.initMouseEvent(
    "click",
     true /* bubble */, true /* cancelable */,
     window, null,
     0, 0, 0, 0, /* coordinates */
     false, false, false, false, /* modifier keys */
     0 /*left*/, null);
  el.dispatchEvent(ev);
}

function getFirstTextLine(commentString) {
  if (!commentString) { return ''; }
      
  var lines = commentString.split('\n');
  var MARKDOWN_CHARS = {
    '#': true,
    '-': true,
    '>': true,
    '`': true
  };

  for (var i = 0; i < lines.length; ++i) {
    // Skip code lines.
    if (lines[i].indexOf('    ') == 0) {
      continue;
    }

    // Skip empty lines.
    if ($.trim(lines[i]).length == 0) {
      continue;
    }

    // Skip control lines.
    if (MARKDOWN_CHARS[$.trim(lines[i])[0]]) {
      continue;
    }

    return getMarkedDown(lines[i]);
  }

  return '';
}

function createRobotAccount(ApiService, is_org, orgname, name, callback) {
  ApiService.createRobot(is_org ? orgname : null, null, {'robot_shortname': name})
    .then(callback, ApiService.errorDisplay('Cannot create robot account'));
}

function createOrganizationTeam(ApiService, orgname, teamname, callback) {
  var data = {
    'name': teamname,
    'role': 'member'
  };

  var params = {
    'orgname': orgname,
    'teamname': teamname
  };
  
  ApiService.updateOrganizationTeam(data, params)
    .then(callback, ApiService.errorDisplay('Cannot create team'));
}

function getMarkedDown(string) {
  return Markdown.getSanitizingConverter().makeHtml(string || '');
}


quayDependencies = ['ngRoute', 'chieffancypants.loadingBar', 'angular-tour', 'restangular', 'angularMoment',
                    'mgcrea.ngStrap', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce', 'ansiToHtml',
                    'ngAnimate'];

if (window.__config && window.__config.MIXPANEL_KEY) {
  quayDependencies.push('angulartics');
  quayDependencies.push('angulartics.mixpanel');
}

quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoadingBarProvider) {
    cfpLoadingBarProvider.includeSpinner = false;

    /**
     * Specialized wrapper around array which provides a toggle() method for viewing the contents of the
     * array in a manner that is asynchronously filled in over a short time period. This prevents long
     * pauses in the UI for ngRepeat's when the array is significant in size.
     */
    $provide.factory('AngularViewArray', ['$interval', function($interval) {
      var ADDTIONAL_COUNT = 20;

      function _ViewArray() {
        this.isVisible = false;
        this.visibleEntries = null;
        this.hasEntries = false;
        this.entries = [];

        this.timerRef_ = null;
        this.currentIndex_ = 0;
      }

      _ViewArray.prototype.length = function() {
        return this.entries.length;
      };

      _ViewArray.prototype.get = function(index) {
        return this.entries[index];
      };

      _ViewArray.prototype.push = function(elem) {
        this.entries.push(elem);
        this.hasEntries = true;

        if (this.isVisible) {
          this.setVisible(true);
        }
      };

      _ViewArray.prototype.toggle = function() {
        this.setVisible(!this.isVisible);
      };

      _ViewArray.prototype.setVisible = function(newState) {
        this.isVisible = newState;

        this.visibleEntries = [];
        this.currentIndex_ = 0;

        if (newState) {
          this.showAdditionalEntries_();
          this.startTimer_();
        } else {
          this.stopTimer_();
        }
      };

      _ViewArray.prototype.showAdditionalEntries_ = function() {
        var i = 0;
        for (i = this.currentIndex_; i < (this.currentIndex_ + ADDTIONAL_COUNT) && i < this.entries.length; ++i) {
          this.visibleEntries.push(this.entries[i]);
        }

        this.currentIndex_ = i;
        if (this.currentIndex_ >= this.entries.length) {
          this.stopTimer_();
        }
      };

      _ViewArray.prototype.startTimer_ = function() {
        var that = this;
        this.timerRef_ = $interval(function() {
          that.showAdditionalEntries_();
        }, 10);
      };
      
      _ViewArray.prototype.stopTimer_ = function() {
        if (this.timerRef_) {
          $interval.cancel(this.timerRef_);
          this.timerRef_ = null;
        }
      };

      var service = {
        'create': function() {
          return new _ViewArray();
        }
      };

      return service;
    }]);

    /**
     * Specialized class for conducting an HTTP poll, while properly preventing multiple calls.
     */
    $provide.factory('AngularPollChannel', ['ApiService', '$timeout', function(ApiService, $timeout) {
      var _PollChannel = function(scope, requester, opt_sleeptime) {
        this.scope_ = scope;
        this.requester_ = requester;
        this.sleeptime_ = opt_sleeptime || (60 * 1000 /* 60s */);
        this.timer_ = null;

        this.working = false;
        this.polling = false;

        var that = this;
        scope.$on('$destroy', function() {
          that.stop();
        });
      };

      _PollChannel.prototype.stop = function() {
        if (this.timer_) {
          $timeout.cancel(this.timer_);
          this.timer_ = null;
          this.polling_ = false;
        }

        this.working = false;
      };

      _PollChannel.prototype.start = function() {
        // Make sure we invoke call outside the normal digest cycle, since
        // we'll call $scope.$apply ourselves.
        var that = this;
        setTimeout(function() { that.call_(); }, 0);
      };

      _PollChannel.prototype.call_ = function() {
        if (this.working) { return; }

        var that = this;
        this.working = true;
        this.scope_.$apply(function() {
          that.requester_(function(status) {
            if (status) {
              that.working = false;
              that.setupTimer_();
            } else {
              that.stop();
            }
          });
        });
      };

      _PollChannel.prototype.setupTimer_ = function() {
        if (this.timer_) { return; }

        var that = this;
        this.polling = true;
        this.timer_ = $timeout(function() {
          that.timer_ = null;
          that.call_();
        }, this.sleeptime_)
      };
    
      var service = {
        'create': function(scope, requester, opt_sleeptime) {
          return new _PollChannel(scope, requester, opt_sleeptime);
        }
      };

      return service;
    }]);

    $provide.factory('DataFileService', [function() {
      var dataFileService = {};

      dataFileService.getName_ = function(filePath) {
        var parts = filePath.split('/');
        return parts[parts.length - 1];
      };

      dataFileService.tryAsZip_ = function(buf, success, failure) {
        var zip = null;
        var zipFiles = null;
        try {      
          var zip = new JSZip(buf);
          zipFiles = zip.files;
        } catch (e) {
          failure();
          return;
        }

        var files = [];
        for (var filePath in zipFiles) {
          if (zipFiles.hasOwnProperty(filePath)) {
            files.push({
              'name': dataFileService.getName_(filePath),
              'path': filePath,
              'canRead': true,
              'toBlob': (function(fp) {
                return function() {
                  return new Blob([zip.file(fp).asArrayBuffer()]);
                };
              }(filePath))
            });
          }
        }

        success(files);
      };

      dataFileService.tryAsTarGz_ = function(buf, success, failure) {
        var gunzip = new Zlib.Gunzip(buf);
        var plain = null;

        try {
          plain = gunzip.decompress();
        } catch (e) {
          failure();
          return;
        }

        dataFileService.tryAsTar_(plain, success, failure);
      };

      dataFileService.tryAsTar_ = function(buf, success, failure) {
        var collapsePath = function(originalPath) {
          // Tar files can contain entries of the form './', so we need to collapse
          // those paths down.
          var parts = originalPath.split('/');
          for (var i = parts.length - 1; i >= 0; i--) {
            var part = parts[i];
            if (part == '.') {
              parts.splice(i, 1);
            }
          }
          return parts.join('/');
        };

        var handler = new Untar(buf);
        handler.process(function(status, read, files, err) {
          switch (status) {
            case 'error':
              failure(err);
              break;

            case 'done':
                var processed = [];
                for (var i = 0; i < files.length; ++i) {
                  var currentFile = files[i];
                  var path = collapsePath(currentFile.meta.filename);

                  if (path == '' || path == 'pax_global_header') { continue; }

                  processed.push({
                    'name': dataFileService.getName_(path),
                    'path': path,
                    'canRead': true,
                    'toBlob': (function(currentFile) {
                      return function() {
                        return new Blob([currentFile.buffer], {type: 'application/octet-binary'});
                      };
                    }(currentFile))
                  });
                }
                success(processed);
                break;
          }
        });        
      };

      dataFileService.blobToString = function(blob, callback) {
        var reader = new FileReader();
        reader.onload = function(event){
          callback(reader.result);
        };
        reader.readAsText(blob);
      };

      dataFileService.arrayToString = function(buf, callback) {
        var bb = new Blob([buf], {type: 'application/octet-binary'});
        var f = new FileReader();
        f.onload = function(e) {
          callback(e.target.result);
        };
        f.onerror = function(e) {
          callback(null);
        };
        f.onabort = function(e) {
          callback(null);
        };
        f.readAsText(bb);
      };

      dataFileService.readDataArrayAsPossibleArchive = function(buf, success, failure) {
        dataFileService.tryAsZip_(buf, success, function() {
          dataFileService.tryAsTarGz_(buf, success, failure);
        });
      };

      dataFileService.downloadDataFileAsArrayBuffer = function($scope, url, progress, error, loaded) {
        var request = new XMLHttpRequest();
        request.open('GET', url, true);
        request.responseType = 'arraybuffer';

        request.onprogress = function(e) {
          $scope.$apply(function() {
            var percentLoaded;
            if (e.lengthComputable) {
              progress(e.loaded / e.total);
            }
          });
        };

        request.onerror = function() {
          $scope.$apply(function() {
            error();
          });
        };

        request.onload = function() {
          if (this.status == 200) {
            $scope.$apply(function() {
              var uint8array = new Uint8Array(request.response);
              loaded(uint8array);
            });
            return;
          }
        };

        request.send();
      };

      return dataFileService;
    }]);


    $provide.factory('UIService', [function() {
      var uiService = {};
      
      uiService.hidePopover = function(elem) {
        var popover = $(elem).data('bs.popover');
        if (popover) {
          popover.hide();
        }
      };

      uiService.showPopover = function(elem, content) {
        var popover = $(elem).data('bs.popover');
        if (!popover) {
          $(elem).popover({'content': '-', 'placement': 'left'});
        }

        setTimeout(function() {
          var popover = $(elem).data('bs.popover');
          popover.options.content = content;
          popover.show();
        }, 500);
      };
      
      uiService.showFormError = function(elem, result) {
        var message =  result.data['message'] || result.data['error_description'] || '';
        if (message) {
          uiService.showPopover(elem, message);
        } else {
          uiService.hidePopover(elem);
        }
      };
      
      return uiService;
    }]);


    $provide.factory('UtilService', ['$sanitize', function($sanitize) {
      var utilService = {};

      utilService.isEmailAddress = function(val) {
        var emailRegex = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
        return emailRegex.test(val);
      };

      utilService.escapeHtmlString = function(text) {
        var adjusted = text.replace(/&/g, "&amp;")
          .replace(/</g, "&lt;")
          .replace(/>/g, "&gt;")
          .replace(/"/g, "&quot;")
          .replace(/'/g, "&#039;");
        
        return adjusted;
      };
      
      utilService.textToSafeHtml = function(text) {
        return $sanitize(utilService.escapeHtmlString(text));
      };
      
      return utilService;
    }]);


    $provide.factory('PingService', [function() {
      var pingService = {};
      var pingCache = {};

      var invokeCallback = function($scope, pings, callback) {
        if (pings[0] == -1) {
          setTimeout(function() {
            $scope.$apply(function() {
              callback(-1, false, -1);
            });
          }, 0);
          return;
        }

        var sum = 0;
        for (var i = 0; i < pings.length; ++i) {
          sum += pings[i];
        }

        // Report the average ping.
        setTimeout(function() {
          $scope.$apply(function() {
            callback(Math.floor(sum / pings.length), true, pings.length);
          });
        }, 0);
      };

      var reportPingResult = function($scope, url, ping, callback) {
        // Lookup the cached ping data, if any.
        var cached = pingCache[url];
        if (!cached) {
          cached = pingCache[url] = {
            'pings': []
          };
        }

        // If an error occurred, report it and done.
        if (ping < 0) {
          cached['pings'] = [-1];
          invokeCallback($scope, [-1], callback);
          return;
        }

        // Otherwise, add the current ping and determine the average.
        cached['pings'].push(ping);

        // Invoke the callback.
        invokeCallback($scope, cached['pings'], callback);

        // Schedule another check if we've done less than three.
        if (cached['pings'].length < 3) {
          setTimeout(function() {
            pingUrlInternal($scope, url, callback);
          }, 1000);
        }
      };
      
      var pingUrlInternal = function($scope, url, callback) {
        var path = url + '?cb=' + (Math.random() * 100);
	var start = new Date();
	var xhr = new XMLHttpRequest();
	xhr.onerror = function() {
          reportPingResult($scope, url, -1, callback);
	};

	xhr.onreadystatechange = function () {
	  if (xhr.readyState === xhr.HEADERS_RECEIVED) {
	    if (xhr.status != 200) {
              reportPingResult($scope, url, -1, callback);
	      return;
	    }

            var ping = (new Date() - start);
            reportPingResult($scope, url, ping, callback);
	  }
	};

	xhr.open("GET", path);
	xhr.send(null);
      };

      pingService.pingUrl = function($scope, url, callback) {
        if (pingCache[url]) {
          invokeCallback($scope, pingCache[url]['pings'], callback);          
          return;
        }

        // Note: We do each in a callback after 1s to prevent it running when other code
        // runs (which can skew the results).
        setTimeout(function() {
          pingUrlInternal($scope, url, callback);
        }, 1000);
      };
      
      return pingService;
    }]);


    $provide.factory('TriggerService', ['UtilService', '$sanitize', function(UtilService, $sanitize) {
      var triggerService = {};

      var triggerTypes = {
        'github': {
          'description': function(config) {
            var source = UtilService.textToSafeHtml(config['build_source']);
            var desc = '<i class="fa fa-github fa-lg" style="margin-left: 2px; margin-right: 2px"></i> Push to Github Repository ';
            desc += '<a href="https://github.com/' + source  + '" target="_blank">' + source + '</a>';
            desc += '<br>Dockerfile folder: //' + UtilService.textToSafeHtml(config['subdir']);
            return desc;
          },

          'run_parameters': [
            {
              'title': 'Branch',
              'type': 'option',
              'name': 'branch_name'
            }
          ]
        }
      }

      triggerService.getDescription = function(name, config) {
        var type = triggerTypes[name];
        if (!type) {
          return 'Unknown';
        }
        return type['description'](config);
      };

      triggerService.getRunParameters = function(name, config) {
        var type = triggerTypes[name];
        if (!type) {
          return [];
        }
        return type['run_parameters'];
      }

      return triggerService;
    }]);

    $provide.factory('StringBuilderService', ['$sce', 'UtilService', function($sce, UtilService) {
      var stringBuilderService = {};

      stringBuilderService.buildUrl = function(value_or_func, metadata) {
        var url = value_or_func;
        if (typeof url != 'string') {
          url = url(metadata);
        }

        // Find the variables to be replaced.
        var varNames = [];
        for (var i = 0; i < url.length; ++i) {
          var c = url[i];
          if (c == '{') {
            for (var j = i + 1; j < url.length; ++j) {
              var d = url[j];
              if (d == '}') {
                varNames.push(url.substring(i + 1, j));
                i = j;
                break;
              }
            }
          }
        }

        // Replace all variables found.
        for (var i = 0; i < varNames.length; ++i) {
          var varName = varNames[i];
          if (!metadata[varName]) {
            return null;
          }

          url = url.replace('{' + varName + '}', metadata[varName]);
        }

        return url;
      };

      stringBuilderService.buildString = function(value_or_func, metadata) {
         var fieldIcons = {
          'inviter': 'user',
          'username': 'user',
          'user': 'user',
          'email': 'envelope',
          'activating_username': 'user',
          'delegate_user': 'user',
          'delegate_team': 'group',
          'team': 'group',
          'token': 'key',
          'repo': 'hdd-o',
          'robot': 'wrench',
          'tag': 'tag',
          'role': 'th-large',
          'original_role': 'th-large',
          'application_name': 'cloud',
          'image': 'archive',
          'original_image': 'archive',
          'client_id': 'chain'
        };

        var filters = {
          'obj': function(value) {
            if (!value) { return []; }
            return Object.getOwnPropertyNames(value);
          },

          'updated_tags': function(value) {
            if (!value) { return []; }
            return Object.getOwnPropertyNames(value);
          }
        };

        var description = value_or_func;
        if (typeof description != 'string') {
          description = description(metadata);
        }
          
        for (var key in metadata) {
          if (metadata.hasOwnProperty(key)) {
            var value = metadata[key] != null ? metadata[key] : '(Unknown)';
            if (filters[key]) {
              value = filters[key](value);
            }

            if (Array.isArray(value)) {
              value = value.join(', ');
            }

            value = value.toString();

            if (key.indexOf('image') >= 0) {
              value = value.substr(0, 12);
            }

            var safe = UtilService.escapeHtmlString(value);
            var markedDown = getMarkedDown(value);
            markedDown = markedDown.substr('<p>'.length, markedDown.length - '<p></p>'.length);

            var icon = fieldIcons[key];
            if (icon) {
              markedDown = '<i class="fa fa-' + icon + '"></i>' + markedDown;
            }

            description = description.replace('{' + key + '}', '<code title="' + safe  + '">' + markedDown + '</code>');
          }
        }
        return $sce.trustAsHtml(description.replace('\n', '<br>'));
      };

      return stringBuilderService;
    }]);


    $provide.factory('ImageMetadataService', ['UtilService', function(UtilService) {
      var metadataService = {};
      metadataService.getFormattedCommand = function(image) {
        if (!image || !image.command || !image.command.length) {
          return '';
        }

        var getCommandStr = function(command) {
          // Handle /bin/sh commands specially.
          if (command.length > 2 && command[0] == '/bin/sh' && command[1] == '-c') {
            return command[2];
          }
          
          return command.join(' ');
        };

        return getCommandStr(image.command);
      };

      metadataService.getEscapedFormattedCommand = function(image) {
        return UtilService.textToSafeHtml(metadataService.getFormattedCommand(image));
      };

      return metadataService;
    }]);

    $provide.factory('Features', [function() {
      if (!window.__features) {
        return {};
      }

      var features = window.__features;
      features.getFeature = function(name, opt_defaultValue) {
        var value = features[name];
        if (value == null) {
          return opt_defaultValue;
        }
        return value;
      };

      features.hasFeature = function(name) {
        return !!features.getFeature(name);
      };
      
      features.matchesFeatures = function(list) {
        for (var i = 0; i < list.length; ++i) {
          var value = features.getFeature(list[i]);
          if (!value) {
            return false;
          }
        }
        return true;
      };

      return features;
    }]);

    $provide.factory('Config', [function() {
      if (!window.__config) {
        return {};
      }

      var config = window.__config;
      config.getDomain = function() {
        return config['SERVER_HOSTNAME'];
      };

      config.getUrl = function(opt_path) {
        var path = opt_path || '';
        return config['PREFERRED_URL_SCHEME'] + '://' + config['SERVER_HOSTNAME'] + path;
      };

      config.getValue = function(name, opt_defaultValue) {
        var value = config[name];
        if (value == null) {
          return opt_defaultValue;
        }
        return value;
      };

      return config;
    }]);

  $provide.factory('ApiService', ['Restangular', '$q', function(Restangular, $q) {
      var apiService = {};

      var getResource = function(path, opt_background) {
        var resource = {};
        resource.url = path;
        resource.withOptions = function(options) {
          this.options = options;
          return this;
        };

        resource.get = function(processor, opt_errorHandler) {
          var options = this.options;
          var performer = Restangular.one(this.url);

          var result = {
            'loading': true,
            'value': null,
            'hasError': false
          };

          if (opt_background) {
            performer.withHttpConfig({
              'ignoreLoadingBar': true
            });
          }

          performer.get(options).then(function(resp) {
            result.value = processor(resp);
            result.loading = false;
          }, function(resp) {
            result.hasError = true;
            result.loading = false;
            if (opt_errorHandler) {
              opt_errorHandler(resp);
            }
          });

          return result;
        };

        return resource;
      };

      var buildUrl = function(path, parameters) {
        // We already have /api/v1/ on the URLs, so remove them from the paths.
        path = path.substr('/api/v1/'.length, path.length);

        // Build the path, adjusted with the inline parameters.
        var used = {};
        var url = '';
        for (var i = 0; i < path.length; ++i) {
          var c = path[i];
          if (c == '{') {
            var end = path.indexOf('}', i);
            var varName = path.substr(i + 1, end - i - 1);

            if (!parameters[varName]) {
              throw new Error('Missing parameter: ' + varName);
            }

            used[varName] = true;
            url += parameters[varName];
            i = end;
            continue;
          }

          url += c;
        }

        // Append any query parameters.
        var isFirst = true;
        for (var paramName in parameters) {
          if (!parameters.hasOwnProperty(paramName)) { continue; }
          if (used[paramName]) { continue; }

          var value = parameters[paramName];
          if (value) {
            url += isFirst ? '?' : '&';
            url += paramName + '=' + encodeURIComponent(value)
            isFirst = false;
          }
        }

        return url;
      };

      var getGenericOperationName = function(userOperationName) {
        return userOperationName.replace('User', '');
      };

      var getMatchingUserOperationName = function(orgOperationName, method, userRelatedResource) {
        if (userRelatedResource) {
          var operations = userRelatedResource['operations'];
          for (var i = 0; i < operations.length; ++i) {
            var operation = operations[i];          
            if (operation['method'].toLowerCase() == method) {
              return operation['nickname'];
            }
          }
        }

        throw new Error('Could not find user operation matching org operation: ' + orgOperationName);
      };

      var buildMethodsForEndpointResource = function(endpointResource, resourceMap) {
        var name = endpointResource['name'];
        var operations = endpointResource['operations'];
        for (var i = 0; i < operations.length; ++i) {
          var operation = operations[i];          
          buildMethodsForOperation(operation, endpointResource, resourceMap);
        }
      };

      var freshLoginFailCheck = function(opName, opArgs) {
        return function(resp) {
          var deferred = $q.defer();
          
          // If the error is a fresh login required, show the dialog.
          if (resp.status == 401 && resp.data['error_type'] == 'fresh_login_required') {
            var verifyNow = function() {
              var info = {
                'password': $('#freshPassword').val()
              };

              $('#freshPassword').val('');

              // Conduct the sign in of the user.
              apiService.verifyUser(info).then(function() {
                // On success, retry the operation. if it succeeds, then resolve the
                // deferred promise with the result. Otherwise, reject the same.
                apiService[opName].apply(apiService, opArgs).then(function(resp) {
                  deferred.resolve(resp);
                }, function(resp) {
                  deferred.reject(resp);
                });
              }, function(resp) {
                // Reject with the sign in error.
                deferred.reject({'data': {'message': 'Invalid verification credentials'}});
              });
            };
           
            var box = bootbox.dialog({
              "message": 'It has been more than a few minutes since you last logged in, ' +
                'so please verify your password to perform this sensitive operation:' + 
                '<form style="margin-top: 10px" action="javascript:void(0)">' +
                '<input id="freshPassword" class="form-control" type="password" placeholder="Current Password">' + 
                '</form>',
              "title": 'Please Verify',
              "buttons": {
                "verify": {
                  "label": "Verify",
                  "className": "btn-success",
                  "callback": verifyNow
                },
                "close": {
                  "label": "Cancel",
                  "className": "btn-default",
                  "callback": function() {
                    deferred.reject({'data': {'message': 'Verification canceled'}});
                  }
                }
              }
            });

            box.bind('shown.bs.modal', function(){
              box.find("input").focus();
              box.find("form").submit(function() {
                if (!$('#freshPassword').val()) { return; }
                
                box.modal('hide');
                verifyNow();
              });
            });

            // Return a new promise. We'll accept or reject it based on the result
            // of the login.
            return deferred.promise;
          }

          // Otherwise, we just 'raise' the error via the reject method on the promise.
          return $q.reject(resp);
        };
      };

      var buildMethodsForOperation = function(operation, resource, resourceMap) {
        var method = operation['method'].toLowerCase();
        var operationName = operation['nickname'];
        var path = resource['path'];

        // Add the operation itself.
        apiService[operationName] = function(opt_options, opt_parameters, opt_background) {
          var one = Restangular.one(buildUrl(path, opt_parameters));
          if (opt_background) {
            one.withHttpConfig({
              'ignoreLoadingBar': true
            });
          }
          
          var opObj = one['custom' + method.toUpperCase()](opt_options);

          // If the operation requires_fresh_login, then add a specialized error handler that
          // will defer the operation's result if sudo is requested.
          if (operation['requires_fresh_login']) {
            opObj = opObj.catch(freshLoginFailCheck(operationName, arguments));
          }
          return opObj;
        };

        // If the method for the operation is a GET, add an operationAsResource method.
        if (method == 'get') {
          apiService[operationName + 'AsResource'] = function(opt_parameters, opt_background) {
            return getResource(buildUrl(path, opt_parameters), opt_background);
          };
        }

        // If the resource has a user-related resource, then make a generic operation for this operation
        // that can call both the user and the organization versions of the operation, depending on the
        // parameters given.
        if (resource['quayUserRelated']) {
          var userOperationName = getMatchingUserOperationName(operationName, method, resourceMap[resource['quayUserRelated']]);
          var genericOperationName = getGenericOperationName(userOperationName);
          apiService[genericOperationName] = function(orgname, opt_options, opt_parameters, opt_background) {
            if (orgname) {
              if (orgname.name) {
                orgname = orgname.name;
              }

              var params = jQuery.extend({'orgname' : orgname}, opt_parameters || {}, opt_background);
              return apiService[operationName](opt_options, params);
            } else {
              return apiService[userOperationName](opt_options, opt_parameters, opt_background);
            }
          };
        }
      };

      if (!window.__endpoints) {
        return apiService;
      }

      var resourceMap = {};

      // Build the map of resource names to their objects.
      for (var i = 0; i < window.__endpoints.length; ++i) {
        var endpointResource = window.__endpoints[i];
        resourceMap[endpointResource['name']] = endpointResource;
      }

      // Construct the methods for each API endpoint.
      for (var i = 0; i < window.__endpoints.length; ++i) {
        var endpointResource = window.__endpoints[i];
        buildMethodsForEndpointResource(endpointResource, resourceMap);
      }

      apiService.getErrorMessage = function(resp, defaultMessage) {
        var message = defaultMessage;
        if (resp['data']) {
          message = resp['data']['error_message'] || resp['data']['message'] || resp['data']['error_description'] || message;
        }

        return message;
      };

      apiService.errorDisplay = function(defaultMessage, opt_handler) {
        return function(resp) {
          var message = apiService.getErrorMessage(resp, defaultMessage);
          if (opt_handler) {
            var handlerMessage = opt_handler(resp);
            if (handlerMessage) {
              message = handlerMessage;
            }
          }

          bootbox.dialog({
            "message": message,
            "title": defaultMessage,
            "buttons": {
              "close": {
                "label": "Close",
                "className": "btn-primary"
              }
            }
          });
        };
      };

      return apiService;
    }]);
    
    $provide.factory('CookieService', ['$cookies', '$cookieStore', function($cookies, $cookieStore) {
      var cookieService = {};
      cookieService.putPermanent = function(name, value) {
        document.cookie = escape(name) + "=" + escape(value) + "; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/";
      };

      cookieService.putSession = function(name, value) {
        $cookies[name] = value;
      };

      cookieService.clear = function(name) {
        $cookies[name] = '';
      };

      cookieService.get = function(name) {
        return $cookies[name];
      };

      return cookieService;
    }]);

    $provide.factory('UserService', ['ApiService', 'CookieService', '$rootScope', 'Config',
                                     function(ApiService, CookieService, $rootScope, Config) {
      var userResponse = {
        verified: false,
        anonymous: true,
        username: null,
        email: null,
        organizations: [],
        logins: []
      }

      var userService = {}

      userService.hasEverLoggedIn = function() {
        return CookieService.get('quay.loggedin') == 'true';
      };

      userService.updateUserIn = function(scope, opt_callback) {
        scope.$watch(function () { return userService.currentUser(); }, function (currentUser) {
          scope.user = currentUser;
          if (opt_callback) {
            opt_callback(currentUser); 
          }
        }, true);
      };

      userService.load = function(opt_callback) {
        var handleUserResponse = function(loadedUser) {
          userResponse = loadedUser;

          if (!userResponse.anonymous) {
            if (Config.MIXPANEL_KEY) {
              mixpanel.identify(userResponse.username);
              mixpanel.people.set({
                '$email': userResponse.email,
                '$username': userResponse.username,
                'verified': userResponse.verified
              });
              mixpanel.people.set_once({
                '$created': new Date()
              })
            }

            if (window.olark !== undefined) {
              olark('api.visitor.getDetails', function(details) {
                if (details.fullName === null) {
                  olark('api.visitor.updateFullName', {fullName: userResponse.username});
                }
              });
              olark('api.visitor.updateEmailAddress', {emailAddress: userResponse.email});
              olark('api.chat.updateVisitorStatus', {snippet: 'username: ' + userResponse.username});
            }

            if (window.Raven !== undefined) {
              Raven.setUser({
                email: userResponse.email,
                id: userResponse.username
              });
            }

            CookieService.putPermanent('quay.loggedin', 'true');
          } else {
            if (window.Raven !== undefined) {
              Raven.setUser();
            }
          }

          if (opt_callback) {
            opt_callback();
          }
        };

        ApiService.getLoggedInUser().then(function(loadedUser) {
          handleUserResponse(loadedUser);
        }, function() {
          handleUserResponse({'anonymous': true});        
        });
      };

      userService.getOrganization = function(name) {
        if (!userResponse || !userResponse.organizations) { return null; }
        for (var i = 0; i < userResponse.organizations.length; ++i) {
          var org = userResponse.organizations[i];
          if (org.name == name) {
            return org;
          }
        }

        return null;
      };

      userService.isNamespaceAdmin = function(namespace) {
        if (namespace == userResponse.username) {
          return true;
        }

        var org = userService.getOrganization(namespace);
        if (!org) {
          return false;
        }

        return org.is_org_admin;
      };

      userService.isKnownNamespace = function(namespace) {
        if (namespace == userResponse.username) {
          return true;
        }

        var org = userService.getOrganization(namespace);
        return !!org;
      };
        
      userService.currentUser = function() {
        return userResponse;
      };

      // Update the user in the root scope.
      userService.updateUserIn($rootScope);

      // Load the user the first time.
      userService.load();

      return userService;
    }]);

  $provide.factory('ExternalNotificationData', ['Config', 'Features', function(Config, Features) {
    var externalNotificationData = {};

    var events = [
      {
        'id': 'repo_push',
        'title': 'Push to Repository',
        'icon': 'fa-upload'
      },
      { 
        'id': 'build_queued',
        'title': 'Dockerfile Build Queued',
        'icon': 'fa-tasks'
      },
      { 
        'id': 'build_start',
        'title': 'Dockerfile Build Started',
        'icon': 'fa-circle-o-notch'
      },
      { 
        'id': 'build_success',
        'title': 'Dockerfile Build Successfully Completed',
        'icon': 'fa-check-circle-o'
      },
      { 
        'id': 'build_failure',
        'title': 'Dockerfile Build Failed',
        'icon': 'fa-times-circle-o'
      }
    ];

    var methods = [
      {
        'id': 'quay_notification',
        'title': Config.REGISTRY_TITLE + ' Notification',
        'icon': 'quay-icon',
        'fields': [
          {
            'name': 'target',
            'type': 'entity',
            'title': 'Recipient'
          }
        ]
      },
      { 
        'id': 'email',
        'title': 'E-mail',
        'icon': 'fa-envelope',
        'fields': [
          {
            'name': 'email',
            'type': 'email',
            'title': 'E-mail address'
          }
        ],
        'enabled': Features.MAILING
      },
      {
        'id': 'webhook',
        'title': 'Webhook POST',
        'icon': 'fa-link',
        'fields': [
          {
            'name': 'url',
            'type': 'url',
            'title': 'Webhook URL'
          }
        ]
      },
      {
        'id': 'flowdock',
        'title': 'Flowdock Team Notification',
        'icon': 'flowdock-icon',
        'fields': [
          {
            'name': 'flow_api_token',
            'type': 'string',
            'title': 'Flow API Token',
            'help_url': 'https://www.flowdock.com/account/tokens'
          }
        ]
      },
      {
        'id': 'hipchat',
        'title': 'HipChat Room Notification',
        'icon': 'hipchat-icon',
        'fields': [
          {
            'name': 'room_id',
            'type': 'string',
            'title': 'Room ID #'
          },
          {
            'name': 'notification_token',
            'type': 'string',
            'title': 'Notification Token'
          }
        ]
      },
      {
        'id': 'slack',
        'title': 'Slack Room Notification',
        'icon': 'slack-icon',
        'fields': [
          {
            'name': 'subdomain',
            'type': 'string',
            'title': 'Slack Subdomain'
          },
          {
            'name': 'token',
            'type': 'string',
            'title': 'Token',
            'help_url': 'https://{subdomain}.slack.com/services/new/incoming-webhook'
          }
        ]
      }
    ];

    var methodMap = {};
    var eventMap = {};

    for (var i = 0; i < methods.length; ++i) {
      methodMap[methods[i].id] = methods[i];
    }

    for (var i = 0; i < events.length; ++i) {
      eventMap[events[i].id] = events[i];
    }

    externalNotificationData.getSupportedEvents = function() {
      return events;
    };

    externalNotificationData.getSupportedMethods = function() {
      var filtered = [];
      for (var i = 0; i < methods.length; ++i) {
        if (methods[i].enabled !== false) {
          filtered.push(methods[i]);
        }
      }
      return filtered;
    };

    externalNotificationData.getEventInfo = function(event) {
      return eventMap[event];
    };

    externalNotificationData.getMethodInfo = function(method) {
      return methodMap[method];
    };

    return externalNotificationData;
  }]);

  $provide.factory('NotificationService', ['$rootScope', '$interval', 'UserService', 'ApiService', 'StringBuilderService', 'PlanService', 'UserService', 'Config', '$location',
                                           function($rootScope, $interval, UserService, ApiService, StringBuilderService, PlanService, UserService, Config, $location) {
      var notificationService = {
        'user': null,
        'notifications': [],
        'notificationClasses': [],
        'notificationSummaries': [],
        'additionalNotifications': false
      };

      var pollTimerHandle = null;

      var notificationKinds = {
        'test_notification': {
          'level': 'primary',
          'message': 'This notification is a long message for testing: {obj}',
          'page': '/about/',
          'dismissable': true
        },
        'org_team_invite': {
          'level': 'primary',
          'message': '{inviter} is inviting you to join team {team} under organization {org}',
          'actions': [
            {
              'title': 'Join team',
              'kind': 'primary',
              'handler': function(notification) {
                window.location = '/confirminvite?code=' + notification.metadata['code'];
              }
            },
            {
              'title': 'Decline',
              'kind': 'default',
              'handler': function(notification) {
                ApiService.declineOrganizationTeamInvite(null, {'code': notification.metadata['code']}).then(function() {
                  notificationService.update();
                });
              }
            }
          ]
        },
        'password_required': {
          'level': 'error',
          'message': 'In order to begin pushing and pulling repositories, a password must be set for your account',
          'page': '/user?tab=password'
        },
        'over_private_usage': {
          'level': 'error',
          'message': 'Namespace {namespace} is over its allowed private repository count. ' +
            '<br><br>Please upgrade your plan to avoid disruptions in service.',
          'page': function(metadata) {
            var organization = UserService.getOrganization(metadata['namespace']);
            if (organization) {
              return '/organization/' + metadata['namespace'] + '/admin';
            } else {
              return '/user';
            }
          }
        },
        'expiring_license': {
          'level': 'error',
          'message': 'Your license will expire at: {expires_at} ' +
            '<br><br>Please contact support to purchase a new license.',
          'page': '/contact/'
        },
        'maintenance': {
          'level': 'warning',
          'message': 'We will be down for schedule maintenance from {from_date} to {to_date} ' +
            'for {reason}. We are sorry about any inconvenience.',
          'page': 'http://status.quay.io/'
        },
        'repo_push': {
          'level': 'info',
          'message': function(metadata) {
            if (metadata.updated_tags && Object.getOwnPropertyNames(metadata.updated_tags).length) {
              return 'Repository {repository} has been pushed with the following tags updated: {updated_tags}';
            } else {
              return 'Repository {repository} has been pushed';
            }
          },
          'page': function(metadata) {
            return '/repository/' + metadata.repository;
          },
          'dismissable': true
        },
        'build_queued': {
          'level': 'info',
          'message': 'A build has been queued for repository {repository}',
          'page': function(metadata) {
            return '/repository/' + metadata.repository + '/build?current=' + metadata.build_id;
          },
          'dismissable': true
        },
        'build_start': {
          'level': 'info',
          'message': 'A build has been started for repository {repository}',
          'page': function(metadata) {
            return '/repository/' + metadata.repository + '/build?current=' + metadata.build_id;
          },
          'dismissable': true
        },
        'build_success': {
          'level': 'info',
          'message': 'A build has succeeded for repository {repository}',
          'page': function(metadata) {
            return '/repository/' + metadata.repository + '/build?current=' + metadata.build_id;
          },
          'dismissable': true
        },
        'build_failure': {
          'level': 'error',
          'message': 'A build has failed for repository {repository}',
          'page': function(metadata) {
            return '/repository/' + metadata.repository + '/build?current=' + metadata.build_id;
          },
          'dismissable': true
        }
      };

      notificationService.dismissNotification = function(notification) {
        notification.dismissed = true;
        var params = {
          'uuid': notification.id
        };
        
        ApiService.updateUserNotification(notification, params, function() {
          notificationService.update();
        }, ApiService.errorDisplay('Could not update notification'));

        var index = $.inArray(notification, notificationService.notifications);
        if (index >= 0) {
          notificationService.notifications.splice(index, 1);
        }
      };

      notificationService.getActions = function(notification) {
        var kindInfo = notificationKinds[notification['kind']];
        if (!kindInfo) {
          return [];
        }

        return kindInfo['actions'] || [];
      };

      notificationService.canDismiss = function(notification) {
        var kindInfo = notificationKinds[notification['kind']];
        if (!kindInfo) {
          return false;
        }
        return !!kindInfo['dismissable'];
      };

      notificationService.getPage = function(notification) {
        var kindInfo = notificationKinds[notification['kind']];
        if (!kindInfo) {
          return null;
        }

        var page = kindInfo['page'];
        if (page != null && typeof page != 'string') {
          page = page(notification['metadata']);
        }
        return page || '';
      };

      notificationService.getMessage = function(notification) {
        var kindInfo = notificationKinds[notification['kind']];
        if (!kindInfo) {
          return '(Unknown notification kind: ' + notification['kind'] + ')';
        }
        return StringBuilderService.buildString(kindInfo['message'], notification['metadata']);
      };

      notificationService.getClass = function(notification) {
        var kindInfo = notificationKinds[notification['kind']];
        if (!kindInfo) {
          return 'notification-info';
        }
        return 'notification-' + kindInfo['level'];
      };

      notificationService.getClasses = function(notifications) {
        var classes = [];
        for (var i = 0; i < notifications.length; ++i) {
          var notification = notifications[i];
          classes.push(notificationService.getClass(notification));
        }
        return classes.join(' ');
      };

      notificationService.update = function() {
        var user = UserService.currentUser();
        if (!user || user.anonymous) {
          return;
        }

        ApiService.listUserNotifications().then(function(resp) {
          notificationService.notifications = resp['notifications'];
          notificationService.additionalNotifications = resp['additional'];
          notificationService.notificationClasses = notificationService.getClasses(notificationService.notifications);
        });
      };

      notificationService.reset = function() {
        $interval.cancel(pollTimerHandle);
        pollTimerHandle = $interval(notificationService.update, 5 * 60 * 1000 /* five minutes */);
      };

      // Watch for plan changes and update.
      PlanService.registerListener(this, function(plan) {
        notificationService.reset();
        notificationService.update();
      });

      // Watch for user changes and update.
      $rootScope.$watch(function() { return UserService.currentUser(); }, function(currentUser) {
        notificationService.reset();
        notificationService.update();
      });

      return notificationService;
    }]);

    $provide.factory('KeyService', ['$location', 'Config', function($location, Config) {
      var keyService = {}

      keyService['stripePublishableKey'] = Config['STRIPE_PUBLISHABLE_KEY'];

      keyService['githubClientId'] = Config['GITHUB_CLIENT_ID'];
      keyService['githubLoginClientId'] = Config['GITHUB_LOGIN_CLIENT_ID'];
      keyService['githubRedirectUri'] = Config.getUrl('/oauth2/github/callback');

      keyService['googleLoginClientId'] = Config['GOOGLE_LOGIN_CLIENT_ID'];
      keyService['googleRedirectUri'] = Config.getUrl('/oauth2/google/callback');

      keyService['googleLoginUrl'] = 'https://accounts.google.com/o/oauth2/auth?response_type=code&';
      keyService['githubLoginUrl'] = 'https://github.com/login/oauth/authorize?';

      keyService['googleLoginScope'] = 'openid email';
      keyService['githubLoginScope'] = 'user:email';

      keyService.getExternalLoginUrl = function(service, action) {
        var state_clause = '';
        if (Config.MIXPANEL_KEY && window.mixpanel) {
          if (mixpanel.get_distinct_id !== undefined) {
            state_clause = "&state=" + encodeURIComponent(mixpanel.get_distinct_id());
          }
        }

        var client_id = keyService[service + 'LoginClientId'];
        var scope = keyService[service + 'LoginScope'];
        var redirect_uri = keyService[service + 'RedirectUri'];
        if (action == 'attach') {
          redirect_uri += '/attach';
        }

        var url = keyService[service + 'LoginUrl'] + 'client_id=' + client_id + '&scope=' + scope +
          '&redirect_uri=' + redirect_uri + state_clause;

        return url;
      };

      return keyService;
    }]);
  
    $provide.factory('PlanService', ['KeyService', 'UserService', 'CookieService', 'ApiService', 'Features', 'Config',
                                     function(KeyService, UserService, CookieService, ApiService, Features, Config) {
      var plans = null;
      var planDict = {};
      var planService = {};
      var listeners = [];

      var previousSubscribeFailure = false;

      planService.getFreePlan = function() {
        return 'free';
      };

      planService.registerListener = function(obj, callback) {
        listeners.push({'obj': obj, 'callback': callback});
      };

      planService.unregisterListener = function(obj) {
        for (var i = 0; i < listeners.length; ++i) {
          if (listeners[i].obj == obj) {
            listeners.splice(i, 1);
            break;
          }
        }
      };

      planService.notePlan = function(planId) {
        if (Features.BILLING) {
          CookieService.putSession('quay.notedplan', planId);
        }
      };

      planService.isOrgCompatible = function(plan) {
        return plan['stripeId'] == planService.getFreePlan() || plan['bus_features'];
      };

      planService.getMatchingBusinessPlan = function(callback) {
        planService.getPlans(function() {
          planService.getSubscription(null, function(sub) {
            var plan = planDict[sub.plan];
            if (!plan) {
              planService.getMinimumPlan(0, true, callback);
              return;
            }

            var count = Math.max(sub.usedPrivateRepos, plan.privateRepos);
            planService.getMinimumPlan(count, true, callback);
          }, function() {
            planService.getMinimumPlan(0, true, callback);
          });
        });
      };

      planService.handleNotedPlan = function() {
        var planId = planService.getAndResetNotedPlan();
        if (!planId || !Features.BILLING) { return false; }

        UserService.load(function() {
          if (UserService.currentUser().anonymous) {
            return;
          }

          planService.getPlan(planId, function(plan) {
            if (planService.isOrgCompatible(plan)) {
              document.location = '/organizations/new/?plan=' + planId;
            } else {
              document.location = '/user?plan=' + planId;
            }
          });
        });

        return true;
      };

      planService.getAndResetNotedPlan = function() {
        var planId = CookieService.get('quay.notedplan');
        CookieService.clear('quay.notedplan');
        return planId;
      };
      
      planService.handleCardError = function(resp) {
        if (!planService.isCardError(resp)) { return; }

        bootbox.dialog({
          "message": resp.data.carderror,
          "title": "Credit card issue",
          "buttons": {
            "close": {
              "label": "Close",
              "className": "btn-primary"
            }
          }
        });
      };

      planService.isCardError = function(resp) {
        return resp && resp.data && resp.data.carderror;
      };

      planService.verifyLoaded = function(callback) {
        if (!Features.BILLING) { return; }

        if (plans && plans.length) {
          callback(plans);
          return;
        }

        ApiService.listPlans().then(function(data) {
          plans = data.plans || [];
          for(var i = 0; i < plans.length; i++) {
            planDict[plans[i].stripeId] = plans[i];
          }
          callback(plans);
        }, function() { callback([]); });
      };

      planService.getPlans = function(callback, opt_includePersonal) {
        planService.verifyLoaded(function() {
          var filtered = [];
          for (var i = 0; i < plans.length; ++i) {
            var plan = plans[i];
            if (plan['deprecated']) { continue; }
            if (!opt_includePersonal && !planService.isOrgCompatible(plan)) { continue; }
            filtered.push(plan);
          }
          callback(filtered);
        });
      };

      planService.getPlan = function(planId, callback) {
        planService.getPlanIncludingDeprecated(planId, function(plan) {
          if (!plan['deprecated']) {
            callback(plan);
          }
        });
      };

      planService.getPlanIncludingDeprecated = function(planId, callback) {
        planService.verifyLoaded(function() {
          if (planDict[planId]) {            
            callback(planDict[planId]);
          }
        });
      };

      planService.getMinimumPlan = function(privateCount, isBusiness, callback) {
        planService.getPlans(function(plans) {          
          for (var i = 0; i < plans.length; i++) {
            var plan = plans[i];
            if (plan.privateRepos >= privateCount) {
              callback(plan);
              return;
            }
          }

          callback(null);
        }, /* include personal */!isBusiness);
      };

      planService.getSubscription = function(orgname, success, failure) {
        if (!Features.BILLING) { return; }

        ApiService.getSubscription(orgname).then(success, failure);
      };

      planService.setSubscription = function(orgname, planId, success, failure, opt_token) {
        if (!Features.BILLING) { return; }

        var subscriptionDetails = {
          plan: planId
        };

        if (opt_token) {
          subscriptionDetails['token'] = opt_token.id;
        }

        ApiService.updateSubscription(orgname, subscriptionDetails).then(function(resp) {
          success(resp);
          planService.getPlan(planId, function(plan) {
            for (var i = 0; i < listeners.length; ++i) {
              listeners[i]['callback'](plan);
            }
          });
        }, failure);
      };

      planService.getCardInfo = function(orgname, callback) {
        if (!Features.BILLING) { return; }

        ApiService.getCard(orgname).then(function(resp) {
          callback(resp.card);
        }, function() {
          callback({'is_valid': false});
        });
      };

      planService.changePlan = function($scope, orgname, planId, callbacks, opt_async) {
        if (!Features.BILLING) { return; }

        if (callbacks['started']) {
          callbacks['started']();
        }

        planService.getPlan(planId, function(plan) {
          if (orgname && !planService.isOrgCompatible(plan)) { return; }

          planService.getCardInfo(orgname, function(cardInfo) {
            if (plan.price > 0 && (previousSubscribeFailure || !cardInfo.last4)) {
              var title = cardInfo.last4 ? 'Subscribe' : 'Start Trial ({{amount}} plan)';
              planService.showSubscribeDialog($scope, orgname, planId, callbacks, title, /* async */true);
              return;
            }
        
            previousSubscribeFailure = false;

            planService.setSubscription(orgname, planId, callbacks['success'], function(resp) {
              previousSubscribeFailure = true;
              planService.handleCardError(resp);
              callbacks['failure'](resp);
            });
          });
        });
      };

      planService.changeCreditCard = function($scope, orgname, callbacks) {
        if (!Features.BILLING) { return; }

        if (callbacks['opening']) {
          callbacks['opening']();
        }

        var submitted = false;
        var submitToken = function(token) {
          if (submitted) { return; }
          submitted = true;
          $scope.$apply(function() {            
            if (callbacks['started']) {
              callbacks['started']();
            }
            
            var cardInfo = {
              'token': token.id
            };

            ApiService.setCard(orgname, cardInfo).then(callbacks['success'], function(resp) {
              planService.handleCardError(resp);
              callbacks['failure'](resp);
            });
          });
        };

        var email = planService.getEmail(orgname);
        StripeCheckout.open({
            key:         KeyService.stripePublishableKey,
            address:     false,
            email:       email,
            currency:    'usd',
            name:        'Update credit card',
            description: 'Enter your credit card number',
            panelLabel:  'Update',
            token:       submitToken,
            image:       'static/img/quay-icon-stripe.png',
            opened:      function() { $scope.$apply(function() { callbacks['opened']() }); },
            closed:      function() { $scope.$apply(function() { callbacks['closed']() }); }
         });
      };

      planService.getEmail = function(orgname) {
        var email = null;
        if (UserService.currentUser()) {
          email = UserService.currentUser().email;

          if (orgname) {
            org = UserService.getOrganization(orgname);
            if (org) {
              emaiil = org.email;
            }
          }
        }
        return email;
      };

      planService.showSubscribeDialog = function($scope, orgname, planId, callbacks, opt_title, opt_async) {
        if (!Features.BILLING) { return; }

        // If the async parameter is true and this is a browser that does not allow async popup of the
        // Stripe dialog (such as Mobile Safari or IE), show a bootbox to show the dialog instead.
        var isIE = navigator.appName.indexOf("Internet Explorer") != -1;
        var isMobileSafari = navigator.userAgent.match(/(iPod|iPhone|iPad)/) && navigator.userAgent.match(/AppleWebKit/);

        if (opt_async && (isIE || isMobileSafari)) {
          bootbox.dialog({
            "message": "Please click 'Subscribe' to continue",
            "buttons": {
              "subscribe": {
                "label": "Subscribe",
                "className": "btn-primary",
                "callback": function() {
                  planService.showSubscribeDialog($scope, orgname, planId, callbacks, opt_title, false);
                }
              },
              "close": {
                "label": "Cancel",
                "className": "btn-default"
              }
            }
          });          
          return;
        }

        if (callbacks['opening']) {
          callbacks['opening']();
        }

        var submitted = false;
        var submitToken = function(token) {
          if (submitted) { return; }
          submitted = true;

          if (Config.MIXPANEL_KEY) {
            mixpanel.track('plan_subscribe');
          }

          $scope.$apply(function() {
            if (callbacks['started']) {
              callbacks['started']();
            }
            planService.setSubscription(orgname, planId, callbacks['success'], callbacks['failure'], token);
          });
        };

        planService.getPlan(planId, function(planDetails) {
          var email = planService.getEmail(orgname);
          StripeCheckout.open({
            key:         KeyService.stripePublishableKey,
            address:     false,
            email:       email,
            amount:      planDetails.price,
            currency:    'usd',
            name:        'Quay.io ' + planDetails.title + ' Subscription',
            description: 'Up to ' + planDetails.privateRepos + ' private repositories',
            panelLabel:  opt_title || 'Subscribe',
            token:       submitToken,
            image:       'static/img/quay-icon-stripe.png',
            opened:      function() { $scope.$apply(function() { callbacks['opened']() }); },
            closed:      function() { $scope.$apply(function() { callbacks['closed']() }); }
          });
        });
      };

      return planService;
    }]);
  }).
  directive('match', function($parse) {
    return {
      require: 'ngModel',
      link: function(scope, elem, attrs, ctrl) {
        scope.$watch(function() {        
          return $parse(attrs.match)(scope) === ctrl.$modelValue;
        }, function(currentValue) {
          ctrl.$setValidity('mismatch', currentValue);
        });
      }
    };
  }).
  directive('onresize', function ($window, $parse) {
    return function (scope, element, attr) {
      var fn = $parse(attr.onresize);

      var notifyResized = function() {
        scope.$apply(function () {
          fn(scope);
        });
      };

      angular.element($window).on('resize', null, notifyResized);

      scope.$on('$destroy', function() {
        angular.element($window).off('resize', null, notifyResized);
      });
    };
  }).
  config(['$routeProvider', '$locationProvider',
    function($routeProvider, $locationProvider) {
    var title = window.__config['REGISTRY_TITLE'] || 'Quay.io';

    $locationProvider.html5Mode(true);

    // WARNING WARNING WARNING
    // If you add a route here, you must add a corresponding route in thr endpoints/web.py
    // index rule to make sure that deep links directly deep into the app continue to work.
    // WARNING WARNING WARNING
    $routeProvider.
      when('/repository/:namespace/:name', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl,
                                            fixFooter: false, reloadOnSearch: false}).
      when('/repository/:namespace/:name/tag/:tag', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl,
                                                     fixFooter: false}).
      when('/repository/:namespace/:name/image/:image', {templateUrl: '/static/partials/image-view.html', controller: ImageViewCtrl, reloadOnSearch: false}).
      when('/repository/:namespace/:name/admin', {templateUrl: '/static/partials/repo-admin.html', controller:RepoAdminCtrl, reloadOnSearch: false}).
      when('/repository/:namespace/:name/build', {templateUrl: '/static/partials/repo-build.html', controller:RepoBuildCtrl, reloadOnSearch: false}).
      when('/repository/:namespace/:name/build/:buildid/buildpack', {templateUrl: '/static/partials/build-package.html', controller:BuildPackageCtrl, reloadOnSearch: false}).
      when('/repository/', {title: 'Repositories', description: 'Public and private docker repositories list',
                            templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl, reloadOnSearch: false}).
      when('/user/', {title: 'Account Settings', description:'Account settings for ' + title, templateUrl: '/static/partials/user-admin.html',
                      reloadOnSearch: false, controller: UserAdminCtrl}).
      when('/superuser/', {title: 'Superuser Admin Panel', description:'Admin panel for ' + title, templateUrl: '/static/partials/super-user.html',
                           reloadOnSearch: false, controller: SuperUserAdminCtrl}).
      when('/guide/', {title: 'Guide', description:'Guide to using private docker repositories on ' + title,
                       templateUrl: '/static/partials/guide.html',
                       controller: GuideCtrl}).
      when('/tutorial/', {title: 'Tutorial', description:'Interactive tutorial for using ' + title, templateUrl: '/static/partials/tutorial.html',
                          controller: TutorialCtrl}).
      when('/contact/', {title: 'Contact Us', description:'Different ways for you to get a hold of us when you need us most.', templateUrl: '/static/partials/contact.html',
                       controller: ContactCtrl}).
      when('/about/', {title: 'About Us', description:'Information about the Quay.io team and the company.', templateUrl: '/static/partials/about.html'}).
      when('/plans/', {title: 'Plans and Pricing', description: 'Plans and pricing for private docker repositories on Quay.io',
                       templateUrl: '/static/partials/plans.html', controller: PlansCtrl}).
      when('/security/', {title: 'Security', description: 'Security features used when transmitting and storing data',
                       templateUrl: '/static/partials/security.html'}).
      when('/signin/', {title: 'Sign In', description: 'Sign into ' + title, templateUrl: '/static/partials/signin.html', controller: SignInCtrl, reloadOnSearch: false}).
      when('/new/', {title: 'Create new repository', description: 'Create a new public or private docker repository, optionally constructing from a dockerfile',
                     templateUrl: '/static/partials/new-repo.html', controller: NewRepoCtrl}).
      when('/organizations/', {title: 'Organizations', description: 'Private docker repository hosting for businesses and organizations',
                                 templateUrl: '/static/partials/organizations.html', controller: OrgsCtrl}).
      when('/organizations/new/', {title: 'New Organization', description: 'Create a new organization on ' + title,
                                   templateUrl: '/static/partials/new-organization.html', controller: NewOrgCtrl}).
      when('/organization/:orgname', {templateUrl: '/static/partials/org-view.html', controller: OrgViewCtrl}).
      when('/organization/:orgname/admin', {templateUrl: '/static/partials/org-admin.html', controller: OrgAdminCtrl, reloadOnSearch: false}).
      when('/organization/:orgname/teams/:teamname', {templateUrl: '/static/partials/team-view.html', controller: TeamViewCtrl}).
      when('/organization/:orgname/logs/:membername', {templateUrl: '/static/partials/org-member-logs.html', controller: OrgMemberLogsCtrl}).
      when('/organization/:orgname/application/:clientid', {templateUrl: '/static/partials/manage-application.html',
                                                            controller: ManageApplicationCtrl, reloadOnSearch: false}).
      when('/v1/', {title: 'Activation information', templateUrl: '/static/partials/v1-page.html', controller: V1Ctrl}).


      when('/tour/', {title: title + ' Tour', templateUrl: '/static/partials/tour.html', controller: TourCtrl}).
      when('/tour/organizations', {title: 'Teams and Organizations Tour', templateUrl: '/static/partials/tour.html', controller: TourCtrl}).
      when('/tour/features', {title: title + ' Features', templateUrl: '/static/partials/tour.html', controller: TourCtrl}).
      when('/tour/enterprise', {title: 'Enterprise Edition', templateUrl: '/static/partials/tour.html', controller: TourCtrl}).

      when('/confirminvite', {title: 'Confirm Invite', templateUrl: '/static/partials/confirm-invite.html', controller: ConfirmInviteCtrl, reloadOnSearch: false}).

      when('/', {title: 'Hosted Private Docker Registry', templateUrl: '/static/partials/landing.html', controller: LandingCtrl,
                 pageClass: 'landing-page'}).
      otherwise({redirectTo: '/'});
  }]).
  config(function(RestangularProvider) {
    RestangularProvider.setBaseUrl('/api/v1/');
  });

  if (window.__config && window.__config.MIXPANEL_KEY) {
    quayApp.config(['$analyticsProvider', function($analyticsProvider) {
      $analyticsProvider.virtualPageviews(true);      
    }]);
  }

  if (window.__config && window.__config.SENTRY_PUBLIC_DSN) {
    quayApp.config(function($provide) {
      $provide.decorator("$exceptionHandler", function($delegate) {
        return function(ex, cause) {
          $delegate(ex, cause);
          Raven.captureException(ex, {extra: {cause: cause}});
        };
      });
    });
  }


function buildConditionalLinker($animate, name, evaluator) {
  // Based off of a solution found here: http://stackoverflow.com/questions/20325480/angularjs-whats-the-best-practice-to-add-ngif-to-a-directive-programmatically
  return function ($scope, $element, $attr, ctrl, $transclude) {
    var block;
    var childScope;
    var roles;

    $attr.$observe(name, function (value) {
      if (evaluator($scope.$eval(value))) {
        if (!childScope) {
          childScope = $scope.$new();
          $transclude(childScope, function (clone) {
            block = {
              startNode: clone[0],
              endNode: clone[clone.length++] = document.createComment(' end ' + name + ': ' + $attr[name] + ' ')
            };
            $animate.enter(clone, $element.parent(), $element);
          });
        }
      } else {
        if (childScope) {
          childScope.$destroy();
          childScope = null;
        }

        if (block) {
          $animate.leave(getBlockElements(block));
          block = null;
        }
      }
    });
  }
}

quayApp.directive('quayRequire', function ($animate, Features) {
  return {
    transclude: 'element',
    priority: 600,
    terminal: true,
    restrict: 'A',
    link: buildConditionalLinker($animate, 'quayRequire', function(value) {
      return Features.matchesFeatures(value);
    })
  };
});


quayApp.directive('quayShow', function($animate, Features, Config) {
  return {
    priority: 590,
    restrict: 'A',
    link: function($scope, $element, $attr, ctrl, $transclude) {
      $scope.Features = Features;
      $scope.Config = Config;
      $scope.$watch($attr.quayShow, function(result) {
        $animate[!!result ? 'removeClass' : 'addClass']($element, 'ng-hide');
      });
    }
  };
});


quayApp.directive('ngIfMedia', function ($animate) {
  return {
    transclude: 'element',
    priority: 600,
    terminal: true,
    restrict: 'A',
    link: buildConditionalLinker($animate, 'ngIfMedia', function(value) {
      return window.matchMedia(value).matches;
    })
  };
});


quayApp.directive('quaySection', function($animate, $location, $rootScope) {
  return {
    priority: 590,
    restrict: 'A',
    link: function($scope, $element, $attr, ctrl, $transclude) {
      var update = function() {
        var result = $location.path().indexOf('/' + $attr.quaySection) == 0;
        $animate[!result ? 'removeClass' : 'addClass']($element, 'active');
      };

      $scope.$watch(function(){
        return $location.path();
      }, update);

      $scope.$watch($attr.quaySection, update);
    }
  };
});


quayApp.directive('quayClasses', function(Features, Config) {
  return {
    priority: 580,
    restrict: 'A',
    link: function($scope, $element, $attr, ctrl, $transclude) {
      
      // Borrowed from ngClass.
      function flattenClasses(classVal) {
        if(angular.isArray(classVal)) {
          return classVal.join(' ');
        } else if (angular.isObject(classVal)) {
          var classes = [], i = 0;
          angular.forEach(classVal, function(v, k) {
            if (v) {
              classes.push(k);
            }
          });
          return classes.join(' ');
        }
        
        return classVal;
      }
      
      function removeClass(classVal) {
        $attr.$removeClass(flattenClasses(classVal));
      }


      function addClass(classVal) {
        $attr.$addClass(flattenClasses(classVal));
      }

      $scope.$watch($attr.quayClasses, function(result) {
        var scopeVals = {
          'Features': Features,
          'Config': Config
        };

        for (var expr in result) {
          if (!result.hasOwnProperty(expr)) { continue; }

          // Evaluate the expression with the entire features list added.
          var value = $scope.$eval(expr, scopeVals);
          if (value) {
            addClass(result[expr]);
          } else {
            removeClass(result[expr]);
          }
        }
      });
    }
  };
});


quayApp.directive('quayInclude', function($compile, $templateCache, $http, Features, Config) {
  return {
    priority: 595,
    restrict: 'A',
    link: function($scope, $element, $attr, ctrl) {
      var getTemplate = function(templateName) {
        var templateUrl = '/static/partials/' + templateName;
        return $http.get(templateUrl, {cache: $templateCache});
      };

      var result = $scope.$eval($attr.quayInclude);
      if (!result) {
        return;
      }

      var scopeVals = {
        'Features': Features,
        'Config': Config
      };

      var templatePath = null;
      for (var expr in result) {
        if (!result.hasOwnProperty(expr)) { continue; }

        // Evaluate the expression with the entire features list added.
        var value = $scope.$eval(expr, scopeVals);
        if (value) {
          templatePath = result[expr];
          break;
        }
      }

      if (!templatePath) {
        return;
      }
      
      var promise = getTemplate(templatePath).success(function(html) {
        $element.html(html);
      }).then(function (response) {
        $element.replaceWith($compile($element.html())($scope));
        if ($attr.onload) {
          $scope.$eval($attr.onload);
        }
      });
    }
  };
});


quayApp.directive('entityReference', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/entity-reference.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'entity': '=entity',
      'namespace': '=namespace',
      'showGravatar': '@showGravatar',
      'gravatarSize': '@gravatarSize'
    },
    controller: function($scope, $element, UserService, UtilService) {
      $scope.getIsAdmin = function(namespace) {
        return UserService.isNamespaceAdmin(namespace);
      };

      $scope.getRobotUrl = function(name) {
        var namespace = $scope.getPrefix(name);
        if (!namespace) {
          return '';
        }

        if (!$scope.getIsAdmin(namespace)) {
          return '';
        }

        var org = UserService.getOrganization(namespace);
        if (!org) {
          // This robot is owned by the user.
          return '/user/?tab=robots&showRobot=' + UtilService.textToSafeHtml(name);
        }

        return '/organization/' + org['name'] + '/admin?tab=robots&showRobot=' + UtilService.textToSafeHtml(name);
      };

      $scope.getPrefix = function(name) {
        if (!name) { return ''; }
        var plus = name.indexOf('+');
        return name.substr(0, plus);
      };

      $scope.getShortenedName = function(name) {
        if (!name) { return ''; }
        var plus = name.indexOf('+');
        return name.substr(plus + 1);
      };
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('applicationInfo', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/application-info.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'application': '=application'
    },
    controller: function($scope, $element, ApiService) {
      
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('applicationReference', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/application-reference.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'title': '=title',
      'clientId': '=clientId'
    },
    controller: function($scope, $element, ApiService, $modal) {
      $scope.showAppDetails = function() {
        var params = {
          'client_id': $scope.clientId
        };

        ApiService.getApplicationInformation(null, params).then(function(resp) {
          $scope.applicationInfo = resp;
          $modal({
            title: 'Application Information',
            scope: $scope,
            template: '/static/directives/application-reference-dialog.html',
            show: true
          });
        }, ApiService.errorDisplay('Application could not be found'));
      };
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('markdownView', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/markdown-view.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'content': '=content',
      'firstLineOnly': '=firstLineOnly'
    },
    controller: function($scope, $element, $sce) {
      $scope.getMarkedDown = function(content, firstLineOnly) {
        if (firstLineOnly) {
          content = getFirstTextLine(content);
        }
        return $sce.trustAsHtml(getMarkedDown(content));
      };
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('repoBreadcrumb', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/repo-breadcrumb.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'repo': '=repo',
      'image': '=image',
      'subsection': '=subsection',
      'subsectionIcon': '=subsectionIcon'
    },
    controller: function($scope, $element) {     
    }
  };
  return directiveDefinitionObject;
});

quayApp.directive('focusablePopoverContent', ['$timeout', '$popover', function ($timeout, $popover) {
  return {
    restrict: "A",
    link: function (scope, element, attrs) {
      $body = $('body');
      var hide = function() {
        $body.off('click');
        scope.$apply(function() {
          scope.$hide();
        });
      };

      scope.$on('$destroy', function() {
        $body.off('click');        
      });

      $timeout(function() {
        $body.on('click', function(evt) {
          var target = evt.target;
          var isPanelMember = $(element).has(target).length > 0 || target == element;
          if (!isPanelMember) {
            hide();
          }
        });

        $(element).find('input').focus();
      }, 100);
    }
  };
}]);

quayApp.directive('repoCircle', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/repo-circle.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'repo': '=repo'
    },
    controller: function($scope, $element) {
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('copyBox', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/copy-box.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'value': '=value',
      'hoveringMessage': '=hoveringMessage'
    },
    controller: function($scope, $element, $rootScope) {
      $scope.disabled = false;

      var number = $rootScope.__copyBoxIdCounter || 0;
      $rootScope.__copyBoxIdCounter = number + 1;
      $scope.inputId = "copy-box-input-" + number;
      
      var button = $($element).find('.input-group-addon');
      var input = $($element).find('input');

      input.attr('id', $scope.inputId);
      button.attr('data-clipboard-target', $scope.inputId);
      $scope.disabled = !button.clipboardCopy();
    }
  };
  return directiveDefinitionObject;
});



quayApp.directive('userSetup', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/user-setup.html',
    replace: false,
    transclude: true,
    restrict: 'C',
    scope: {
      'redirectUrl': '=redirectUrl',

      'inviteCode': '=inviteCode',

      'signInStarted': '&signInStarted',
      'signedIn': '&signedIn',
      'userRegistered': '&userRegistered'
    },
    controller: function($scope, $location, $timeout, ApiService, KeyService, UserService) {
      $scope.sendRecovery = function() {
        $scope.sendingRecovery = true;

        ApiService.requestRecoveryEmail($scope.recovery).then(function() {
          $scope.invalidRecovery = false;
          $scope.errorMessage = '';
          $scope.sent = true;
          $scope.sendingRecovery = false;
        }, function(result) {
          $scope.invalidRecovery = true;
          $scope.errorMessage = result.data;
          $scope.sent = false;
          $scope.sendingRecovery = false;
        });
      };

      $scope.handleUserRegistered = function(username) {
        $scope.userRegistered({'username': username});
      };

      $scope.hasSignedIn = function() {
        return UserService.hasEverLoggedIn();
      };
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('externalLoginButton', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/external-login-button.html',
    replace: false,
    transclude: true,
    restrict: 'C',
    scope: {
      'signInStarted': '&signInStarted',
      'redirectUrl': '=redirectUrl',
      'provider': '@provider',
      'action': '@action'
    },
    controller: function($scope, $timeout, $interval, ApiService, KeyService, CookieService, Features, Config) {
      $scope.signingIn = false;
      $scope.startSignin = function(service) {
        $scope.signInStarted({'service': service});

        var url = KeyService.getExternalLoginUrl(service, $scope.action || 'login');
        
        // Save the redirect URL in a cookie so that we can redirect back after the service returns to us.
        var redirectURL = $scope.redirectUrl || window.location.toString();
        CookieService.putPermanent('quay.redirectAfterLoad', redirectURL);

        // Needed to ensure that UI work done by the started callback is finished before the location
        // changes.
        $scope.signingIn = true;
        $timeout(function() {
          document.location = url;
        }, 250);
      };
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('signinForm', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/signin-form.html',
    replace: false,
    transclude: true,
    restrict: 'C',
    scope: {
      'redirectUrl': '=redirectUrl',
      'signInStarted': '&signInStarted',
      'signedIn': '&signedIn'
    },
    controller: function($scope, $location, $timeout, $interval, ApiService, KeyService, UserService, CookieService, Features, Config) {
      $scope.tryAgainSoon = 0;
      $scope.tryAgainInterval = null;
      $scope.signingIn = false;

      $scope.markStarted = function() {
       $scope.signingIn = true;
       if ($scope.signInStarted != null) {
         $scope.signInStarted();
       }
      };

      $scope.cancelInterval = function() {
        $scope.tryAgainSoon = 0;

        if ($scope.tryAgainInterval) {
          $interval.cancel($scope.tryAgainInterval);
        }

        $scope.tryAgainInterval = null;
      };

      $scope.$watch('user.username', function() {
        $scope.cancelInterval();
      });

      $scope.$on('$destroy', function() {
        $scope.cancelInterval();
      });

      $scope.signin = function() {
        if ($scope.tryAgainSoon > 0) { return; }

        $scope.markStarted();
        $scope.cancelInterval();

        ApiService.signinUser($scope.user).then(function() {
          $scope.signingIn = false;
          $scope.needsEmailVerification = false;
          $scope.invalidCredentials = false;

          if ($scope.signedIn != null) {
            $scope.signedIn();
          }

          // Load the newly created user.
          UserService.load();

          // Redirect to the specified page or the landing page
          // Note: The timeout of 500ms is needed to ensure dialogs containing sign in
          // forms get removed before the location changes.
          $timeout(function() {
            var redirectUrl = $scope.redirectUrl;
            if (redirectUrl == $location.path() || redirectUrl == null) {
              return;
            }
            window.location = (redirectUrl ? redirectUrl : '/');
          }, 500);
        }, function(result) {
          $scope.signingIn = false;

          if (result.status == 429 /* try again later */) {
            $scope.needsEmailVerification = false;
            $scope.invalidCredentials = false;

            $scope.cancelInterval();

            $scope.tryAgainSoon = result.headers('Retry-After');
            $scope.tryAgainInterval = $interval(function() {              
              $scope.tryAgainSoon--;
              if ($scope.tryAgainSoon <= 0) {
                $scope.cancelInterval();
              }
            }, 1000, $scope.tryAgainSoon);
          } else {
            $scope.needsEmailVerification = result.data.needsEmailVerification;
            $scope.invalidCredentials = result.data.invalidCredentials;
          }
        });
      };
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('signupForm', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/signup-form.html',
    replace: false,
    transclude: true,
    restrict: 'C',
    scope: {
      'inviteCode': '=inviteCode',

      'userRegistered': '&userRegistered'
    },
    controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, Config, UIService) {   
      $('.form-signup').popover();

      $scope.awaitingConfirmation = false;      
      $scope.registering = false;

      $scope.register = function() {
        UIService.hidePopover('#signupButton');
        $scope.registering = true;

        if ($scope.inviteCode) {
          $scope.newUser['invite_code'] = $scope.inviteCode;
        }

        ApiService.createNewUser($scope.newUser).then(function(resp) {
          $scope.registering  = false;
          $scope.awaitingConfirmation = !!resp['awaiting_verification'];
          
          if (Config.MIXPANEL_KEY) {
            mixpanel.alias($scope.newUser.username);
          }

          $scope.userRegistered({'username': $scope.newUser.username});

          if (!$scope.awaitingConfirmation) {
            document.location = '/';
          }
        }, function(result) {
          $scope.registering  = false;
          UIService.showFormError('#signupButton', result);
        });
      };
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('tourContent', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/tour-content.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'kind': '=kind'
    },
    controller: function($scope, $element, $timeout, UserService) {
      // Monitor any user changes and place the current user into the scope.
      UserService.updateUserIn($scope);

      $scope.chromify = function() {
        browserchrome.update();
      };

      $scope.$watch('kind', function(kind) {
        $timeout(function() {
          $scope.chromify();
        });
      });
    },
    link: function($scope, $element, $attr, ctrl) {
      $scope.chromify();
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('plansTable', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/plans-table.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'plans': '=plans',
      'currentPlan': '=currentPlan'
    },
    controller: function($scope, $element) {
      $scope.setPlan = function(plan) {
        $scope.currentPlan = plan;
      };
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('dockerAuthDialog', function (Config) {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/docker-auth-dialog.html',
    replace: false,
    transclude: true,
    restrict: 'C',
    scope: {
      'username': '=username',
      'token': '=token',
      'shown': '=shown',
      'counter': '=counter',
      'supportsRegenerate': '@supportsRegenerate',
      'regenerate': '&regenerate'
    },
    controller: function($scope, $element) {
      var updateCommand = function() {
        var escape = function(v) {
          if (!v) { return v; }
          return v.replace('$', '\\$');
        };
        $scope.command = 'docker login -e="." -u="' + escape($scope.username) +
          '" -p="' + $scope.token + '" ' + Config['SERVER_HOSTNAME'];
      };

      $scope.$watch('username', updateCommand);
      $scope.$watch('token', updateCommand);

      $scope.regenerating = true;

      $scope.askRegenerate = function() {
        bootbox.confirm('Are you sure you want to regenerate the token? All existing login credentials will become invalid', function(resp) {
          if (resp) {
            $scope.regenerating = true;
            $scope.regenerate({'username': $scope.username, 'token': $scope.token});
          }
        });
      };

      $scope.isDownloadSupported = function() {
        var isSafari = /^((?!chrome).)*safari/i.test(navigator.userAgent);
        if (isSafari) {
          // Doesn't work properly in Safari, sadly.
          return false;
        }

        try { return !!new Blob(); } catch(e) {}
        return false;
      };

      $scope.downloadCfg = function() {
        var auth = $.base64.encode($scope.username + ":" + $scope.token);
        config = {}
        config[Config['SERVER_HOSTNAME']] = {
          "auth": auth,
          "email": ""
        };

        var file = JSON.stringify(config, null, ' ');
        var blob = new Blob([file]);
        saveAs(blob, '.dockercfg');
      };

      var show = function(r) {
        $scope.regenerating = false;

        if (!$scope.shown || !$scope.username || !$scope.token) {
          $('#dockerauthmodal').modal('hide');
          return;
        }
         
        $('#copyClipboard').clipboardCopy();
        $('#dockerauthmodal').modal({});
      };

      $scope.$watch('counter', show);
      $scope.$watch('shown', show);
      $scope.$watch('username', show);
      $scope.$watch('token', show);
    }
  };
  return directiveDefinitionObject;
});


quayApp.filter('reverse', function() {
  return function(items) {
    return items.slice().reverse();
  };
});


quayApp.filter('bytes', function() {
  return function(bytes, precision) {
    if (!bytes || isNaN(parseFloat(bytes)) || !isFinite(bytes)) return 'Unknown';
    if (typeof precision === 'undefined') precision = 1;
    var units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
    number = Math.floor(Math.log(bytes) / Math.log(1024));
    return (bytes / Math.pow(1024, Math.floor(number))).toFixed(precision) + ' ' + units[number];
  }
});


quayApp.filter('visibleLogFilter', function () {
  return function (logs, allowed) {
    if (!allowed) {
      return logs;
    }
    
    var filtered = [];
    angular.forEach(logs, function (log) {
      if (allowed[log.kind]) {
        filtered.push(log);
      }
    });

    return filtered;
  };
});


quayApp.directive('billingInvoices', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/billing-invoices.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'organization': '=organization',
      'user': '=user',
      'makevisible': '=makevisible'
    },
    controller: function($scope, $element, $sce, ApiService) {
      $scope.loading = false;
      $scope.invoiceExpanded = {};

      $scope.toggleInvoice = function(id) {
        $scope.invoiceExpanded[id] = !$scope.invoiceExpanded[id];
      };

      var update = function() {
        var hasValidUser = !!$scope.user;
        var hasValidOrg = !!$scope.organization;
        var isValid = hasValidUser || hasValidOrg;

        if (!$scope.makevisible || !isValid) {
          return;
        }
        
        $scope.loading = true;

        ApiService.listInvoices($scope.organization).then(function(resp) {
          $scope.invoices = resp.invoices;
          $scope.loading = false;
        });
      };

      $scope.$watch('organization', update);
      $scope.$watch('user', update);
      $scope.$watch('makevisible', update);
    }
  };

  return directiveDefinitionObject;
});


quayApp.directive('logsView', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/logs-view.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'organization': '=organization',
      'user': '=user',
      'makevisible': '=makevisible',
      'repository': '=repository',
      'performer': '=performer',
      'allLogs': '@allLogs'
    },
    controller: function($scope, $element, $sce, Restangular, ApiService, TriggerService,
                         StringBuilderService, ExternalNotificationData) {
      $scope.loading = true;
      $scope.logs = null;
      $scope.kindsAllowed = null;
      $scope.chartVisible = true;
      $scope.logsPath = '';
      
      var datetime = new Date();
      $scope.logStartDate = new Date(datetime.getUTCFullYear(), datetime.getUTCMonth(), datetime.getUTCDate() - 7);
      $scope.logEndDate = new Date(datetime.getUTCFullYear(), datetime.getUTCMonth(), datetime.getUTCDate());

      var defaultPermSuffix = function(metadata) {
        if (metadata.activating_username) {
          return ', when creating user is {activating_username}';
        }
        return '';
      };

      var logDescriptions = {
          'account_change_plan': 'Change plan',
          'account_change_cc': 'Update credit card',
          'account_change_password': 'Change password',
          'account_convert': 'Convert account to organization',
          'create_robot': 'Create Robot Account: {robot}',
          'delete_robot': 'Delete Robot Account: {robot}',
          'create_repo': 'Create Repository: {repo}',
          'push_repo': 'Push to repository: {repo}',
          'pull_repo': function(metadata) {
            if (metadata.token) {
              return 'Pull repository {repo} via token {token}';
            } else if (metadata.username) {
              return 'Pull repository {repo} by {username}';
            } else {
              return 'Public pull of repository {repo} by {_ip}';
            }
          },
          'delete_repo': 'Delete repository: {repo}',
          'change_repo_permission': function(metadata) {
            if (metadata.username) {
              return 'Change permission for user {username} in repository {repo} to {role}';
            } else if (metadata.team) {
                return 'Change permission for team {team} in repository {repo} to {role}';
            } else if (metadata.token) {
              return 'Change permission for token {token} in repository {repo} to {role}';
            }
          },
          'delete_repo_permission': function(metadata) {
            if (metadata.username) {
              return 'Remove permission for user {username} from repository {repo}';
            } else if (metadata.team) {
                return 'Remove permission for team {team} from repository {repo}';
            } else if (metadata.token) {
              return 'Remove permission for token {token} from repository {repo}';
            }
          },
          'delete_tag': 'Tag {tag} deleted in repository {repo} by user {username}',
          'create_tag': 'Tag {tag} created in repository {repo} on image {image} by user {username}',
          'move_tag': 'Tag {tag} moved from image {original_image} to image {image} in repository {repo} by user {username}',
          'change_repo_visibility': 'Change visibility for repository {repo} to {visibility}',
          'add_repo_accesstoken': 'Create access token {token} in repository {repo}',
          'delete_repo_accesstoken': 'Delete access token {token} in repository {repo}',
          'set_repo_description': 'Change description for repository {repo}: {description}',
          'build_dockerfile': function(metadata) {
            if (metadata.trigger_id) {
              var triggerDescription = TriggerService.getDescription(
                metadata['service'], metadata['config']);
              return 'Build image from Dockerfile for repository {repo} triggered by ' + triggerDescription;
            }
            return 'Build image from Dockerfile for repository {repo}';
          },
          'org_create_team': 'Create team: {team}',
          'org_delete_team': 'Delete team: {team}',
          'org_add_team_member': 'Add member {member} to team {team}',
          'org_remove_team_member': 'Remove member {member} from team {team}',
          'org_invite_team_member': function(metadata) {
            if (metadata.user) {
              return 'Invite {user} to team {team}';
            } else {
              return 'Invite {email} to team {team}';
            }
          },
          'org_delete_team_member_invite': function(metadata) {
            if (metadata.user) {
              return 'Rescind invite of {user} to team {team}';
            } else {
              return 'Rescind invite of {email} to team {team}';
            }
          },

          'org_team_member_invite_accepted': 'User {member}, invited by {inviter}, joined team {team}',
          'org_team_member_invite_declined': 'User {member}, invited by {inviter}, declined to join team {team}',

          'org_set_team_description': 'Change description of team {team}: {description}',
          'org_set_team_role': 'Change permission of team {team} to {role}',
          'create_prototype_permission': function(metadata) {
            if (metadata.delegate_user) {
              return 'Create default permission: {role} for {delegate_user}' + defaultPermSuffix(metadata);
            } else if (metadata.delegate_team) {
              return 'Create default permission: {role} for {delegate_team}' + defaultPermSuffix(metadata);
            }
          },
          'modify_prototype_permission': function(metadata) {              
            if (metadata.delegate_user) {
              return 'Modify default permission: {role} (from {original_role}) for {delegate_user}' + defaultPermSuffix(metadata);
            } else if (metadata.delegate_team) {
              return 'Modify default permission: {role} (from {original_role}) for {delegate_team}' + defaultPermSuffix(metadata);
            }
          },
          'delete_prototype_permission': function(metadata) {
            if (metadata.delegate_user) {
              return 'Delete default permission: {role} for {delegate_user}' + defaultPermSuffix(metadata);
            } else if (metadata.delegate_team) {
              return 'Delete default permission: {role} for {delegate_team}' + defaultPermSuffix(metadata);
            }
          },
          'setup_repo_trigger': function(metadata) {
            var triggerDescription = TriggerService.getDescription(
              metadata['service'], metadata['config']);
            return 'Setup build trigger - ' + triggerDescription;
          },
          'delete_repo_trigger': function(metadata) {
            var triggerDescription = TriggerService.getDescription(
              metadata['service'], metadata['config']);
            return 'Delete build trigger - ' + triggerDescription;
          },
          'create_application': 'Create application {application_name} with client ID {client_id}',
          'update_application': 'Update application to {application_name} for client ID {client_id}',
          'delete_application': 'Delete application {application_name} with client ID {client_id}',
          'reset_application_client_secret': 'Reset the Client Secret of application {application_name} ' +
            'with client ID {client_id}',

          'add_repo_notification': function(metadata) {
            var eventData = ExternalNotificationData.getEventInfo(metadata.event);
            return 'Add notification of event "' + eventData['title'] + '" for repository {repo}';
          },

          'delete_repo_notification': function(metadata) {
            var eventData = ExternalNotificationData.getEventInfo(metadata.event);
            return 'Delete notification of event "' + eventData['title'] + '" for repository {repo}';
          },

          'regenerate_robot_token': 'Regenerated token for robot {robot}',

          // Note: These are deprecated.
          'add_repo_webhook': 'Add webhook in repository {repo}',
          'delete_repo_webhook': 'Delete webhook in repository {repo}'
      };

      var logKinds = {
        'account_change_plan': 'Change plan',
        'account_change_cc': 'Update credit card',
        'account_change_password': 'Change password',
        'account_convert': 'Convert account to organization',
        'create_robot': 'Create Robot Account',
        'delete_robot': 'Delete Robot Account',
        'create_repo': 'Create Repository',
        'push_repo': 'Push to repository',
        'pull_repo': 'Pull repository',
        'delete_repo': 'Delete repository',
        'change_repo_permission': 'Change repository permission',
        'delete_repo_permission': 'Remove user permission from repository',
        'change_repo_visibility': 'Change repository visibility',
        'add_repo_accesstoken': 'Create access token',
        'delete_repo_accesstoken': 'Delete access token',
        'set_repo_description': 'Change repository description',
        'build_dockerfile': 'Build image from Dockerfile',
        'delete_tag': 'Delete Tag',
        'create_tag': 'Create Tag',
        'move_tag': 'Move Tag',
        'org_create_team': 'Create team',
        'org_delete_team': 'Delete team',
        'org_add_team_member': 'Add team member',
        'org_invite_team_member': 'Invite team member',
        'org_delete_team_member_invite': 'Rescind team member invitation',
        'org_remove_team_member': 'Remove team member',
        'org_team_member_invite_accepted': 'Team invite accepted',
        'org_team_member_invite_declined': 'Team invite declined',
        'org_set_team_description': 'Change team description',
        'org_set_team_role': 'Change team permission',
        'create_prototype_permission': 'Create default permission',
        'modify_prototype_permission': 'Modify default permission',
        'delete_prototype_permission': 'Delete default permission',
        'setup_repo_trigger': 'Setup build trigger',
        'delete_repo_trigger': 'Delete build trigger',
        'create_application': 'Create Application',
        'update_application': 'Update Application',
        'delete_application': 'Delete Application',
        'reset_application_client_secret': 'Reset Client Secret',
        'add_repo_notification': 'Add repository notification',
        'delete_repo_notification': 'Delete repository notification',
        'regenerate_robot_token': 'Regenerate Robot Token',

        // Note: these are deprecated.
        'add_repo_webhook': 'Add webhook',
        'delete_repo_webhook': 'Delete webhook'
      };

      var getDateString = function(date) {
        return (date.getMonth() + 1) + '/' + date.getDate() + '/' + date.getFullYear();
      };

      var getOffsetDate = function(date, days) {
        return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days);
      };

      var update = function() {
        var hasValidUser = !!$scope.user;
        var hasValidOrg = !!$scope.organization;
        var hasValidRepo = $scope.repository && $scope.repository.namespace;
        var isValid = hasValidUser || hasValidOrg || hasValidRepo || $scope.allLogs;

        if (!$scope.makevisible || !isValid) {
          return;
        }

        var twoWeeksAgo = getOffsetDate($scope.logEndDate, -14);
        if ($scope.logStartDate > $scope.logEndDate || $scope.logStartDate < twoWeeksAgo) {
          $scope.logStartDate = twoWeeksAgo;
        }

        $scope.loading = true;

        // Note: We construct the URLs here manually because we also use it for the download
        // path.
        var url = getRestUrl('user/logs');
        if ($scope.organization) {
          url = getRestUrl('organization', $scope.organization.name, 'logs');
        }
        if ($scope.repository) {
          url = getRestUrl('repository', $scope.repository.namespace, $scope.repository.name, 'logs');
        }

        if ($scope.allLogs) {
          url = getRestUrl('superuser', 'logs')
        }

        url += '?starttime=' + encodeURIComponent(getDateString($scope.logStartDate));
        url += '&endtime=' + encodeURIComponent(getDateString($scope.logEndDate));

        if ($scope.performer) {
          url += '&performer=' + encodeURIComponent($scope.performer.name);
        }

        var loadLogs = Restangular.one(url);
        loadLogs.customGET().then(function(resp) {
          $scope.logsPath = '/api/v1/' + url;

          if (!$scope.chart) {
            window.console.log('creating chart');
            $scope.chart = new LogUsageChart(logKinds);
            $($scope.chart).bind('filteringChanged', function(e) {
              $scope.$apply(function() { $scope.kindsAllowed = e.allowed; });
            });
           }

          $scope.chart.draw('bar-chart', resp.logs, $scope.logStartDate, $scope.logEndDate);
          $scope.kindsAllowed = null;
          $scope.logs = resp.logs;
          $scope.loading = false;
        });
      };

      $scope.toggleChart = function() {
        $scope.chartVisible = !$scope.chartVisible;
      };

      $scope.isVisible = function(allowed, kind) {
        return allowed == null || allowed.hasOwnProperty(kind);
      };

      $scope.getColor = function(kind) {
        return $scope.chart.getColor(kind);
      };

      $scope.getDescription = function(log) {       
        log.metadata['_ip'] = log.ip ? log.ip : null;
        return StringBuilderService.buildString(logDescriptions[log.kind] || log.kind, log.metadata);
      };

      $scope.$watch('organization', update);
      $scope.$watch('user', update);
      $scope.$watch('repository', update);
      $scope.$watch('makevisible', update);
      $scope.$watch('performer', update);
      $scope.$watch('logStartDate', update);
      $scope.$watch('logEndDate', update);
    }
  };

  return directiveDefinitionObject;
});


quayApp.directive('applicationManager', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/application-manager.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'organization': '=organization',
      'makevisible': '=makevisible'
    },
    controller: function($scope, $element, ApiService) {      
      $scope.loading = false;
      $scope.applications = [];
     
      $scope.createApplication = function(appName) {
        if (!appName) { return; }

        var params = {
          'orgname': $scope.organization.name
        };

        var data = {
          'name': appName
        };

        ApiService.createOrganizationApplication(data, params).then(function(resp) {
          $scope.applications.push(resp);
        }, ApiService.errorDisplay('Cannot create application'));
      };

      var update = function() {
        if (!$scope.organization || !$scope.makevisible) { return; }
        if ($scope.loading) { return; }

        $scope.loading = true;

        var params = {
          'orgname': $scope.organization.name
        };

        ApiService.getOrganizationApplications(null, params).then(function(resp) {
          $scope.loading = false;
          $scope.applications = resp['applications'] || [];
        });
      };

      $scope.$watch('organization', update);
      $scope.$watch('makevisible', update);
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('robotsManager', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/robots-manager.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'organization': '=organization',
      'user': '=user'
    },
    controller: function($scope, $element, ApiService, $routeParams) {
      $scope.ROBOT_PATTERN = ROBOT_PATTERN;
      $scope.robots = null;
      $scope.loading = false;
      $scope.shownRobot = null;
      $scope.showRobotCounter = 0;

      $scope.regenerateToken = function(username) {
        if (!username) { return; }

        var shortName = $scope.getShortenedName(username);
        ApiService.regenerateRobotToken($scope.organization, null, {'robot_shortname': shortName}).then(function(updated) {
          var index = $scope.findRobotIndexByName(username);
          if (index >= 0) {
            $scope.robots.splice(index, 1);
            $scope.robots.push(updated);
          }
          $scope.shownRobot = updated;
        }, ApiService.errorDisplay('Cannot regenerate robot account token'));
      };

      $scope.showRobot = function(info) {
        $scope.shownRobot = info;
        $scope.showRobotCounter++;
      };
      
      $scope.findRobotIndexByName = function(name) {
        for (var i = 0; i < $scope.robots.length; ++i) {
          if ($scope.robots[i].name == name) {
            return i;
          }
        }
        return -1;
      };

      $scope.getShortenedName = function(name) {
        var plus = name.indexOf('+');
        return name.substr(plus + 1);
      };

      $scope.getPrefix = function(name) {
        var plus = name.indexOf('+');
        return name.substr(0, plus);
      };

      $scope.createRobot = function(name) {
        if (!name) { return; }

        createRobotAccount(ApiService, !!$scope.organization, $scope.organization ? $scope.organization.name : '', name,
          function(created) {
            $scope.robots.push(created);
          });
      };

      $scope.deleteRobot = function(info) {
        var shortName = $scope.getShortenedName(info.name);
        ApiService.deleteRobot($scope.organization, null, {'robot_shortname': shortName}).then(function(resp) {
          var index = $scope.findRobotIndexByName(info.name);
          if (index >= 0) {
            $scope.robots.splice(index, 1);
          }
        }, ApiService.errorDisplay('Cannot delete robot account'));
      };

      var update = function() {
        if (!$scope.user && !$scope.organization) { return; }
        if ($scope.loading) { return; }

        $scope.loading = true;
        ApiService.getRobots($scope.organization).then(function(resp) {
          $scope.robots = resp.robots;
          $scope.loading = false;

          if ($routeParams.showRobot) {
            var index = $scope.findRobotIndexByName($routeParams.showRobot);
            if (index >= 0) {
              $scope.showRobot($scope.robots[index]);
            }
          }
        });
      };

      $scope.$watch('organization', update);
      $scope.$watch('user', update);
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('prototypeManager', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/prototype-manager.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'organization': '=organization'
    },
    controller: function($scope, $element, ApiService) {
      $scope.loading = false;
      $scope.activatingForNew = null;
      $scope.delegateForNew = null;
      $scope.clearCounter = 0;
      $scope.newForWholeOrg = true;

      $scope.roles = [
        { 'id': 'read', 'title': 'Read', 'kind': 'success' },
        { 'id': 'write', 'title': 'Write', 'kind': 'success' },
        { 'id': 'admin', 'title': 'Admin', 'kind': 'primary' }
      ];

      $scope.setRole = function(role, prototype) {
        var params = {
          'orgname': $scope.organization.name,
          'prototypeid': prototype.id
        };

        var data = {
          'id': prototype.id,
          'role': role
        };

        ApiService.updateOrganizationPrototypePermission(data, params).then(function(resp) {
          prototype.role = role;
        }, ApiService.errorDisplay('Cannot modify permission'));
      };

      $scope.comparePrototypes = function(p) {
        return p.activating_user ? p.activating_user.name : ' ';
      };

      $scope.setRoleForNew = function(role) {
        $scope.newRole = role;
      };

      $scope.setNewForWholeOrg = function(value) {
        $scope.newForWholeOrg = value;
      };

      $scope.showAddDialog = function() {
        $scope.activatingForNew = null;
        $scope.delegateForNew = null;
        $scope.newRole = 'read';
        $scope.clearCounter++;
        $scope.newForWholeOrg = true;
        $('#addPermissionDialogModal').modal({});
      };

      $scope.createPrototype = function() {
        $scope.loading = true;

        var params = {
          'orgname': $scope.organization.name
        };

        var data = {
          'delegate': $scope.delegateForNew,
          'role': $scope.newRole
        };

        if (!$scope.newForWholeOrg) {
          data['activating_user'] = $scope.activatingForNew;
        }

        var errorHandler = ApiService.errorDisplay('Cannot create permission',
                                                   function(resp) {
                                                     $('#addPermissionDialogModal').modal('hide');
                                                   });

        ApiService.createOrganizationPrototypePermission(data, params).then(function(resp) {
          $scope.prototypes.push(resp);
          $scope.loading = false;
          $('#addPermissionDialogModal').modal('hide');
        }, errorHandler);
      };

      $scope.deletePrototype = function(prototype) {
        $scope.loading = true;

        var params = {
          'orgname': $scope.organization.name,
          'prototypeid': prototype.id
        };

        ApiService.deleteOrganizationPrototypePermission(null, params).then(function(resp) {
          $scope.prototypes.splice($scope.prototypes.indexOf(prototype), 1);
          $scope.loading = false;
        }, ApiService.errorDisplay('Cannot delete permission'));
      };

      var update = function() {
        if (!$scope.organization) { return; }
        if ($scope.loading) { return; }

        var params = {'orgname': $scope.organization.name};

        $scope.loading = true;
        ApiService.getOrganizationPrototypePermissions(null, params).then(function(resp) {
          $scope.prototypes = resp.prototypes;
          $scope.loading = false;
        });
      };

      $scope.$watch('organization', update);
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('deleteUi', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/delete-ui.html',
    replace: false,
    transclude: true,
    restrict: 'C',
    scope: {
      'deleteTitle': '=deleteTitle',
      'buttonTitle': '=buttonTitle',
      'performDelete': '&performDelete'
    },
    controller: function($scope, $element) {
      $scope.buttonTitleInternal = $scope.buttonTitle || 'Delete';

      $element.children().attr('tabindex', 0);
      $scope.focus = function() {
        $element[0].firstChild.focus();
      };
    }
  };
  return  directiveDefinitionObject;
});


quayApp.directive('popupInputButton', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/popup-input-button.html',
    replace: false,
    transclude: true,
    restrict: 'C',
    scope: {
      'placeholder': '=placeholder',
      'pattern': '=pattern',
      'submitted': '&submitted'
    },
    controller: function($scope, $element) {
      $scope.popupShown = function() {
        setTimeout(function() {
          var box = $('#input-box');
          box[0].value = '';
          box.focus();
        }, 40);
      };

      $scope.getRegexp = function(pattern) {
        if (!pattern) {
          pattern = '.*';
        }
        return new RegExp(pattern);
      };

      $scope.inputSubmit = function() {
        var box = $('#input-box');
        if (box.hasClass('ng-invalid')) { return; }

        var entered = box[0].value;
        if (!entered) {
          return;
        }

        if ($scope.submitted) {
          $scope.submitted({'value': entered});
        }
      };
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('resourceView', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/resource-view.html',
    replace: false,
    transclude: true,
    restrict: 'C',
    scope: {
      'resource': '=resource',
      'errorMessage': '=errorMessage'
    },
    controller: function($scope, $element) {
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('quaySpinner', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/spinner.html',
    replace: false,
    transclude: true,
    restrict: 'C',
    scope: {},
    controller: function($scope, $element) {
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('registryName', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/registry-name.html',
    replace: false,
    transclude: true,
    restrict: 'C',
    scope: {},
    controller: function($scope, $element, Config) {
      $scope.name = Config.REGISTRY_TITLE;
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('organizationHeader', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/organization-header.html',
    replace: false,
    transclude: true,
    restrict: 'C',
    scope: {
      'organization': '=organization',
      'teamName': '=teamName',
      'clickable': '=clickable'
    },
    controller: function($scope, $element) {
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('markdownInput', function () {
  var counter = 0;

  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/markdown-input.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'content': '=content',
      'canWrite': '=canWrite',
      'contentChanged': '=contentChanged',
      'fieldTitle': '=fieldTitle'
    },
    controller: function($scope, $element) {
      var elm = $element[0];

      $scope.id = (counter++);

      $scope.editContent = function() {
        if (!$scope.canWrite) { return; }

        if (!$scope.markdownDescriptionEditor) {
          var converter = Markdown.getSanitizingConverter();
          var editor = new Markdown.Editor(converter, '-description-' + $scope.id);
          editor.run();
          $scope.markdownDescriptionEditor = editor;
        }

        $('#wmd-input-description-' + $scope.id)[0].value = $scope.content;
        $(elm).find('.modal').modal({});
      };

      $scope.saveContent = function() {
        $scope.content = $('#wmd-input-description-' + $scope.id)[0].value;
        $(elm).find('.modal').modal('hide');

        if ($scope.contentChanged) {
          $scope.contentChanged($scope.content);
        }
      };
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('repoSearch', function () {
  var number = 0;
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/repo-search.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
    },
    controller: function($scope, $element, $location, UserService, Restangular) {
      var searchToken = 0;
      $scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
        ++searchToken;
      }, true);
        
      var repoHound = new Bloodhound({
        name: 'repositories',
        remote: {
          url: '/api/v1/find/repository?query=%QUERY',
          replace: function (url, uriEncodedQuery) {
            url = url.replace('%QUERY', uriEncodedQuery);              
            url += '&cb=' + searchToken;
            return url;
          },
          filter: function(data) {
            var datums = [];
            for (var i = 0; i < data.repositories.length; ++i) {
              var repo = data.repositories[i];
              datums.push({
                'value': repo.name,
                'tokens': [repo.name, repo.namespace],
                'repo': repo
              });
            }
            return datums;
          }
        },
        datumTokenizer: function(d) { 
          return Bloodhound.tokenizers.whitespace(d.val); 
        },
        queryTokenizer: Bloodhound.tokenizers.whitespace
      });
      repoHound.initialize();

      var element = $($element[0].childNodes[0]);
      element.typeahead({ 'highlight': true }, {
        source: repoHound.ttAdapter(),       
        templates: {
          'suggestion': function (datum) {
            template = '<div class="repo-mini-listing">';
            template += '<i class="fa fa-hdd-o fa-lg"></i>'
            template += '<span class="name">' + datum.repo.namespace +'/' + datum.repo.name + '</span>'
            if (datum.repo.description) {
              template += '<span class="description">' + getFirstTextLine(datum.repo.description) + '</span>'
            }

            template += '</div>'
            return template;
          }
        }
      });

      element.on('typeahead:selected', function (e, datum) {
        element.typeahead('val', '');
        $scope.$apply(function() {
          $location.path('/repository/' + datum.repo.namespace + '/' + datum.repo.name);
        });
      });
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('headerBar', function () {
  var number = 0;
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/header-bar.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
    },
    controller: function($scope, $element, $location, UserService, PlanService, ApiService, NotificationService) {
      $scope.notificationService = NotificationService;
     
      // Monitor any user changes and place the current user into the scope.
      UserService.updateUserIn($scope);
     
      $scope.signout = function() {
        ApiService.logout().then(function() {
          UserService.load();
          $location.path('/');
        });
      };
        
      $scope.appLinkTarget = function() {
        if ($("div[ng-view]").length === 0) {
            return "_self";
        }
        return "";
      };     
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('entitySearch', function () {
  var number = 0;
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/entity-search.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    require: '?ngModel',
    link: function(scope, element, attr, ctrl) {
      scope.ngModel = ctrl;
    },
    scope: {
      'namespace': '=namespace',
      'placeholder': '=placeholder',

      // Default: ['user', 'team', 'robot']
      'allowedEntities': '=allowedEntities',

      'currentEntity': '=currentEntity',

      'entitySelected': '&entitySelected',
      'emailSelected': '&emailSelected',

      // When set to true, the contents of the control will be cleared as soon
      // as an entity is selected.
      'autoClear': '=autoClear',

      // Set this property to immediately clear the contents of the control.
      'clearValue': '=clearValue',

      // Whether e-mail addresses are allowed.
      'allowEmails': '=allowEmails',
      'emailMessage': '@emailMessage',

      // True if the menu should pull right.
      'pullRight': '@pullRight'
    },
    controller: function($rootScope, $scope, $element, Restangular, UserService, ApiService, UtilService, Config) {
      $scope.lazyLoading = true;

      $scope.teams = null;
      $scope.robots = null;

      $scope.isAdmin = false;
      $scope.isOrganization = false;

      $scope.includeTeams = true;
      $scope.includeRobots = true;
      $scope.includeOrgs = false;

      $scope.currentEntityInternal = $scope.currentEntity;

      var isSupported = function(kind, opt_array) {
        return $.inArray(kind, opt_array || $scope.allowedEntities || ['user', 'team', 'robot']) >= 0;
      };

      $scope.lazyLoad = function() {
        if (!$scope.namespace || !$scope.lazyLoading) { return; }

        // Reset the cached teams and robots.
        $scope.teams = null;
        $scope.robots = null;
        
        // Load the organization's teams (if applicable).
        if ($scope.isOrganization && isSupported('team')) {
          // Note: We load the org here again so that we always have the fully up-to-date
          // teams list.
          ApiService.getOrganization(null, {'orgname': $scope.namespace}).then(function(resp) {
            $scope.teams = resp.teams;
          });
        }

        // Load the user/organization's robots (if applicable).
        if ($scope.isAdmin && isSupported('robot')) {
          ApiService.getRobots($scope.isOrganization ? $scope.namespace : null).then(function(resp) {
            $scope.robots = resp.robots;
            $scope.lazyLoading = false;
          }, function() {
            $scope.lazyLoading = false;
          });
        } else {
          $scope.lazyLoading = false;
        }
      };

      $scope.createTeam = function() {
        if (!$scope.isAdmin) { return; }

        bootbox.prompt('Enter the name of the new team', function(teamname) {
          if (!teamname) { return; }
            
          var regex = new RegExp(TEAM_PATTERN);
          if (!regex.test(teamname)) {
            bootbox.alert('Invalid team name');
            return;
          }

          createOrganizationTeam(ApiService, $scope.namespace, teamname, function(created) {
            $scope.setEntity(created.name, 'team', false);
            $scope.teams[teamname] = created;
          });          
        });
      };

      $scope.createRobot = function() {
        if (!$scope.isAdmin) { return; }

        bootbox.prompt('Enter the name of the new robot account', function(robotname) {
          if (!robotname) { return; }

          var regex = new RegExp(ROBOT_PATTERN);
          if (!regex.test(robotname)) {
            bootbox.alert('Invalid robot account name');
            return;
          }

          createRobotAccount(ApiService, $scope.isOrganization, $scope.namespace, robotname, function(created) {
            $scope.setEntity(created.name, 'user', true);
            $scope.robots.push(created);
          });          
        });
      };

      $scope.setEntity = function(name, kind, is_robot) {
        var entity = {
          'name': name,
          'kind': kind,
          'is_robot': is_robot
        };

        if ($scope.isOrganization) {
          entity['is_org_member'] = true;
        }

        $scope.setEntityInternal(entity, false);
      };

      $scope.clearEntityInternal = function() {
        $scope.currentEntityInternal = null;
        $scope.currentEntity = null;
        $scope.entitySelected({'entity': null});
        if ($scope.ngModel) {
          $scope.ngModel.$setValidity('entity', false);
        }
      };

      $scope.setEntityInternal = function(entity, updateTypeahead) {
        if (updateTypeahead) {
          $(input).typeahead('val', $scope.autoClear ? '' : entity.name);
        } else {
          $(input).val($scope.autoClear ? '' : entity.name);
        }

        if (!$scope.autoClear) {
          $scope.currentEntityInternal = entity;
          $scope.currentEntity = entity;
        }

        $scope.entitySelected({'entity': entity});
        if ($scope.ngModel) {
          $scope.ngModel.$setValidity('entity', !!entity);
        }
      };

      // Setup the typeahead.
      var input = $element[0].firstChild.firstChild;

      (function() {
        // Create the bloodhound search query system.
        $rootScope.__entity_search_counter = (($rootScope.__entity_search_counter || 0) + 1);
        var entitySearchB = new Bloodhound({
          name: 'entities' + $rootScope.__entity_search_counter,
          remote: {
            url: '/api/v1/entities/%QUERY',
            replace: function (url, uriEncodedQuery) {
              var namespace = $scope.namespace || '';
              url = url.replace('%QUERY', uriEncodedQuery);
              url += '?namespace=' + encodeURIComponent(namespace);
              if ($scope.isOrganization && isSupported('team')) {
                url += '&includeTeams=true'
              }
              if (isSupported('org')) {
                url += '&includeOrgs=true'
              }
              return url;
            },
            filter: function(data) {
              var datums = [];
              for (var i = 0; i < data.results.length; ++i) {
                var entity = data.results[i];

                var found = 'user';
                if (entity.kind == 'user') {
                  found = entity.is_robot ? 'robot' : 'user';
                } else if (entity.kind == 'team') {
                  found = 'team';
                } else if (entity.kind == 'org') {
                  found = 'org';
                }

                if (!isSupported(found)) {
                  continue;
                }

                datums.push({
                  'value': entity.name,
                  'tokens': [entity.name],
                  'entity': entity
                });
              }
              return datums;
            }
          },
          datumTokenizer: function(d) { 
            return Bloodhound.tokenizers.whitespace(d.val); 
          },
          queryTokenizer: Bloodhound.tokenizers.whitespace
        });
        entitySearchB.initialize();

        // Setup the typeahead.
        $(input).typeahead({
          'highlight': true
        }, {
          source: entitySearchB.ttAdapter(),
          templates: {
            'empty': function(info) {
              // Only display the empty dialog if the server load has finished.
              if (info.resultKind == 'remote') {
                var val = $(input).val();
                if (!val) {
                  return null;
                }

                if (UtilService.isEmailAddress(val)) {
                  if ($scope.allowEmails) {
                    return '<div class="tt-message">' + $scope.emailMessage + '</div>';
                  } else {
                    return '<div class="tt-empty">A ' + Config.REGISTRY_TITLE_SHORT + ' username (not an e-mail address) must be specified</div>';
                  }
                }

                var classes = [];

                if (isSupported('user')) { classes.push('users'); }
                if (isSupported('org')) { classes.push('organizations'); }
                if ($scope.isAdmin && isSupported('robot')) { classes.push('robot accounts'); }
                if ($scope.isOrganization && isSupported('team')) { classes.push('teams'); }

                if (classes.length > 1) {
                  classes[classes.length - 1] = 'or ' +  classes[classes.length - 1];
                }

                var class_string = '';
                for (var i = 0; i < classes.length; ++i) {
                  if (i > 0) {                   
                    if (i == classes.length - 1) {
                      class_string += ' or ';
                    } else {
                      class_string += ', ';
                    }
                  }

                  class_string += classes[i];
                }

                return '<div class="tt-empty">No matching ' + Config.REGISTRY_TITLE_SHORT + ' ' + class_string + ' found</div>';
              }

              return null;
            }, 
            'suggestion': function (datum) {
              template = '<div class="entity-mini-listing">';
              if (datum.entity.kind == 'user' && !datum.entity.is_robot) {
                template += '<i class="fa fa-user fa-lg"></i>';
              } else if (datum.entity.kind == 'user' && datum.entity.is_robot) {
                template += '<i class="fa fa-wrench fa-lg"></i>';            
              } else if (datum.entity.kind == 'team') {
                template += '<i class="fa fa-group fa-lg"></i>';
              } else if (datum.entity.kind == 'org') {
                template += '<i class="fa"><img src="//www.gravatar.com/avatar/' + 
                  datum.entity.gravatar + '?s=16&amp;d=identicon"></i>';
              }

              template += '<span class="name">' + datum.value + '</span>';

              if (datum.entity.is_org_member === false && datum.entity.kind == 'user') {
                template += '<i class="fa fa-exclamation-triangle" title="User is outside the organization"></i>';
              }

              template += '</div>';
              return template;
            }}
        });

        $(input).on('keypress', function(e) {
          var val = $(input).val();
          var code = e.keyCode || e.which;
          if (code == 13 && $scope.allowEmails && UtilService.isEmailAddress(val)) {
            $scope.$apply(function() {
              $scope.emailSelected({'email': val});
            });
          }
        });

        $(input).on('input', function(e) {
          $scope.$apply(function() {
            $scope.clearEntityInternal();
          });
        });

        $(input).on('typeahead:selected', function(e, datum) {
          $scope.$apply(function() {
            $scope.setEntityInternal(datum.entity, true);
          });
        });
      })();

      $scope.$watch('clearValue', function() {
        if (!input) { return; }

        $(input).typeahead('val', '');
        $scope.clearEntityInternal();        
      });

      $scope.$watch('placeholder', function(title) {
        input.setAttribute('placeholder', title);
      });

      $scope.$watch('allowedEntities', function(allowed) {
        if (!allowed) { return; }
        $scope.includeTeams = isSupported('team', allowed);
        $scope.includeRobots = isSupported('robot', allowed);
      });

      $scope.$watch('namespace', function(namespace) {
        if (!namespace) { return; }

        $scope.isAdmin = UserService.isNamespaceAdmin(namespace);
        $scope.isOrganization = !!UserService.getOrganization(namespace);
      });

      $scope.$watch('currentEntity', function(entity) {
        if ($scope.currentEntityInternal != entity) {
          if (entity) {
            $scope.setEntityInternal(entity, false);
          } else {
            $scope.clearEntityInternal();
          }
        }
      });
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('roleGroup', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/role-group.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'roles': '=roles',
      'currentRole': '=currentRole',
      'roleChanged': '&roleChanged'
    },
    controller: function($scope, $element) {
      $scope.setRole = function(role) {
        if ($scope.currentRole == role) { return; }
        if ($scope.roleChanged) {          
          $scope.roleChanged({'role': role});
        } else {
          $scope.currentRole = role;
        }
      };
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('billingOptions', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/billing-options.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'user': '=user',
      'organization': '=organization'
    },
    controller: function($scope, $element, PlanService, ApiService) {
      $scope.invoice_email = false;
      $scope.currentCard = null;

      // Listen to plan changes.
      PlanService.registerListener(this, function(plan) {
        if (plan && plan.price > 0) {
          update();
        }
      });

      $scope.$on('$destroy', function() {
        PlanService.unregisterListener(this);
      });

      $scope.isExpiringSoon = function(cardInfo) {
        var current = new Date();
        var expires = new Date(cardInfo.exp_year, cardInfo.exp_month, 1);
        var difference = expires - current;
        return difference < (60 * 60 * 24 * 60 * 1000 /* 60 days */);
      };

      $scope.changeCard = function() {
        var previousCard = $scope.currentCard;
        $scope.changingCard = true;
        var callbacks = {
          'opened': function() { $scope.changingCard = true; },
          'closed': function() { $scope.changingCard = false; },
          'started': function() { $scope.currentCard = null; },
          'success': function(resp) {
             $scope.currentCard = resp.card;
             $scope.changingCard = false;
          },
          'failure': function(resp) {
             $scope.changingCard = false;
             $scope.currentCard = previousCard;

             if (!PlanService.isCardError(resp)) {
               $('#cannotchangecardModal').modal({});
             }
          }
        };

        PlanService.changeCreditCard($scope, $scope.organization ? $scope.organization.name : null, callbacks);
      };

      $scope.getCreditImage = function(creditInfo) {
        if (!creditInfo || !creditInfo.type) { return 'credit.png'; }

        var kind = creditInfo.type.toLowerCase() || 'credit';
        var supported = {
          'american express': 'amex',
          'credit': 'credit',
          'diners club': 'diners',
          'discover': 'discover',
          'jcb': 'jcb',
          'mastercard': 'mastercard',       
          'visa': 'visa'
        };
        
        kind = supported[kind] || 'credit';
        return kind + '.png';
      };

      var update = function() {
        if (!$scope.user && !$scope.organization) { return; }
        $scope.obj = $scope.user ? $scope.user : $scope.organization;
        $scope.invoice_email = $scope.obj.invoice_email;

        // Load the credit card information.
        PlanService.getCardInfo($scope.organization ? $scope.organization.name : null, function(card) {
          $scope.currentCard = card;
        });
      };

      var save = function() {
        $scope.working = true;

        var errorHandler = ApiService.errorDisplay('Could not change user details');
        ApiService.changeDetails($scope.organization, $scope.obj).then(function(resp) {
          $scope.working = false;
        }, errorHandler);
      };

      var checkSave = function() {
        if (!$scope.obj) { return; }
        if ($scope.obj.invoice_email != $scope.invoice_email) {
          $scope.obj.invoice_email = $scope.invoice_email;
          save();
        }
      };
      
      $scope.$watch('invoice_email', checkSave);
      $scope.$watch('organization', update);
      $scope.$watch('user', update);
    }
  };
  return directiveDefinitionObject;
});

      
quayApp.directive('planManager', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/plan-manager.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'user': '=user',
      'organization': '=organization',
      'readyForPlan': '&readyForPlan',
      'planChanged': '&planChanged'
    },
    controller: function($scope, $element, PlanService, ApiService) {
      $scope.isExistingCustomer = false;
        
      $scope.parseDate = function(timestamp) {
        return new Date(timestamp * 1000);
      };

      $scope.isPlanVisible = function(plan, subscribedPlan) {
        if (plan['deprecated']) {
          return plan == subscribedPlan;
        }

        if ($scope.organization && !PlanService.isOrgCompatible(plan)) {
          return false;
        }

        return true;
      };

      $scope.changeSubscription = function(planId, opt_async) {
        if ($scope.planChanging) { return; }

        var callbacks = {
          'opening': function() { $scope.planChanging = true; },
          'started': function() { $scope.planChanging = true; },
          'opened': function() { $scope.planChanging = true; },
          'closed': function() { $scope.planChanging = false; },
          'success': subscribedToPlan,
          'failure': function(resp) {            
             $scope.planChanging = false;
          }
        };

        PlanService.changePlan($scope, $scope.organization, planId, callbacks, opt_async);
      };

      $scope.cancelSubscription = function() {
        $scope.changeSubscription(PlanService.getFreePlan());
      };

      var subscribedToPlan = function(sub) {
        $scope.subscription = sub;
        $scope.isExistingCustomer = !!sub['isExistingCustomer'];

        PlanService.getPlanIncludingDeprecated(sub.plan, function(subscribedPlan) {
          $scope.subscribedPlan = subscribedPlan;
          $scope.planUsagePercent = sub.usedPrivateRepos * 100 / $scope.subscribedPlan.privateRepos;
          
          if ($scope.planChanged) {
            $scope.planChanged({ 'plan': subscribedPlan });
          }

          if (sub.usedPrivateRepos > $scope.subscribedPlan.privateRepos) {
            $scope.limit = 'over';
          } else if (sub.usedPrivateRepos == $scope.subscribedPlan.privateRepos) {
            $scope.limit = 'at';
          } else if (sub.usedPrivateRepos >= $scope.subscribedPlan.privateRepos * 0.7) {
            $scope.limit = 'near';
          } else {
            $scope.limit = 'none';
          }

          if (!$scope.chart) {
            $scope.chart = new UsageChart();
            $scope.chart.draw('repository-usage-chart');
          }

          $scope.chart.update(sub.usedPrivateRepos || 0, $scope.subscribedPlan.privateRepos || 0);

          $scope.planChanging = false;
          $scope.planLoading = false;
        });
      };

      var update = function() {
        $scope.planLoading = true;
        if (!$scope.plans) { return; }

        PlanService.getSubscription($scope.organization, subscribedToPlan, function() {
          $scope.isExistingCustomer = false;
          subscribedToPlan({ 'plan': PlanService.getFreePlan() });
        });
      };

      var loadPlans = function() {
        if ($scope.plans || $scope.loadingPlans) { return; }
        if (!$scope.user && !$scope.organization) { return; }

        $scope.loadingPlans = true;
        PlanService.verifyLoaded(function(plans) {
          $scope.plans = plans;
          update();
            
          if ($scope.readyForPlan) {
            var planRequested = $scope.readyForPlan();
            if (planRequested && planRequested != PlanService.getFreePlan()) {
              $scope.changeSubscription(planRequested, /* async */true);
            }
          }
        });
      };

      // Start the initial download.
      $scope.planLoading = true;
      loadPlans();

      $scope.$watch('organization', loadPlans);
      $scope.$watch('user', loadPlans);
    }
  };
  return directiveDefinitionObject;
});



quayApp.directive('namespaceSelector', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/namespace-selector.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'user': '=user',
      'namespace': '=namespace',
      'requireCreate': '=requireCreate'
    },
    controller: function($scope, $element, $routeParams, $location, CookieService) {
      $scope.namespaces = {};

      $scope.initialize = function(user) {
        var preferredNamespace = user.username;
        var namespaces = {};
        namespaces[user.username] = user;
        if (user.organizations) {
          for (var i = 0; i < user.organizations.length; ++i) {
            namespaces[user.organizations[i].name] = user.organizations[i];
            if (user.organizations[i].preferred_namespace) {
              preferredNamespace = user.organizations[i].name;
            }
          }
        }

        var initialNamespace = $routeParams['namespace'] || CookieService.get('quay.namespace') ||
            preferredNamespace || $scope.user.username;
        $scope.namespaces = namespaces;
        $scope.setNamespace($scope.namespaces[initialNamespace]);
      };

      $scope.setNamespace = function(namespaceObj) {
        if (!namespaceObj) {
          namespaceObj = $scope.namespaces[$scope.user.username];
        }

        if ($scope.requireCreate && !namespaceObj.can_create_repo) {
          namespaceObj = $scope.namespaces[$scope.user.username];
        }

        var newNamespace = namespaceObj.name || namespaceObj.username;       
        $scope.namespaceObj = namespaceObj;
        $scope.namespace = newNamespace;

        if (newNamespace) {
          CookieService.putPermanent('quay.namespace', newNamespace);

          if ($routeParams['namespace'] && $routeParams['namespace'] != newNamespace) {
            $location.search({'namespace': newNamespace});
          }
        }
      };

      $scope.$watch('namespace', function(namespace) {
        if ($scope.namespaceObj && namespace && namespace != $scope.namespaceObj.username) {
          $scope.setNamespace($scope.namespaces[namespace]);
        }
      });

      $scope.$watch('user', function(user) {
        $scope.user = user;
        $scope.initialize(user);
      });
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('buildLogPhase', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/build-log-phase.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'phase': '=phase'
    },
    controller: function($scope, $element) {
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('buildLogError', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/build-log-error.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'error': '=error',
      'entries': '=entries'
    },
    controller: function($scope, $element, Config) {
      $scope.getLocalPullInfo = function() {
        if ($scope.entries.__localpull !== undefined) {
          return $scope.entries.__localpull;
        }

        var localInfo = {
          'isLocal': false
        };

        // Find the 'pulling' phase entry, and then extra any metadata found under
        // it.
        for (var i = 0; i < $scope.entries.length; ++i) {
          var entry = $scope.entries[i];
          if (entry.type == 'phase' && entry.message == 'pulling') {
            for (var j = 0; j < entry.logs.length(); ++j) {
              var log = entry.logs.get(j);
              if (log.data && log.data.phasestep == 'login') {
                localInfo['login'] = log.data;
              }

              if (log.data && log.data.phasestep == 'pull') {
                var repo_url = log.data['repo_url'];
                var repo_and_tag = repo_url.substring(Config.SERVER_HOSTNAME.length + 1);
                var tagIndex = repo_and_tag.lastIndexOf(':');
                var repo = repo_and_tag.substring(0, tagIndex);

                localInfo['repo_url'] = repo_url;
                localInfo['repo'] = repo;                

                localInfo['isLocal'] = repo_url.indexOf(Config.SERVER_HOSTNAME + '/') == 0;
              }
            }
            break;
          }
        }

        return $scope.entries.__localpull = localInfo;
      };
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('triggerDescription', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/trigger-description.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'trigger': '=trigger',
      'short': '=short'
    },
    controller: function($scope, $element) {
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('dropdownSelect', function ($compile) {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/dropdown-select.html',
    replace: true,
    transclude: true,
    restrict: 'C',
    scope: {
      'selectedItem': '=selectedItem',
      'placeholder': '=placeholder',
      'lookaheadItems': '=lookaheadItems',

      'allowCustomInput': '@allowCustomInput',

      'handleItemSelected': '&handleItemSelected',
      'handleInput': '&handleInput',

      'clearValue': '=clearValue'
    },
    controller: function($scope, $element, $rootScope) {
      if (!$rootScope.__dropdownSelectCounter) {
        $rootScope.__dropdownSelectCounter = 1;
      }

      $scope.placeholder = $scope.placeholder || '';
      $scope.internalItem = null;

      // Setup lookahead.
      var input = $($element).find('.lookahead-input');
          
      $scope.$watch('clearValue', function(cv) {
        if (cv) {
          $scope.selectedItem = null;
          $(input).val('');
        }
      });

      $scope.$watch('selectedItem', function(item) {
        if ($scope.selectedItem == $scope.internalItem) {
          // The item has already been set due to an internal action.
          return;
        }

        if ($scope.selectedItem != null) {
          $(input).val(item.toString());
        } else {
          $(input).val('');
        }
      });

      $scope.$watch('lookaheadItems', function(items) {
        $(input).off();
        if (!items) {
          return;
        }

        var formattedItems = [];
        for (var i = 0; i < items.length; ++i) {
          var formattedItem = items[i];
          if (typeof formattedItem == 'string') {
            formattedItem = {
              'value': formattedItem
            };
          }
          formattedItems.push(formattedItem);
        }

        var dropdownHound = new Bloodhound({
          name: 'dropdown-items-' + $rootScope.__dropdownSelectCounter,
          local: formattedItems,
          datumTokenizer: function(d) {
            return Bloodhound.tokenizers.whitespace(d.val || d.value || '');
          },
          queryTokenizer: Bloodhound.tokenizers.whitespace          
        });
        dropdownHound.initialize();

        $(input).typeahead({}, {
          source: dropdownHound.ttAdapter(),
          templates: {
            'suggestion': function (datum) {
              template = datum['template'] ? datum['template'](datum) : datum['value'];
              return template;
            }
          }
        });

        $(input).on('input', function(e) {
          $scope.$apply(function() {
            $scope.internalItem = null;
            $scope.selectedItem = null;
            if ($scope.handleInput) {
              $scope.handleInput({'input': $(input).val()});
            }
          });
        });

        $(input).on('typeahead:selected', function(e, datum) {
          $scope.$apply(function() {
            $scope.internalItem = datum['item'] || datum['value'];
            $scope.selectedItem = datum['item'] || datum['value'];
            if ($scope.handleItemSelected) {
              $scope.handleItemSelected({'datum': datum});
            }
          });
        });

        $rootScope.__dropdownSelectCounter++;
      });
    },
    link: function(scope, element, attrs) {
      var transcludedBlock = element.find('div.transcluded');
      var transcludedElements = transcludedBlock.children();

      var iconContainer = element.find('div.dropdown-select-icon-transclude');
      var menuContainer = element.find('div.dropdown-select-menu-transclude');
      
      angular.forEach(transcludedElements, function(elem) {
        if (angular.element(elem).hasClass('dropdown-select-icon')) {
          iconContainer.append(elem);
        } else if (angular.element(elem).hasClass('dropdown-select-menu')) {
          menuContainer.replaceWith(elem);
        }
      });

      transcludedBlock.remove();      
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('dropdownSelectIcon', function () {
  var directiveDefinitionObject = {
    priority: 1,
    require: '^dropdownSelect',
    templateUrl: '/static/directives/dropdown-select-icon.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
    },
    controller: function($scope, $element) {
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('dropdownSelectMenu', function () {
  var directiveDefinitionObject = {
    priority: 1,
    require: '^dropdownSelect',
    templateUrl: '/static/directives/dropdown-select-menu.html',
    replace: true,
    transclude: true,
    restrict: 'C',
    scope: {
    },
    controller: function($scope, $element) {
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('manualTriggerBuildDialog', function () {
  var directiveDefinitionObject = {
    templateUrl: '/static/directives/manual-trigger-build-dialog.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'repository': '=repository',
      'counter': '=counter',
      'trigger': '=trigger',
      'startBuild': '&startBuild'
    },
    controller: function($scope, $element, ApiService, TriggerService) {
      $scope.parameters = {};
      $scope.fieldOptions = {};

      $scope.startTrigger = function() {
        $('#startTriggerDialog').modal('hide');
        $scope.startBuild({
          'trigger': $scope.trigger,
          'parameters': $scope.parameters
        });
      };

      $scope.show = function() {
        $scope.parameters = {};
        $scope.fieldOptions = {};

        var parameters = TriggerService.getRunParameters($scope.trigger.service);
        for (var i = 0; i < parameters.length; ++i) {
          var parameter = parameters[i];
          if (parameter['type'] == 'option') {
            // Load the values for this parameter.
            var params = {
              'repository': $scope.repository.namespace + '/' + $scope.repository.name,
              'trigger_uuid': $scope.trigger.id,
              'field_name': parameter['name']
            };

            ApiService.listTriggerFieldValues(null, params).then(function(resp) {
              $scope.fieldOptions[parameter['name']] = resp['values'];
            });
          }
        }
        $scope.runParameters = parameters;

        $('#startTriggerDialog').modal('show');
      };

      $scope.$watch('counter', function(counter) {
        if (counter) {
          $scope.show();
        }
      });
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('setupTriggerDialog', function () {
  var directiveDefinitionObject = {
    templateUrl: '/static/directives/setup-trigger-dialog.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'repository': '=repository',
      'trigger': '=trigger',
      'counter': '=counter',
      'canceled': '&canceled',
      'activated': '&activated'
    },
    controller: function($scope, $element, ApiService, UserService) {
      var modalSetup = false;

      $scope.show = function() {
        if (!$scope.trigger || !$scope.repository) { return; }

        $scope.activating = false;
        $scope.pullEntity = null;
        $scope.publicPull = true;
        $scope.showPullRequirements = false;

        $('#setupTriggerModal').modal({});

        if (!modalSetup) {
          $('#setupTriggerModal').on('hidden.bs.modal', function () {
            if (!$scope.trigger || $scope.trigger['is_active']) { return; }

            $scope.$apply(function() {
              $scope.cancelSetupTrigger();
            });
          });
          modalSetup = true;
        }
      };

      $scope.isNamespaceAdmin = function(namespace) {
        return UserService.isNamespaceAdmin(namespace);
      };

      $scope.cancelSetupTrigger = function() {
        $scope.canceled({'trigger': $scope.trigger});
      };

      $scope.hide = function() {
        $scope.activating = false;
        $('#setupTriggerModal').modal('hide');
      };

      $scope.setPublicPull = function(value) {
        $scope.publicPull = value;
      };

      $scope.checkAnalyze = function(isValid) {
        if (!isValid) {
          $scope.publicPull = true;
          $scope.pullEntity = null;          
          $scope.showPullRequirements = false;
          $scope.checkingPullRequirements = false;
          return;
        }
        
        $scope.checkingPullRequirements = true;
        $scope.showPullRequirements = true;
        $scope.pullRequirements = null;

        var params = {
          'repository': $scope.repository.namespace + '/' + $scope.repository.name,
          'trigger_uuid': $scope.trigger.id
        };

        var data = {
          'config': $scope.trigger.config
        };

        ApiService.analyzeBuildTrigger(data, params).then(function(resp) {
          $scope.pullRequirements = resp;

          if (resp['status'] == 'publicbase') {
            $scope.publicPull = true;
            $scope.pullEntity = null;
          } else if (resp['namespace']) {
            $scope.publicPull = false;

            if (resp['robots'] && resp['robots'].length > 0) {
              $scope.pullEntity = resp['robots'][0];
            } else {
              $scope.pullEntity = null;
            }
          }

          $scope.checkingPullRequirements = false;
        }, function(resp) {
          $scope.pullRequirements = resp;          
          $scope.checkingPullRequirements = false;
        });
      };

      $scope.activate = function() {
        var params = {
          'repository': $scope.repository.namespace + '/' + $scope.repository.name,
          'trigger_uuid': $scope.trigger.id
        };

        var data = {
          'config': $scope.trigger['config']
        };

        if ($scope.pullEntity) {
          data['pull_robot'] = $scope.pullEntity['name'];
        }

        $scope.activating = true;

        var errorHandler = ApiService.errorDisplay('Cannot activate build trigger', function(resp) {
          $scope.hide();
          $scope.canceled({'trigger': $scope.trigger});
        });

        ApiService.activateBuildTrigger(data, params).then(function(resp) {
          $scope.hide();
          $scope.trigger['is_active'] = true;
          $scope.trigger['pull_robot'] = resp['pull_robot'];
          $scope.activated({'trigger': $scope.trigger});
        }, errorHandler);
      };

      var check = function() {
        if ($scope.counter && $scope.trigger && $scope.repository) {
          $scope.show();
        }
      };

      $scope.$watch('trigger', check);
      $scope.$watch('counter', check);
      $scope.$watch('repository', check);
    }
  };
  return directiveDefinitionObject;
});



quayApp.directive('triggerSetupGithub', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/trigger-setup-github.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'repository': '=repository',
      'trigger': '=trigger',
      'analyze': '&analyze'
    },
    controller: function($scope, $element, ApiService) {
      $scope.analyzeCounter = 0;
      $scope.setupReady = false;
      $scope.loading = true;
           
      $scope.handleLocationInput = function(location) {
        $scope.trigger['config']['subdir'] = location || '';
        $scope.isInvalidLocation = $scope.locations.indexOf(location) < 0;
        $scope.analyze({'isValid': !$scope.isInvalidLocation});
      };

      $scope.handleLocationSelected = function(datum) {
        $scope.setLocation(datum['value']);
      };

      $scope.setLocation = function(location) {
        $scope.currentLocation = location;
        $scope.trigger['config']['subdir'] = location || '';
        $scope.isInvalidLocation = false;
        $scope.analyze({'isValid': true});
      };
 
      $scope.selectRepo = function(repo, org) {
        $scope.currentRepo = {
          'repo': repo,
          'avatar_url': org['info']['avatar_url'],
          'toString': function() {
            return this.repo;
          }
        };
      };

      $scope.selectRepoInternal = function(currentRepo) {
        if (!currentRepo) {
          $scope.trigger.$ready = false;
          return;
        }

        var params = {
          'repository': $scope.repository.namespace + '/' + $scope.repository.name,
          'trigger_uuid': $scope.trigger['id']
        };

        var repo = currentRepo['repo'];
        $scope.trigger['config'] = {
          'build_source': repo,
          'subdir': ''
        };

        // Lookup the possible Dockerfile locations.
        $scope.locations = null;
        if (repo) {
          ApiService.listBuildTriggerSubdirs($scope.trigger['config'], params).then(function(resp) {
            if (resp['status'] == 'error') {
              $scope.locationError = resp['message'] || 'Could not load Dockerfile locations';
              $scope.locations = null;
              $scope.trigger.$ready = false;
              $scope.isInvalidLocation = false;
              $scope.analyze({'isValid': false});
              return;
            }

            $scope.locationError = null;
            $scope.locations = resp['subdir'] || [];
            $scope.trigger.$ready = true;

            if ($scope.locations.length > 0) {
              $scope.setLocation($scope.locations[0]);
            } else {
              $scope.currentLocation = null;
              $scope.isInvalidLocation = resp['subdir'].indexOf('') < 0;
              $scope.analyze({'isValid': !$scope.isInvalidLocation});
            }
          }, function(resp) {
            $scope.locationError = resp['message'] || 'Could not load Dockerfile locations';
            $scope.locations = null;
            $scope.trigger.$ready = false;
            $scope.isInvalidLocation = false;
            $scope.analyze({'isValid': false});
          });
        }
      };

      var setupTypeahead = function() {        
        var repos = [];
        for (var i = 0; i < $scope.orgs.length; ++i) {
          var org = $scope.orgs[i];
          var orepos = org['repos'];
          for (var j = 0; j < orepos.length; ++j) {
            var repoValue = {
              'repo': orepos[j],
              'avatar_url': org['info']['avatar_url'],
              'toString': function() {
                return this.repo;
              }
            };
            var datum = {
              'name': orepos[j],
              'org': org,
              'value': orepos[j],
              'title': orepos[j],
              'item': repoValue
            };
            repos.push(datum);
          }
        }

        $scope.repoLookahead = repos;
      };

      var loadSources = function() {
        var params = {
          'repository': $scope.repository.namespace + '/' + $scope.repository.name,
          'trigger_uuid': $scope.trigger.id
        };

        ApiService.listTriggerBuildSources(null, params).then(function(resp) {
          $scope.orgs = resp['sources'];
          setupTypeahead();
          $scope.loading = false;
        });
      };

      var check = function() {
        if ($scope.repository && $scope.trigger) {
          loadSources();
        }
      };

      $scope.$watch('repository', check);
      $scope.$watch('trigger', check);

      $scope.$watch('currentRepo', function(repo) {
        $scope.selectRepoInternal(repo);
      });
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('buildLogCommand', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/build-log-command.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'command': '=command'
    },
    controller: function($scope, $element) {
      $scope.getWithoutStep = function(fullTitle) {
        var colon = fullTitle.indexOf(':');
        if (colon <= 0) {
          return '';
        }
        
        return $.trim(fullTitle.substring(colon + 1));
      };
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('dockerfileCommand', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/dockerfile-command.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'command': '=command'
    },
    controller: function($scope, $element, UtilService, Config) {
      var registryHandlers = {
        'quay.io': function(pieces) {
          var rnamespace =  pieces[pieces.length - 2];
          var rname = pieces[pieces.length - 1].split(':')[0];
          return '/repository/' + rnamespace + '/' + rname + '/'; 
        },

        '': function(pieces) {
          var rnamespace = pieces.length == 1 ? '_' : 'u/' + pieces[0];
          var rname = pieces[pieces.length - 1].split(':')[0];
          return 'https://registry.hub.docker.com/' + rnamespace + '/' + rname + '/';
        }
      };

      registryHandlers[Config.getDomain()] = registryHandlers['quay.io'];

      var kindHandlers = {
        'FROM': function(title) {
          var pieces = title.split('/');
          var registry = pieces.length < 3 ? '' : pieces[0];
          if (!registryHandlers[registry]) {
            return title;
          }
          
          return '<i class="fa fa-hdd-o"></i> <a href="' + registryHandlers[registry](pieces) + '" target="_blank">' + title + '</a>';
        }
      };
     
      $scope.getCommandKind = function(title) {
        var space = title.indexOf(' ');
        return title.substring(0, space);
      };

      $scope.getCommandTitleHtml = function(title) {
        var space = title.indexOf(' ');
        if (space <= 0) {
          return UtilService.textToSafeHtml(title);
        }
        
        var kind = $scope.getCommandKind(title);
        var sanitized = UtilService.textToSafeHtml(title.substring(space + 1));
        
        var handler = kindHandlers[kind || ''];
        if (handler) {
          return handler(sanitized);
        } else {
          return sanitized;
        }
      };      
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('dockerfileView', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/dockerfile-view.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'contents': '=contents'
    },
    controller: function($scope, $element, UtilService) {
      $scope.$watch('contents', function(contents) {
        $scope.lines = [];

        var lines = contents ? contents.split('\n') : [];
        for (var i = 0; i < lines.length; ++i) {
          var line = $.trim(lines[i]);
          var kind = 'text';
          if (line && line[0] == '#') {
            kind = 'comment';
          } else if (line.match(/^([A-Z]+\s)/)) {
            kind = 'command';
          }

          var lineInfo = {
            'text': line,
            'kind': kind
          };
          $scope.lines.push(lineInfo);
        }
      });
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('buildStatus', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/build-status.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'build': '=build'
    },
    controller: function($scope, $element) {
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('buildMessage', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/build-message.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'phase': '=phase'
    },
    controller: function($scope, $element) {
      $scope.getBuildMessage = function (phase) {
        switch (phase) {
          case 'cannot_load':
            return 'Cannot load build status - Please report this error';

          case 'starting':
          case 'initializing':
            return 'Starting Dockerfile build';
          
          case 'waiting':
            return 'Waiting for available build worker';

          case 'unpacking':
            return 'Unpacking build package';

          case 'pulling':
            return 'Pulling base image';
          
          case 'building':
            return 'Building image from Dockerfile';
          
          case 'pushing':
            return 'Pushing image built from Dockerfile';
            
          case 'complete':
            return 'Dockerfile build completed and pushed';
          
          case 'error':
            return 'Dockerfile build failed';
        }
      };
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('buildProgress', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/build-progress.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'build': '=build'
    },
    controller: function($scope, $element) {
      $scope.getPercentage = function(buildInfo) {
        switch (buildInfo.phase) {
          case 'pulling':
            return buildInfo.status.pull_completion * 100;
            break;

          case 'building':
            return (buildInfo.status.current_command / buildInfo.status.total_commands) * 100;
            break;

          case 'pushing':
            return buildInfo.status.push_completion * 100;
            break;

          case 'complete':
            return 100;
            break;

          case 'initializing':
          case 'starting':
          case 'waiting':
          case 'cannot_load':
          case 'unpacking':
            return 0;
            break;
        }
        
        return -1;
      };
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('externalNotificationView', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/external-notification-view.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'repository': '=repository',
      'notification': '=notification',
      'notificationDeleted': '&notificationDeleted'
    },
    controller: function($scope, $element, ExternalNotificationData, ApiService) {
      $scope.deleteNotification = function() {
        var params = {
          'repository': $scope.repository.namespace + '/' + $scope.repository.name,
          'uuid': $scope.notification.uuid
        };

        ApiService.deleteRepoNotification(null, params).then(function() {
          $scope.notificationDeleted({'notification': $scope.notification});
        });
      };

      $scope.testNotification = function() {
        var params = {
          'repository': $scope.repository.namespace + '/' + $scope.repository.name,
          'uuid': $scope.notification.uuid
        };

        ApiService.testRepoNotification(null, params).then(function() {
          bootbox.dialog({
            "title": "Test Notification Queued",
            "message": "A test version of this notification has been queued and should appear shortly",
            "buttons": {
              "close": {
                "label": "Close",
                "className": "btn-primary"
              }
            }
          });          
        });
      };

      $scope.$watch('notification', function(notification) {
        if (notification) {
          $scope.eventInfo = ExternalNotificationData.getEventInfo(notification.event);
          $scope.methodInfo = ExternalNotificationData.getMethodInfo(notification.method);
          $scope.config = notification.config;
        }
      });
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('createExternalNotificationDialog', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/create-external-notification-dialog.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'repository': '=repository',
      'counter': '=counter',
      'notificationCreated': '&notificationCreated'
    },
    controller: function($scope, $element, ExternalNotificationData, ApiService, $timeout, StringBuilderService) {
      $scope.currentEvent = null;
      $scope.currentMethod = null;
      $scope.status = '';
      $scope.currentConfig = {};
      $scope.clearCounter = 0;
      $scope.unauthorizedEmail = false;

      $scope.events = ExternalNotificationData.getSupportedEvents();
      $scope.methods = ExternalNotificationData.getSupportedMethods();
   
      $scope.setEvent = function(event) {
        $scope.currentEvent = event;
      };

      $scope.setMethod = function(method) {
        $scope.currentConfig = {};
        $scope.currentMethod = method;
        $scope.unauthorizedEmail = false;
      };

      $scope.createNotification = function() {
        if (!$scope.currentConfig.email) {
          $scope.performCreateNotification();
          return;
        }

        $scope.status = 'checking-email';
        $scope.checkEmailAuthorization();
      };

      $scope.checkEmailAuthorization = function() {
        var params = {
          'repository': $scope.repository.namespace + '/' + $scope.repository.name,
          'email': $scope.currentConfig.email
        };

        ApiService.checkRepoEmailAuthorized(null, params).then(function(resp) {
          $scope.handleEmailCheck(resp.confirmed);
        }, function(resp) {
          $scope.handleEmailCheck(false);
        });
      };

      $scope.performCreateNotification = function() {
        $scope.status = 'creating';

        var params = {
          'repository': $scope.repository.namespace + '/' + $scope.repository.name
        };

        var data = {
          'event': $scope.currentEvent.id,
          'method': $scope.currentMethod.id,
          'config': $scope.currentConfig
        };

        ApiService.createRepoNotification(data, params).then(function(resp) {
          $scope.status = '';
          $scope.notificationCreated({'notification': resp});
          $('#createNotificationModal').modal('hide');
        });
      };

      $scope.handleEmailCheck = function(isAuthorized) {
        if (isAuthorized) {
          $scope.performCreateNotification();
          return; 
        }

        if ($scope.status == 'authorizing-email-sent') {
          $scope.watchEmail();
        } else {
          $scope.status = 'unauthorized-email';
        }

        $scope.unauthorizedEmail = true;
      };

      $scope.sendAuthEmail = function() {
        $scope.status = 'authorizing-email';

        var params = {
          'repository': $scope.repository.namespace + '/' + $scope.repository.name,
          'email': $scope.currentConfig.email
        };

        ApiService.sendAuthorizeRepoEmail(null, params).then(function(resp) {
          $scope.status = 'authorizing-email-sent';
          $scope.watchEmail();
        });
      };

      $scope.watchEmail = function() {
        // TODO: change this to SSE?
        $timeout(function() {
          $scope.checkEmailAuthorization();
        }, 1000);
      };

      $scope.getHelpUrl = function(field, config) {
        var helpUrl = field['help_url'];
        if (!helpUrl) {
          return null;
        }

        return StringBuilderService.buildUrl(helpUrl, config);
      };

      $scope.$watch('counter', function(counter) {
        if (counter) {
          $scope.clearCounter++;
          $scope.status = '';
          $scope.currentEvent = null;
          $scope.currentMethod = null;
          $scope.unauthorizedEmail = false;
          $('#createNotificationModal').modal({});
        }
      });      
    }
  };
  return directiveDefinitionObject;
});



quayApp.directive('twitterView', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/twitter-view.html',
    replace: false,
    transclude: true,
    restrict: 'C',
    scope: {
      'avatarUrl': '@avatarUrl',
      'authorName': '@authorName',
      'authorUser': '@authorUser',
      'messageUrl': '@messageUrl',
      'messageDate': '@messageDate'
    },
    controller: function($scope, $element) {
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('notificationsBubble', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/notifications-bubble.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
    },
    controller: function($scope, UserService, NotificationService) {
      $scope.notificationService = NotificationService;
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('notificationView', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/notification-view.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'notification': '=notification',
      'parent': '=parent'
    },
    controller: function($scope, $element, $window, $location, UserService, NotificationService, ApiService) {
      var stringStartsWith = function (str, prefix) {
        return str.slice(0, prefix.length) == prefix;
      };

      $scope.getMessage = function(notification) {
        return NotificationService.getMessage(notification);
      };

      $scope.getGravatar = function(orgname) {
        var organization = UserService.getOrganization(orgname);
        return organization['gravatar'] || '';
      };

      $scope.parseDate = function(dateString) {
        return Date.parse(dateString);
      };

      $scope.showNotification = function() {
        var url = NotificationService.getPage($scope.notification);
        if (url) {
          if (stringStartsWith(url, 'http://') || stringStartsWith(url, 'https://')) {
            $window.location.href = url;
          } else {
            var parts = url.split('?')
            $location.path(parts[0]);
            
            if (parts.length > 1) {
              $location.search(parts[1]);
            }

            $scope.parent.$hide();
          }
        }
      };

      $scope.dismissNotification = function(notification) {
        NotificationService.dismissNotification(notification);
      };

      $scope.canDismiss = function(notification) {
        return NotificationService.canDismiss(notification);
      };

      $scope.getClass = function(notification) {
        return NotificationService.getClass(notification);
      };

      $scope.getActions = function(notification) {
        return NotificationService.getActions(notification);
      };
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('dockerfileBuildDialog', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/dockerfile-build-dialog.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'repository': '=repository',
      'showNow': '=showNow',
      'buildStarted': '&buildStarted'
    },
    controller: function($scope, $element) {
      $scope.building = false;
      $scope.uploading = false;
      $scope.startCounter = 0;

      $scope.handleBuildStarted = function(build) {
        $('#dockerfilebuildModal').modal('hide');
        if ($scope.buildStarted) {
          $scope.buildStarted({'build': build});
        }
      };

      $scope.handleBuildFailed = function(message) {
        $scope.errorMessage = message;
      };

      $scope.startBuild = function() {
        $scope.errorMessage = null;
        $scope.startCounter++;
      };
      
      $scope.$watch('showNow', function(sn) {
        if (sn && $scope.repository) {
          $('#dockerfilebuildModal').modal({});
        }
      });
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('dockerfileBuildForm', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/dockerfile-build-form.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'repository': '=repository',
      'startNow': '=startNow',
      'hasDockerfile': '=hasDockerfile',
      'uploadFailed': '&uploadFailed',
      'uploadStarted': '&uploadStarted',
      'buildStarted': '&buildStarted',
      'buildFailed': '&buildFailed',
      'missingFile': '&missingFile',
      'uploading': '=uploading',
      'building': '=building'
    },
    controller: function($scope, $element, ApiService) {
      $scope.internal = {'hasDockerfile': false};

      var handleBuildFailed = function(message) {
        message = message || 'Dockerfile build failed to start';

        var result = false;
        if ($scope.buildFailed) {
          result = $scope.buildFailed({'message': message});
        }

        if (!result) {
          bootbox.dialog({
            "message": message,
            "title": "Cannot start Dockerfile build",
            "buttons": {
              "close": {
                "label": "Close",
                "className": "btn-primary"
              }
            }
          });
        }
      };

      var handleUploadFailed = function(message) {
        message = message || 'Error with file upload';

        var result = false;
        if ($scope.uploadFailed) {
          result = $scope.uploadFailed({'message': message});
        }

        if (!result) {
          bootbox.dialog({
            "message": message,
            "title": "Cannot upload file for Dockerfile build",
            "buttons": {
              "close": {
                "label": "Close",
                "className": "btn-primary"
              }
            }
          });
        }
      };

      var handleMissingFile = function() {
        var result = false;
        if ($scope.missingFile) {
          result = $scope.missingFile({});
        }

         if (!result) {
          bootbox.dialog({
            "message": 'A Dockerfile or an archive containing a Dockerfile is required',
            "title": "Missing Dockerfile",
            "buttons": {
              "close": {
                "label": "Close",
                "className": "btn-primary"
              }
            }
          });
        }
      };

      var startBuild = function(fileId) {
        $scope.building = true;

        var repo = $scope.repository;
        var data = {
          'file_id': fileId
        };

        var params = {
          'repository': repo.namespace + '/' + repo.name
        };

        ApiService.requestRepoBuild(data, params).then(function(resp) {
          $scope.building = false;
          $scope.uploading = false;

          if ($scope.buildStarted) {
            $scope.buildStarted({'build': resp});
          }
        }, function(resp) {
          $scope.building = false;
          $scope.uploading = false;

          handleBuildFailed(resp.message);
        });
      };

      var conductUpload = function(file, url, fileId, mimeType) {
        if ($scope.uploadStarted) {
          $scope.uploadStarted({});
        }

        var request = new XMLHttpRequest();
        request.open('PUT', url, true);
        request.setRequestHeader('Content-Type', mimeType);
        request.onprogress = function(e) {
          $scope.$apply(function() {
            var percentLoaded;
            if (e.lengthComputable) {
              $scope.upload_progress = (e.loaded / e.total) * 100;
            }
          });
        };
        request.onerror = function() {
          $scope.$apply(function() {
            handleUploadFailed();
          });
        };
        request.onreadystatechange = function() {
          var state = request.readyState;
          if (state == 4) {
            $scope.$apply(function() {
              startBuild(fileId);
              $scope.uploading = false;
            });
            return;
          }
        };
        request.send(file);
      };
      
      var startFileUpload = function(repo) {
        $scope.uploading = true;
        $scope.uploading_progress = 0;
        
        var uploader = $('#file-drop')[0];
        if (uploader.files.length == 0) {
          handleMissingFile();
          $scope.uploading = false;
          return;
        }

        var file = uploader.files[0];
        $scope.upload_file = file.name;
        
        var mimeType = file.type || 'application/octet-stream';
        var data = {
          'mimeType': mimeType
        };
      
        var getUploadUrl = ApiService.getFiledropUrl(data).then(function(resp) {
          conductUpload(file, resp.url, resp.file_id, mimeType);
        }, function() {
          handleUploadFailed('Could not retrieve upload URL');
        });
      };      

      $scope.$watch('internal.hasDockerfile', function(d) {
        $scope.hasDockerfile = d;
      });

      $scope.$watch('startNow', function() {
        if ($scope.startNow && $scope.repository && !$scope.uploading && !$scope.building) {
          startFileUpload();
        }
      });
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('locationView', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/location-view.html',
    replace: false,
    transclude: true,
    restrict: 'C',
    scope: {
      'location': '=location'
    },
    controller: function($rootScope, $scope, $element, $http, PingService) {
      var LOCATIONS = {
	'local_us': { 'country': 'US', 'data': 'quay-registry.s3.amazonaws.com', 'title': 'United States' },
	'local_eu': { 'country': 'EU', 'data': 'quay-registry-eu.s3-eu-west-1.amazonaws.com', 'title': 'Europe' },

	's3_us_east_1': { 'country': 'US', 'data': 'quay-registry.s3.amazonaws.com', 'title': 'United States (East)' },
	's3_us_west_1': { 'country': 'US', 'data': 'quay-registry-cali.s3.amazonaws.com', 'title': 'United States (West)' },

	's3_eu_west_1': { 'country': 'EU', 'data': 'quay-registry-eu.s3-eu-west-1.amazonaws.com', 'title': 'Europe' },

	's3_ap_southeast_1': { 'country': 'SG', 'data': 'quay-registry-singapore.s3-ap-southeast-1.amazonaws.com', 'title': 'Singapore' },
	's3_ap_southeast_2': { 'country': 'AU', 'data': 'quay-registry-sydney.s3-ap-southeast-2.amazonaws.com', 'title': 'Australia' },

//	's3_ap_northeast-1': { 'country': 'JP', 'data': 's3-ap-northeast-1.amazonaws.com', 'title': 'Japan' },
//	's3_sa_east1': { 'country': 'BR', 'data': 's3-east-1.amazonaws.com', 'title': 'Sao Paulo' }
      };

      $scope.locationPing = null;
      $scope.locationPingClass = null;

      $scope.getLocationTooltip = function(location, ping) {
	var tip = $scope.getLocationTitle(location) + '<br>';
        if (ping == null) {
	  tip += '(Loading)';
        } else if (ping < 0) {
	  tip += '<br><b>Note: Could not contact server</b>';
	} else {
	  tip += 'Estimated Ping: ' + (ping ? ping + 'ms' : '(Loading)');
	}
	return tip;
      };

      $scope.getLocationTitle = function(location) {
	if (!LOCATIONS[location]) {
	  return '(Unknown)';
	}
	return 'Image data is located in ' + LOCATIONS[location]['title'];
      };

      $scope.getLocationImage = function(location) {
	if (!LOCATIONS[location]) {
	  return 'unknown.png';
	}
	return LOCATIONS[location]['country'] + '.png';
      };

      $scope.getLocationPing = function(location) {
        var url = 'https://' + LOCATIONS[location]['data'] + '/okay.txt';
        PingService.pingUrl($scope, url, function(ping, success, count) {
          if (count == 3 || !success) {
            $scope.locationPing = success ? ping : -1;
          }
        });
      };

      $scope.$watch('location', function(location) {
	if (!location) { return; }
	$scope.getLocationPing(location);
      });

      $scope.$watch('locationPing', function(locationPing) {
	if (locationPing == null) {
	  $scope.locationPingClass = null;
	  return;
	}

	if (locationPing < 0) {
	  $scope.locationPingClass = 'error';
	  return;
	}

	if (locationPing < 100) {
	  $scope.locationPingClass = 'good';
	  return;
	}

	if (locationPing < 250) {
	  $scope.locationPingClass = 'fair';
	  return;
	}

	if (locationPing < 500) {
	  $scope.locationPingClass = 'barely';
	  return;
	}

	$scope.locationPingClass = 'poor';
      });
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('tagSpecificImagesView', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/tag-specific-images-view.html',
    replace: false,
    transclude: true,
    restrict: 'C',
    scope: {
      'repository': '=repository',
      'tag': '=tag',
      'images': '=images',
      'imageCutoff': '=imageCutoff'
    },
    controller: function($scope, $element) {
      $scope.getFirstTextLine = getFirstTextLine;

      $scope.hasImages = false;
      $scope.tagSpecificImages = [];

      $scope.getImageListingClasses = function(image) {
        var classes = '';
        if (image.ancestors.length > 1) {
          classes += 'child ';
        }
 
        var currentTag = $scope.repository.tags[$scope.tag];
        if (image.id == currentTag.image_id) {
          classes += 'tag-image ';
        }
        
        return classes;
      };

      var forAllTagImages = function(tag, callback, opt_cutoff) {
        if (!tag) { return; }

        if (!$scope.imageByDockerId) {
          $scope.imageByDockerId = [];
          for (var i = 0; i < $scope.images.length; ++i) {
            var currentImage = $scope.images[i];
            $scope.imageByDockerId[currentImage.id] = currentImage;
          }
        }

        var tag_image = $scope.imageByDockerId[tag.image_id];
        if (!tag_image) {
          return;
        }
        
        callback(tag_image);

        var ancestors = tag_image.ancestors.split('/').reverse();
        for (var i = 0; i < ancestors.length; ++i) {
          var image = $scope.imageByDockerId[ancestors[i]];
          if (image) {
            if (image == opt_cutoff) {
              return;
            }
            
            callback(image);
          }
        }
      };

      var refresh = function() {
        if (!$scope.repository || !$scope.tag || !$scope.images) {
          $scope.tagSpecificImages = [];
          return;
        }

        var tag = $scope.repository.tags[$scope.tag];
        if (!tag) {
          $scope.tagSpecificImages = [];
          return;
        }
        
        var getIdsForTag = function(currentTag) {
          var ids = {};
          forAllTagImages(currentTag, function(image) {
            ids[image.id] = true;
          }, $scope.imageCutoff);
          return ids;
        };

        // Remove any IDs that match other tags.
        var toDelete = getIdsForTag(tag);
        for (var currentTagName in $scope.repository.tags) {
          var currentTag = $scope.repository.tags[currentTagName];
          if (currentTag != tag) {
            for (var id in getIdsForTag(currentTag)) {
              delete toDelete[id];
            }
          }
        }

        // Return the matching list of images.
        var images = [];
        for (var i = 0; i < $scope.images.length; ++i) {
          var image = $scope.images[i];
          if (toDelete[image.id]) {
            images.push(image);
          }
        }
        
        images.sort(function(a, b) {
          var result = new Date(b.created) - new Date(a.created);
          if (result != 0) {
            return result;
          }

          return b.sort_index - a.sort_index;
        });

        $scope.tagSpecificImages = images;
      };

      $scope.$watch('repository', refresh);
      $scope.$watch('tag', refresh);
      $scope.$watch('images', refresh);
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('fallbackSrc', function () {
  return {
    restrict: 'A',
    link: function postLink(scope, element, attributes) {
      element.bind('error', function() {
        angular.element(this).attr("src", attributes.fallbackSrc);
      });
    }
  };
});


// Note: ngBlur is not yet in Angular stable, so we add it manaully here.
quayApp.directive('ngBlur', function() {
  return function( scope, elem, attrs ) {
    elem.bind('blur', function() {
      scope.$apply(attrs.ngBlur);
    });
  };
});

quayApp.directive("filePresent", [function () {
  return {
    restrict: 'A',
    scope: {
      'filePresent': "="
    },
    link: function (scope, element, attributes) {
      element.bind("change", function (changeEvent) {
        scope.$apply(function() {
          scope.filePresent = changeEvent.target.files.length > 0;
        });
      });
    }
  }
}]);

quayApp.directive('ngVisible', function () {
  return function (scope, element, attr) {
    scope.$watch(attr.ngVisible, function (visible) {
      element.css('visibility', visible ? 'visible' : 'hidden');
    });
  };
});

quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanService', '$http', '$timeout', 'CookieService', 'Features', '$anchorScroll',
  function($location, $rootScope, Restangular, UserService, PlanService, $http, $timeout, CookieService, Features, $anchorScroll) {

  // Handle session security.
  Restangular.setDefaultRequestParams(['post', 'put', 'remove', 'delete'], {'_csrf_token': window.__token || ''});

  // Handle session expiration.
  Restangular.setErrorInterceptor(function(response) {
    if (response.status == 401 && response.data['error_type'] == 'invalid_token' &&
        response.data['session_required'] !== false) {
      $('#sessionexpiredModal').modal({});
      return false;
    }

    if (response.status == 503) {
      $('#cannotContactService').modal({});
      return false;
    }

    if (response.status == 500) {
      document.location = '/500';
      return false;
    }

    return true;
  });

  // Check if we need to redirect based on a previously chosen plan.
  var result = PlanService.handleNotedPlan();

  // Check to see if we need to show a redirection page.
  var redirectUrl = CookieService.get('quay.redirectAfterLoad');
  CookieService.clear('quay.redirectAfterLoad');

  if (!result && redirectUrl && redirectUrl.indexOf(window.location.href) == 0) {
    window.location = redirectUrl;
    return;
  }

  var changeTab = function(activeTab, opt_timeout) {
    var checkCount = 0;

    $timeout(function() {
      if (checkCount > 5) { return; }
      checkCount++;

      $('a[data-toggle="tab"]').each(function(index) {
        var tabName = this.getAttribute('data-target').substr(1);
        if (tabName != activeTab) {
          return;
        }

        if (this.clientWidth == 0) {
          changeTab(activeTab, 500);
          return;
        }

        clickElement(this);
      });
    }, opt_timeout);
  };

  var resetDefaultTab = function() {
    $timeout(function() {
      $('a[data-toggle="tab"]').each(function(index) {
        if (index == 0) {
          clickElement(this);
        }
      });
    });
  };

  $rootScope.$watch('description', function(description) {
    if (!description) {
      description = 'Hosted private docker repositories. Includes full user management and history. Free for public repositories.';
    }
    
    // Note: We set the content of the description tag manually here rather than using Angular binding
    // because we need the <meta> tag to have a default description that is not of the form "{{ description }}",
    // we read by tools that do not properly invoke the Angular code.
    $('#descriptionTag').attr('content', description);    
  });

  $rootScope.$on('$routeUpdate', function(){
    if ($location.search()['tab']) {
      changeTab($location.search()['tab']);
    } else {
      resetDefaultTab();
    }
  });

  $rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
    $rootScope.pageClass = '';
    $rootScope.current = current.$$route;

    if (!current.$$route) { return; }

    if (current.$$route.title) {
      $rootScope.title = current.$$route.title;
    }

    if (current.$$route.pageClass) {
      $rootScope.pageClass = current.$$route.pageClass;
    }

    if (current.$$route.description) {
      $rootScope.description = current.$$route.description;
    } else {
      $rootScope.description = '';
    }

    $rootScope.fixFooter = !!current.$$route.fixFooter;
    $anchorScroll();
  });

  $rootScope.$on('$viewContentLoaded', function(event, current) {
    var activeTab = $location.search()['tab'];

    // Setup deep linking of tabs. This will change the search field of the URL whenever a tab
    // is changed in the UI.
    $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
      var tabName = e.target.getAttribute('data-target').substr(1);
      $rootScope.$apply(function() {
        var isDefaultTab = $('a[data-toggle="tab"]')[0] == e.target;
        var newSearch = $.extend($location.search(), {});
        if (isDefaultTab) {
          delete newSearch['tab'];
        } else {
          newSearch['tab'] = tabName;
        }

        $location.search(newSearch);
      });
      
      e.preventDefault();        
    });

    if (activeTab) {
      changeTab(activeTab);
    }
  });

  var initallyChecked = false;
  window.__isLoading = function() {
    if (!initallyChecked) {
      initallyChecked = true;
      return true;
    }
    return $http.pendingRequests.length > 0;
  };
}]);