text: // Copyright 2013 The Chromium Authors. All rights reserved.
  // Use of this source code is governed by a BSD-style license that can be
  // found in the LICENSE file.
  
  // clang-format off
  // #import {ProgressCenterPanelInterface} from '../../../externs/progress_center_panel.m.js';
  // #import {assertNotReached} from 'chrome://resources/js/assert.m.js';
  // #import {strf, str} from '../../../common/js/util.m.js';
  // #import {ProgressItemType, ProgressItemState, ProgressCenterItem} from '../../../common/js/progress_center_common.m.js';
  // clang-format on
  
  /**
   * Progress center panel.
   * @implements {ProgressCenterPanelInterface}
   */
  /* #export */ class ProgressCenterPanel {
    constructor() {
      /**
       * Reference to the feedback panel host.
       * TODO(crbug.com/947388) Add closure annotation here.
       */
      this.feedbackHost_ = document.querySelector('#progress-panel');
  
      /**
       * Items that are progressing, or completed.
       * Key is item ID.
       * @private {!Object<ProgressCenterItem>}
       */
      this.items_ = {};
  
      /**
       * Callback to be called with the ID of the progress item when the cancel
       * button is clicked.
       * @type {?function(string)}
       */
      this.cancelCallback = null;
  
      /**
       * Callback to be called with the ID of the error item when user pressed
       * dismiss button of it.
       * @type {?function(string)}
       */
      this.dismissErrorItemCallback = null;
  
      /**
       * Timeout for hiding file operations in progress.
       * @type {number}
       */
      this.PENDING_TIME_MS_ = 2000;
      if (window.IN_TEST) {
        this.PENDING_TIME_MS_ = 0;
      }
    }
  
    /**
     * Generate source string for display on the feedback panel.
     * @param {!ProgressCenterItem} item Item we're generating a message for.
     * @param {Object} info Cached information to use for formatting.
     * @return {string} String formatted based on the item state.
     */
    generateSourceString_(item, info) {
      switch (item.state) {
        case 'progressing':
          if (item.itemCount === 1) {
            if (item.type === ProgressItemType.COPY) {
              return strf('COPY_FILE_NAME', info['source']);
            } else if (item.type === ProgressItemType.MOVE) {
              return strf('MOVE_FILE_NAME', info['source']);
            } else {
              return item.message;
            }
          } else {
            if (item.type === ProgressItemType.COPY) {
              return strf('COPY_ITEMS_REMAINING', info['source']);
            } else if (item.type === ProgressItemType.MOVE) {
              return strf('MOVE_ITEMS_REMAINING', info['source']);
            } else {
              return item.message;
            }
          }
          break;
        case 'completed':
          if (info['count'] > 1) {
            return strf('FILE_ITEMS', info['source']);
          }
          return info['source'] || item.message;
        case 'error':
          return item.message;
        case 'canceled':
          return '';
        default:
          assertNotReached();
          break;
      }
      return '';
    }
  
    /**
     * Test if we have an empty or all whitespace string.
     * @param {string} candidate String we're checking.
     * @return {boolean} true if there's content in the candidate.
     */
    isNonEmptyString_(candidate) {
      if (!candidate || candidate.trim().length === 0) {
        return false;
      }
      return true;
    }
  
    /**
     * Generate primary text string for display on the feedback panel.
     * It is used for TransferDetails mode.
     * @param {!ProgressCenterItem} item Item we're generating a message for.
     * @param {Object} info Cached information to use for formatting.
     * @return {string} String formatted based on the item state.
     */
    generatePrimaryString_(item, info) {
      const hasDestination = this.isNonEmptyString_(info['destination']);
      switch (item.state) {
        case 'progressing':
          // Source and primary string are the same for missing destination.
          if (!hasDestination) {
            return this.generateSourceString_(item, info);
          }
          // fall through
        case 'completed':
          if (item.itemCount === 1) {
            if (item.type === ProgressItemType.COPY) {
              if (hasDestination) {
                return strf(
                    'COPY_FILE_NAME_LONG', info['source'], info['destination']);
              } else {
                return strf('FILE_COPIED', info['source']);
              }
            } else if (item.type === ProgressItemType.MOVE) {
              if (hasDestination) {
                return strf(
                    'MOVE_FILE_NAME_LONG', info['source'], info['destination']);
              } else {
                return strf('FILE_MOVED', info['source']);
              }
            } else {
              return item.message;
            }
          } else {
            if (item.type === ProgressItemType.COPY) {
              if (hasDestination) {
                return strf(
                    'COPY_ITEMS_REMAINING_LONG', info['source'],
                    info['destination']);
              } else {
                return strf('FILE_ITEMS_COPIED', info['source']);
              }
            } else if (item.type === ProgressItemType.MOVE) {
              if (hasDestination) {
                return strf(
                    'MOVE_ITEMS_REMAINING_LONG', info['source'],
                    info['destination']);
              } else {
                return strf('FILE_ITEMS_MOVED', info['source']);
              }
            } else {
              return item.message;
            }
          }
          break;
        case 'error':
          return item.message;
        case 'canceled':
          return '';
        default:
          assertNotReached();
          break;
      }
      return '';
    }
  
    /**
     * Generates remaining time message with formatted time.
     *
     * The time format in hour and minute and the durations more
     * than 24 hours also formatted in hour.
     *
     * As ICU syntax is not implemented in web ui yet (crbug/481718), the i18n
     * of time part is handled using Intl methods.
     *
     * @param {!ProgressCenterItem} item Item we're generating a message for.
     * @return {!string} Remaining time message.
     */
    generateRemainingTimeMessage(item) {
      const seconds = item.remainingTime;
      if (seconds == 0 && item.state == 'progressing') {
        return str('PENDING_LABEL');
      }
  
      // Return empty string for not supported operation (didn't set
      // remainingTime) or 0 sec remainingTime in non progressing state.
      if (!seconds) {
        return '';
      }
  
      const hours = Math.floor(seconds / 3600);
      const minutes = Math.floor((seconds % 3600) / 60);
  
      const hourFormatter = new Intl.NumberFormat(
          navigator.language, {style: 'unit', unit: 'hour', unitDisplay: 'long'});
      const minuteFormatter = new Intl.NumberFormat(
          navigator.language,
          {style: 'unit', unit: 'minute', unitDisplay: 'short'});
  
      if (hours > 0 && minutes > 0) {
        return strf(
            'TIME_REMAINING_ESTIMATE_2', hourFormatter.format(hours),
            minuteFormatter.format(minutes));
      } else if (hours > 0) {
        return strf('TIME_REMAINING_ESTIMATE', hourFormatter.format(hours));
      } else if (minutes > 0) {
        return strf('TIME_REMAINING_ESTIMATE', minuteFormatter.format(minutes));
      } else {
        // Round up to 1 min for short period of remaining time.
        return strf('TIME_REMAINING_ESTIMATE', minuteFormatter.format(1));
      }
    }
  
    /**
     * Process item updates for feedback panels.
     * @param {!ProgressCenterItem} item Item being updated.
     * @param {?ProgressCenterItem} newItem Item updating with new content.
     * @suppress {checkTypes}
     * TODO(crbug.com/947388) Remove the suppress, and fix closure compile.
     */
    updateFeedbackPanelItem(item, newItem) {
      let panelItem = this.feedbackHost_.findPanelItemById(item.id);
      if (newItem) {
        if (!panelItem) {
          panelItem = this.feedbackHost_.createPanelItem(item.id);
          // Show the panel only for long running operations.
          setTimeout(() => {
            this.feedbackHost_.attachPanelItem(panelItem);
          }, this.PENDING_TIME_MS_);
          if (item.type === 'format') {
            panelItem.panelType = panelItem.panelTypeFormatProgress;
          } else if (item.type === 'sync') {
            panelItem.panelType = panelItem.panelTypeSyncProgress;
          } else {
            panelItem.panelType = panelItem.panelTypeProgress;
          }
          panelItem.userData = {
            'source': item.sourceMessage,
            'destination': item.destinationMessage,
            'count': item.itemCount,
          };
        }
  
        const primaryText = this.generatePrimaryString_(item, panelItem.userData);
        panelItem.secondaryText = this.generateRemainingTimeMessage(item);
        panelItem.primaryText = primaryText;
        panelItem.setAttribute('data-progress-id', item.id);
  
        // On progress panels, make the cancel button aria-label more useful.
        const cancelLabel = strf('CANCEL_ACTIVITY_LABEL', primaryText);
        panelItem.closeButtonAriaLabel = cancelLabel;
        panelItem.signalCallback = (signal) => {
          if (signal === 'cancel' && item.cancelCallback) {
            item.cancelCallback();
          }
          if (signal === 'dismiss') {
            this.feedbackHost_.removePanelItem(panelItem);
            this.dismissErrorItemCallback(item.id);
          }
        };
        panelItem.progress = item.progressRateInPercent.toString();
        switch (item.state) {
          case 'completed':
            // Create a completed panel for copies, moves and formats.
            // TODO(crbug.com/947388) decide if we want these for delete, etc.
            if (item.type === 'copy' || item.type === 'move' ||
                item.type === 'format') {
              const donePanelItem = this.feedbackHost_.addPanelItem(item.id);
              donePanelItem.id = item.id;
              donePanelItem.panelType = donePanelItem.panelTypeDone;
              donePanelItem.primaryText = primaryText;
              donePanelItem.secondaryText = str('COMPLETE_LABEL');
              donePanelItem.signalCallback = (signal) => {
                if (signal === 'dismiss') {
                  this.feedbackHost_.removePanelItem(donePanelItem);
                  delete this.items_[donePanelItem.id];
                }
              };
              // Delete after 4 seconds, doesn't matter if it's manually deleted
              // before the timer fires, as removePanelItem handles that case.
              setTimeout(() => {
                this.feedbackHost_.removePanelItem(donePanelItem);
                delete this.items_[donePanelItem.id];
              }, 4000);
            }
            // Drop through to remove the progress panel.
          case 'canceled':
            // Remove the feedback panel when complete.
            this.feedbackHost_.removePanelItem(panelItem);
            break;
          case 'error':
            panelItem.panelType = panelItem.panelTypeError;
            panelItem.primaryText = item.message;
            panelItem.secondaryText = '';
            // Make sure the panel is attached so it shows immediately.
            this.feedbackHost_.attachPanelItem(panelItem);
            break;
        }
      } else if (panelItem) {
        this.feedbackHost_.removePanelItem(panelItem);
      }
    }
  
    /**
     * Starts the item update and checks state changes.
     * @param {!ProgressCenterItem} item Item containing updated information.
     */
    updateItemState_(item) {
      // Compares the current state and the new state to check if the update is
      // valid or not.
      const previousItem = this.items_[item.id];
      switch (item.state) {
        case ProgressItemState.ERROR:
          if (previousItem &&
              previousItem.state !== ProgressItemState.PROGRESSING) {
            return;
          }
          this.items_[item.id] = item.clone();
          break;
  
        case ProgressItemState.PROGRESSING:
        case ProgressItemState.COMPLETED:
          if ((!previousItem && item.state === ProgressItemState.COMPLETED) ||
              (previousItem &&
               previousItem.state !== ProgressItemState.PROGRESSING)) {
            return;
          }
          this.items_[item.id] = item.clone();
          break;
  
        case ProgressItemState.CANCELED:
          if (!previousItem ||
              previousItem.state !== ProgressItemState.PROGRESSING) {
            return;
          }
          delete this.items_[item.id];
      }
    }
  
    /**
     * Updates an item to the progress center panel.
     * @param {!ProgressCenterItem} item Item including new contents.
     */
    updateItem(item) {
      this.updateItemState_(item);
  
      // Update an open view item.
      const newItem = this.items_[item.id] || null;
      this.updateFeedbackPanelItem(item, newItem);
    }
  
    /**
     * Called by background page when an error dialog is dismissed.
     * @param {string} id Item id.
     */
    dismissErrorItem(id) {
      delete this.items_[id];
    }
  }
  