text: /*
   * Licensed to the Apache Software Foundation (ASF) under one
   * or more contributor license agreements.  See the NOTICE file
   * distributed with this work for additional information
   * regarding copyright ownership.  The ASF licenses this file
   * to you under the Apache License, Version 2.0 (the
   * "License"); you may not use this file except in compliance
   * with the License.  You may obtain a copy of the License at
   *
   *   http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing,
   * software distributed under the License is distributed on an
   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   * KIND, either express or implied.  See the License for the
   * specific language governing permissions and limitations
   * under the License.
   */
  
  /**
   * A directive for displaying a non-global notification describing the status
   * of a specific Guacamole client, including prompts for any information
   * necessary to continue the connection.
   */
  angular.module('client').directive('guacClientNotification', [function guacClientNotification() {
  
      const directive = {
          restrict: 'E',
          replace: true,
          templateUrl: 'app/client/templates/guacClientNotification.html'
      };
  
      directive.scope = {
  
          /**
           * The client whose status should be displayed.
           *
           * @type ManagedClient
           */
          client : '='
  
      };
  
      directive.controller = ['$scope', '$injector', '$element',
          function guacClientNotificationController($scope, $injector, $element) {
  
          // Required types
          const ManagedClient      = $injector.get('ManagedClient');
          const ManagedClientState = $injector.get('ManagedClientState');
          const Protocol           = $injector.get('Protocol');
  
          // Required services
          const $location              = $injector.get('$location');
          const authenticationService  = $injector.get('authenticationService');
          const guacClientManager      = $injector.get('guacClientManager');
          const guacTranslate          = $injector.get('guacTranslate');
          const requestService         = $injector.get('requestService');
          const userPageService        = $injector.get('userPageService');
  
          /**
           * A Notification object describing the client status to display as a
           * dialog or prompt, as would be accepted by guacNotification.showStatus(),
           * or false if no status should be shown.
           *
           * @type {Notification|Object|Boolean}
           */
          $scope.status = false;
  
          /**
           * All error codes for which automatic reconnection is appropriate when a
           * client error occurs.
           */
          const CLIENT_AUTO_RECONNECT = {
              0x0200: true,
              0x0202: true,
              0x0203: true,
              0x0207: true,
              0x0208: true,
              0x0301: true,
              0x0308: true
          };
  
          /**
           * All error codes for which automatic reconnection is appropriate when a
           * tunnel error occurs.
           */
          const TUNNEL_AUTO_RECONNECT = {
              0x0200: true,
              0x0202: true,
              0x0203: true,
              0x0207: true,
              0x0208: true,
              0x0308: true
          };
  
          /**
           * Action which logs out from Guacamole entirely.
           */
          const LOGOUT_ACTION = {
              name      : "CLIENT.ACTION_LOGOUT",
              className : "logout button",
              callback  : function logoutCallback() {
                  authenticationService.logout()
                  ['catch'](requestService.IGNORE);
              }
          };
  
          /**
           * Action which returns the user to the home screen. If the home page has
           * not yet been determined, this will be null.
           */
          let NAVIGATE_HOME_ACTION = null;
  
          // Assign home page action once user's home page has been determined
          userPageService.getHomePage()
          .then(function homePageRetrieved(homePage) {
  
              // Define home action only if different from current location
              if ($location.path() !== homePage.url) {
                  NAVIGATE_HOME_ACTION = {
                      name      : "CLIENT.ACTION_NAVIGATE_HOME",
                      className : "home button",
                      callback  : function navigateHomeCallback() {
                          $location.url(homePage.url);
                      }
                  };
              }
  
          }, requestService.WARN);
  
          /**
           * Action which replaces the current client with a newly-connected client.
           */
          const RECONNECT_ACTION = {
              name      : "CLIENT.ACTION_RECONNECT",
              className : "reconnect button",
              callback  : function reconnectCallback() {
                  $scope.client = guacClientManager.replaceManagedClient($scope.client.id);
                  $scope.status = false;
              }
          };
  
          /**
           * The reconnect countdown to display if an error or status warrants an
           * automatic, timed reconnect.
           */
          const RECONNECT_COUNTDOWN = {
              text: "CLIENT.TEXT_RECONNECT_COUNTDOWN",
              callback: RECONNECT_ACTION.callback,
              remaining: 15
          };
  
          /**
           * Displays a notification at the end of a Guacamole connection, whether
           * that connection is ending normally or due to an error. As the end of
           * a Guacamole connection may be due to changes in authentication status,
           * this will also implicitly peform a re-authentication attempt to check
           * for such changes, possibly resulting in auth-related events like
           * guacInvalidCredentials.
           *
           * @param {Notification|Boolean|Object} status
           *     The status notification to show, as would be accepted by
           *     guacNotification.showStatus().
           */
          const notifyConnectionClosed = function notifyConnectionClosed(status) {
  
              // Re-authenticate to verify auth status at end of connection
              authenticationService.updateCurrentToken($location.search())
              ['catch'](requestService.IGNORE)
  
              // Show the requested status once the authentication check has finished
              ['finally'](function authenticationCheckComplete() {
                  $scope.status = status;
              });
  
          };
  
          /**
           * Notifies the user that the connection state has changed.
           *
           * @param {String} connectionState
           *     The current connection state, as defined by
           *     ManagedClientState.ConnectionState.
           */
          const notifyConnectionState = function notifyConnectionState(connectionState) {
  
              // Hide any existing status
              $scope.status = false;
  
              // Do not display status if status not known
              if (!connectionState)
                  return;
  
              // Build array of available actions
              let actions;
              if (NAVIGATE_HOME_ACTION)
                  actions = [ NAVIGATE_HOME_ACTION, RECONNECT_ACTION, LOGOUT_ACTION ];
              else
                  actions = [ RECONNECT_ACTION, LOGOUT_ACTION ];
  
              // Get any associated status code
              const status = $scope.client.clientState.statusCode;
  
              // Connecting
              if (connectionState === ManagedClientState.ConnectionState.CONNECTING
               || connectionState === ManagedClientState.ConnectionState.WAITING) {
                  $scope.status = {
                      className : "connecting",
                      title: "CLIENT.DIALOG_HEADER_CONNECTING",
                      text: {
                          key : "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase()
                      }
                  };
              }
  
              // Client error
              else if (connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR) {
  
                  // Translation IDs for this error code
                  const errorPrefix = "CLIENT.ERROR_CLIENT_";
                  const errorId = errorPrefix + status.toString(16).toUpperCase();
                  const defaultErrorId = errorPrefix + "DEFAULT";
  
                  // Determine whether the reconnect countdown applies
                  const countdown = (status in CLIENT_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null;
  
                  // Use the guacTranslate service to determine if there is a translation for
                  // this error code; if not, use the default
                  guacTranslate(errorId, defaultErrorId).then(
  
                      // Show error status
                      translationResult => notifyConnectionClosed({
                          className : "error",
                          title     : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR",
                          text      : {
                              key : translationResult.id
                          },
                          countdown : countdown,
                          actions   : actions
                      })
                  );
  
              }
  
              // Tunnel error
              else if (connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR) {
  
                  // Translation IDs for this error code
                  const errorPrefix = "CLIENT.ERROR_TUNNEL_";
                  const errorId = errorPrefix + status.toString(16).toUpperCase();
                  const defaultErrorId = errorPrefix + "DEFAULT";
  
                  // Determine whether the reconnect countdown applies
                  const countdown = (status in TUNNEL_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null;
  
                  // Use the guacTranslate service to determine if there is a translation for
                  // this error code; if not, use the default
                  guacTranslate(errorId, defaultErrorId).then(
  
                      // Show error status
                      translationResult => notifyConnectionClosed({
                          className : "error",
                          title     : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR",
                          text      : {
                              key : translationResult.id
                          },
                          countdown : countdown,
                          actions   : actions
                      })
                  );
  
              }
  
              // Disconnected
              else if (connectionState === ManagedClientState.ConnectionState.DISCONNECTED) {
                  notifyConnectionClosed({
                      title   : "CLIENT.DIALOG_HEADER_DISCONNECTED",
                      text    : {
                          key : "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase()
                      },
                      actions : actions
                  });
              }
  
              // Hide status for all other states
              else
                  $scope.status = false;
  
          };
  
          /**
           * Prompts the user to enter additional connection parameters. If the
           * protocol and associated parameters of the underlying connection are not
           * yet known, this function has no effect and should be re-invoked once
           * the parameters are known.
           *
           * @param {Object.<String, String>} requiredParameters
           *     The set of all parameters requested by the server via "required"
           *     instructions, where each object key is the name of a requested
           *     parameter and each value is the current value entered by the user.
           */
          const notifyParametersRequired = function notifyParametersRequired(requiredParameters) {
  
              /**
               * Action which submits the current set of parameter values, requesting
               * that the connection continue.
               */
              const SUBMIT_PARAMETERS = {
                  name      : "CLIENT.ACTION_CONTINUE",
                  className : "button",
                  callback  : function submitParameters() {
                      if ($scope.client) {
                          const params = $scope.client.requiredParameters;
                          $scope.client.requiredParameters = null;
                          ManagedClient.sendArguments($scope.client, params);
                      }
                  }
              };
  
              /**
               * Action which cancels submission of additional parameters and
               * disconnects from the current connection.
               */
              const CANCEL_PARAMETER_SUBMISSION = {
                  name      : "CLIENT.ACTION_CANCEL",
                  className : "button",
                  callback  : function cancelSubmission() {
                      $scope.client.requiredParameters = null;
                      $scope.client.client.disconnect();
                  }
              };
  
              // Attempt to prompt for parameters only if the parameters that apply
              // to the underlying connection are known
              if (!$scope.client.protocol || !$scope.client.forms)
                  return;
  
              // Prompt for parameters
              $scope.status = {
                  className : "parameters-required",
                  formNamespace : Protocol.getNamespace($scope.client.protocol),
                  forms : $scope.client.forms,
                  formModel : requiredParameters,
                  formSubmitCallback : SUBMIT_PARAMETERS.callback,
                  actions : [ SUBMIT_PARAMETERS, CANCEL_PARAMETER_SUBMISSION ]
              };
  
          };
  
          /**
           * Returns whether the given connection state allows for submission of
           * connection parameters via "argv" instructions.
           *
           * @param {String} connectionState
           *     The connection state to test, as defined by
           *     ManagedClientState.ConnectionState.
           *
           * @returns {boolean}
           *     true if the given connection state allows submission of connection
           *     parameters via "argv" instructions, false otherwise.
           */
          const canSubmitParameters = function canSubmitParameters(connectionState) {
              return (connectionState === ManagedClientState.ConnectionState.WAITING ||
                      connectionState === ManagedClientState.ConnectionState.CONNECTED);
          };
  
          // Show status dialog when connection status changes
          $scope.$watchGroup([
              'client.clientState.connectionState',
              'client.requiredParameters',
              'client.protocol',
              'client.forms'
          ], function clientStateChanged(newValues) {
  
              const connectionState = newValues[0];
              const requiredParameters = newValues[1];
  
              // Prompt for parameters only if parameters can actually be submitted
              if (requiredParameters && canSubmitParameters(connectionState))
                  notifyParametersRequired(requiredParameters);
  
              // Otherwise, just show general connection state
              else
                  notifyConnectionState(connectionState);
  
          });
  
          /**
           * Prevents the default behavior of the given AngularJS event if a
           * notification is currently shown and the client is focused.
           *
           * @param {event} e
           *     The AngularJS event to selectively prevent.
           */
          const preventDefaultDuringNotification = function preventDefaultDuringNotification(e) {
              if ($scope.status && $scope.client.clientProperties.focused)
                  e.preventDefault();
          };
  
          // Block internal handling of key events (by the client) if a
          // notification is visible
          $scope.$on('guacBeforeKeydown', preventDefaultDuringNotification);
          $scope.$on('guacBeforeKeyup', preventDefaultDuringNotification);
  
      }];
  
      return directive;
  
  }]);
  