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 getRestUrl(args) {
  var url = '';
  for (var i = 0; i < arguments.length; ++i) {
    if (i > 0) {
       url += '/';
    }
    url += encodeURI(arguments[i])
  }
  return url;
}

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

// Start the application code itself.
quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics', 'angulartics.mixpanel', '$strap.directives', 'ngCookies'], function($provide) {
    $provide.factory('UserService', ['Restangular', 'PlanService', function(Restangular, PlanService) {
      var userResponse = {
        verified: false,
        anonymous: true,
        username: null,
        email: null,
        askForPassword: false,
        organizations: []
      }

      var userService = {}
      var currentSubscription = null;

      userService.load = function() {
        var userFetch = Restangular.one('user/');
        userFetch.get().then(function(loadedUser) {
          userResponse = loadedUser;

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

      userService.resetCurrentSubscription = function() {
        currentSubscription = null;
      };

      userService.getCurrentSubscription = function(callback, failure) {
        if (currentSubscription) { callback(currentSubscription); }
        PlanService.getSubscription(null, function(sub) {
          currentSubscription = sub;
          callback(sub);
        }, failure);
      };

      userService.currentUser = function() {
        return userResponse;
      }

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

      return userService;
    }]);

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

      if ($location.host() === 'quay.io') {
        keyService['stripePublishableKey'] = 'pk_live_P5wLU0vGdHnZGyKnXlFG4oiu';
        keyService['githubClientId'] = '5a8c08b06c48d89d4d1e';
      } else {        
        keyService['stripePublishableKey'] = 'pk_test_uEDHANKm9CHCvVa2DLcipGRh';
        keyService['githubClientId'] = 'cfbc4aca88e5c1b40679';
      }

      return keyService;
    }]);

    $provide.factory('PlanService', ['Restangular', 'KeyService', function(Restangular, KeyService) {
      var plans = null;
      var planDict = {};
      var planService = {}

      planService.verifyLoaded = function(callback) {
        if (plans) {
          callback(plans);
          return;
        }

        var getPlans = Restangular.one('plans');
        getPlans.get().then(function(data) {
          var i = 0;
          for(i = 0; i < data.user.length; i++) {
            planDict[data.user[i].stripeId] = data.user[i];
          }
          for(i = 0; i < data.business.length; i++) {
            planDict[data.business[i].stripeId] = data.business[i];
          }
          plans = data;
          callback(plans);
        }, function() { callback([]); });
      };

      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.getPlans = function(callback) {
        planService.verifyLoaded(callback);
      };

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

      planService.getMinimumPlan = function(privateCount, isBusiness, callback) {
        planService.verifyLoaded(function() {
          var planSource = plans.user;
          if (isBusiness) {
            planSource = plans.business;
          }

          for (var i = 0; i < planSource.length; i++) {
            var plan = planSource[i];
            if (plan.privateRepos >= privateCount) {
              callback(plan);
              return;
            }
          }

          callback(null);
        });
      };

      planService.getSubscription = function(organization, success, failure) {
        var url = planService.getSubscriptionUrl(organization);
        var getSubscription = Restangular.one(url);
        getSubscription.get().then(success, failure);
      };

      planService.getSubscriptionUrl = function(orgname) {
        return orgname ? getRestUrl('organization', orgname, 'plan') : 'user/plan';
      };

      planService.setSubscription = function(orgname, planId, success, failure, opt_token) {
        var subscriptionDetails = {
          plan: planId
        };

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

        var url = planService.getSubscriptionUrl(orgname);
        var createSubscriptionRequest = Restangular.one(url);
        createSubscriptionRequest.customPUT(subscriptionDetails).then(success, failure);
      };

      planService.changePlan = function($scope, orgname, planId, hasExistingSubscription, started, success, failure) {
        if (!hasExistingSubscription) {
          planService.showSubscribeDialog($scope, orgname, planId, started, success, failure);
          return;
        }
        
        started();
        planService.setSubscription(orgname, planId, success, failure);
      };

      planService.showSubscribeDialog = function($scope, orgname, planId, started, success, failure) {
        var submitToken = function(token) {
          mixpanel.track('plan_subscribe');

          $scope.$apply(function() {
            started();
            planService.setSubscription(orgname, planId, success, failure);
          });
        };

        planService.getPlan(planId, function(planDetails) {
          StripeCheckout.open({
            key:         KeyService.stripePublishableKey,
            address:     false,
            amount:      planDetails.price,
            currency:    'usd',
            name:        'Quay ' + planDetails.title + ' Subscription',
            description: 'Up to ' + planDetails.privateRepos + ' private repositories',
            panelLabel:  'Subscribe',
            token:       submitToken,
            image:       'static/img/quay-icon-stripe.png'
          });
        });
      };

      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', '$analyticsProvider',
    function($routeProvider, $locationProvider, $analyticsProvider) {

    $analyticsProvider.virtualPageviews(true);

    $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, reloadOnSearch: false}).
      when('/repository/:namespace/:name/tag/:tag', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl}).
      when('/repository/:namespace/:name/image/:image', {templateUrl: '/static/partials/image-view.html', controller: ImageViewCtrl}).
      when('/repository/:namespace/:name/admin', {templateUrl: '/static/partials/repo-admin.html', controller:RepoAdminCtrl}).
      when('/repository/', {title: 'Repositories', templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl}).
      when('/user/', {title: 'Account Settings', templateUrl: '/static/partials/user-admin.html', controller: UserAdminCtrl}).
      when('/guide/', {title: 'Guide', templateUrl: '/static/partials/guide.html', controller: GuideCtrl}).
      when('/plans/', {title: 'Plans and Pricing', templateUrl: '/static/partials/plans.html', controller: PlansCtrl}).
      when('/signin/', {title: 'Sign In', templateUrl: '/static/partials/signin.html', controller: SigninCtrl}).
      when('/new/', {title: 'Create new repository', templateUrl: '/static/partials/new-repo.html', controller: NewRepoCtrl}).

      when('/organizations/', {title: 'Organizations', templateUrl: '/static/partials/organizations.html', controller: OrgsCtrl}).
      when('/organizations/new/', {title: 'New Organization', 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}).
      when('/organization/:orgname/teams/:teamname', {templateUrl: '/static/partials/team-view.html', controller: TeamViewCtrl}).

      when('/v1/', {title: 'Activation information', templateUrl: '/static/partials/v1-page.html', controller: V1Ctrl}).

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


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) {
      $scope.getMarkedDown = function(content, firstLineOnly) {
        if (firstLineOnly) {
          content = getFirstTextLine(content);
        }
        return getMarkedDown(content);
      };
    }
  };
  return directiveDefinitionObject;
});


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('signinForm', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/signin-form.html',
    replace: false,
    transclude: true,
    restrict: 'C',
    scope: {
      'redirectUrl': '=redirectUrl'
    },
    controller: function($scope, $location, $timeout, Restangular, KeyService, UserService) {
      $scope.githubClientId = KeyService.githubClientId;

      var appendMixpanelId = function() {
        if (mixpanel.get_distinct_id !== undefined) {
          $scope.mixpanelDistinctIdClause = "&state=" + mixpanel.get_distinct_id();
        } else {
          // Mixpanel not yet loaded, try again later
          $timeout(appendMixpanelId, 200);
        }
      };

      appendMixpanelId();

      $scope.signin = function() {
        var signinPost = Restangular.one('signin');
        signinPost.customPOST($scope.user).then(function() {
          $scope.needsEmailVerification = false;
          $scope.invalidCredentials = false;

          // Redirect to the specified page or the landing page
          UserService.load();
          $location.path($scope.redirectUrl ? $scope.redirectUrl : '/');
        }, function(result) {
          $scope.needsEmailVerification = result.data.needsEmailVerification;
          $scope.invalidCredentials = result.data.invalidCredentials;
        });
      };
    }
  };
  return directiveDefinitionObject;
});


quayApp.directive('plansTable', function () {
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/plans-table.html',
    replace: false,
    transclude: true,
    restrict: 'C',
    scope: {
      'plans': '=plans',
      'currentPlan': '=currentPlan'
    },
    controller: function($scope, $element) {
      $scope.setPlan = function(plan) {
        $scope.currentPlan = plan;
      };
    }
  };
  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'
    },
    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('entitySearch', function () {
  var number = 0;
  var directiveDefinitionObject = {
    priority: 0,
    templateUrl: '/static/directives/entity-search.html',
    replace: false,
    transclude: false,
    restrict: 'C',
    scope: {
      'organization': '=organization',
      'inputTitle': '=inputTitle',
      'entitySelected': '=entitySelected'      
    },
    controller: function($scope, $element) {
      if (!$scope.entitySelected) { return; }

      number++;

      var input = $element[0].firstChild;
      $scope.organization = $scope.organization || '';
      $(input).typeahead({
        name: 'entities' + number,
        remote: {
          url: '/api/entities/%QUERY',
          replace: function (url, uriEncodedQuery) {
              url = url.replace('%QUERY', uriEncodedQuery);
              if ($scope.organization) {
                url += '?organization=' + encodeURIComponent($scope.organization);
              }
              return url;
          },
          filter: function(data) {
            var datums = [];
            for (var i = 0; i < data.results.length; ++i) {
              var entity = data.results[i];
              datums.push({
                'value': entity.name,
                'tokens': [entity.name],
                'entity': entity
              });
            }
            return datums;
          }
        },
        template: function (datum) {
          template = '<div class="entity-mini-listing">';
          if (datum.entity.kind == 'user') {
            template += '<i class="fa fa-user fa-lg"></i>';
          } else if (datum.entity.kind == 'team') {
            template += '<i class="fa fa-group fa-lg"></i>';
          }
          template += '<span class="name">' + datum.value + '</span>';

          if (datum.entity.is_org_member !== undefined && !datum.entity.is_org_member) {
            template += '<div class="alert-warning warning">This user is outside your organization</div>';
          }

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

      $(input).on('typeahead:selected', function(e, datum) {
        $(input).typeahead('setQuery', '');
        $scope.entitySelected(datum.entity);
      });

      $scope.$watch('inputTitle', function(title) {
        input.setAttribute('placeholder', title);
      });
    }
  };
  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('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'
    },
    controller: function($scope, $element, PlanService, Restangular) {
      var hasSubscription = false;
        
      $scope.getActiveSubClass = function() {
        return 'active';
      };

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

        PlanService.changePlan($scope, $scope.organization, planId, hasSubscription, function() {
          // Started.
          $scope.planChanging = true;
        }, function(sub) {
          // Success.
          subscribedToPlan(sub);
        }, function() {
          // Failure.
          $scope.planChanging = false;
        });
      };

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

      var subscribedToPlan = function(sub) {
        $scope.subscription = sub;

        if (sub.plan != getFreePlan()) {
          hasSubscription = true;
        }

        PlanService.getPlan(sub.plan, function(subscribedPlan) {
          $scope.subscribedPlan = subscribedPlan;
          $scope.planUsagePercent = sub.usedPrivateRepos * 100 / $scope.subscribedPlan.privateRepos;

          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 RepositoryUsageChart();
            $scope.chart.draw('repository-usage-chart');
          }

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

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

      var getFreePlan = function() {
        for (var i = 0; i < $scope.plans.length; ++i) {
          if ($scope.plans[i].price == 0) {
            return $scope.plans[i].stripeId;
          }
        }
        return 'free';
      };

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

        PlanService.getSubscription($scope.organization, subscribedToPlan, function() {
          // User/Organization has no subscription.
          subscribedToPlan({ 'plan': getFreePlan() });
        });
      };

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

        $scope.loadingPlans = true;
        PlanService.getPlans(function(plans) {
          $scope.plans = plans[$scope.organization ? 'business' : 'user'];
          update();
            
          if ($scope.readyForPlan) {
            var planRequested = $scope.readyForPlan();
            if (planRequested && planRequested != getFreePlan()) {
              $scope.changeSubscription(planRequested);
            }
          }
        });
      };

      // 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, $cookieStore) {
      $scope.namespaces = {};

      $scope.initialize = function(user) {
        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];
          }
        }

        var initialNamespace = $routeParams['namespace'] || $cookieStore.get('quay.currentnamespace') || $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;
        $cookieStore.put('quay.currentnamespace', newNamespace);
      };

      $scope.$watch('user', function(user) {
        $scope.user = user;
        $scope.initialize(user);
      });
    }
  };
  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) {
        $scope.getBuildProgress = function(buildInfo) {
            switch (buildInfo.status) {
              case 'building':
                return (buildInfo.current_command / buildInfo.total_commands) * 100;
                break;
                
              case 'pushing':
                var imagePercentDecimal = (buildInfo.image_completion_percent / 100);
                return ((buildInfo.current_image + imagePercentDecimal) / buildInfo.total_images) * 100;
                break;

              case 'complete':
                return 100;
                break;

              case 'initializing':
              case 'starting':
              case 'waiting':
                return 0;
                break;
            }
            
            return -1;
        };

        $scope.getBuildMessage = function(buildInfo) {
            switch (buildInfo.status) {
            case 'initializing':
                return 'Starting Dockerfile build';
                break;

            case 'starting':
            case 'waiting':
            case 'building':
                return 'Building image from Dockerfile';
                break;

            case 'pushing':
                return 'Pushing image built from Dockerfile';
                break;

            case 'complete':
                return 'Dockerfile build completed and pushed';
                break;
                
            case 'error':
                return 'Dockerfile build failed: ' + buildInfo.message;
                break;
            }
        };
    }
  };
  return directiveDefinitionObject;
});

// 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.run(['$location', '$rootScope', function($location, $rootScope) {
  $rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
    if (current.$$route.title) {
      $rootScope.title = current.$$route.title;
    }
  });
}]);