# -*- coding: utf-8 -*-
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed 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.
"""Base classes for gsutil UI controller, UIThread and MainThreadUIQueue."""

from __future__ import absolute_import
from __future__ import print_function
from __future__ import division
from __future__ import unicode_literals

from collections import deque
import sys
import threading
import time

from six.moves import queue as Queue

from gslib.metrics import LogPerformanceSummaryParams
from gslib.metrics import LogRetryableError
from gslib.thread_message import FileMessage
from gslib.thread_message import FinalMessage
from gslib.thread_message import MetadataMessage
from gslib.thread_message import PerformanceSummaryMessage
from gslib.thread_message import ProducerThreadMessage
from gslib.thread_message import ProgressMessage
from gslib.thread_message import RetryableErrorMessage
from gslib.thread_message import SeekAheadMessage
from gslib.thread_message import StatusMessage
from gslib.utils import parallelism_framework_util
from gslib.utils.unit_util import DecimalShort
from gslib.utils.unit_util import HumanReadableWithDecimalPlaces
from gslib.utils.unit_util import MakeHumanReadable
from gslib.utils.unit_util import PrettyTime

_ZERO_TASKS_TO_DO_ARGUMENT = (
    parallelism_framework_util.ZERO_TASKS_TO_DO_ARGUMENT)


class EstimationSource(object):
  """enum for total size source."""
  # Integer to indicate total size came from the final ProducerThreadMessage.
  # It has priority over all other total_size sources.
  PRODUCER_THREAD_FINAL = 1
  # Integer to indicate total size came from SeekAheadThread.
  # It has priority over self.SEEK_AHEAD_THREAD and over
  # self.INDIVIDUAL_MESSAGES.
  SEEK_AHEAD_THREAD = 2
  # Integer to indicate total size came from a ProducerThread estimation.
  # It has priority over self.INDIVIDUAL_MESSAGES.
  PRODUCER_THREAD_ESTIMATE = 3
  # Stores the actual source from total_size. We start from FileMessages or
  # MetadataMessages.
  INDIVIDUAL_MESSAGES = 4
  # Note: this priority based model was used in case we add new sources for
  # total_size in the future. It also allows us to search for smaller numbers
  # (larger priorities) rather than having to list those with higher priority.


def BytesToFixedWidthString(num_bytes, decimal_places=1):
  """Adjusts proper width for printing num_bytes in readable format.

  Args:
    num_bytes: The number of bytes we must display.
    decimal_places: The standard number of decimal places.
  Returns:
    String of fixed width representing num_bytes.
  """
  human_readable = HumanReadableWithDecimalPlaces(num_bytes,
                                                  decimal_places=decimal_places)
  number_format = human_readable.split()
  if int(round(float(number_format[0]))) >= 1000:
    # If we are in the [1000:1024) range for the whole part of the number,
    # we must remove the decimal part.
    last_character = len(number_format[0]) - decimal_places - 1
    number_format[0] = number_format[0][:last_character]
  return '%9s' % (' '.join(number_format))


class StatusMessageManager(object):
  """General manager for common functions shared by data and metadata managers.

  This subclass has the responsibility of having a common constructor and the
  same handler for SeekAheadMessages and ProducerThreadMessages.
  """

  class _ThroughputInformation(object):
    """Class that contains all information needed for throughput calculation.

    This _ThroughputInformation is used to track progress and time at several
    points of our operation.
    """

    def __init__(self, progress, report_time):
      """Constructor of _ThroughputInformation.

      Args:
        progress: The current progress, in bytes/second or objects/second.
        report_time: Float representing when progress was reported (seconds
            since Epoch).
      """
      self.progress = progress
      self.time = report_time

  def __init__(self,
               update_message_period=1,
               update_spinner_period=0.6,
               sliding_throughput_period=5,
               first_throughput_latency=10,
               quiet_mode=False,
               custom_time=None,
               verbose=False,
               console_width=80):
    """Instantiates a StatusMessageManager.

    Args:
      update_message_period: Minimum period for refreshing and  displaying
                             new information. A non-positive value will ignore
                             any time restrictions imposed by this field, but
                             it will affect throughput and time remaining
                             estimations.
      update_spinner_period: Minimum period for refreshing and displaying the
                             spinner. A non-positive value will ignore
                             any time restrictions imposed by this field.
      sliding_throughput_period: Sliding period for throughput calculation. A
                                 non-positive value will make it impossible to
                                 calculate the throughput.
      first_throughput_latency: Minimum waiting time before actually displaying
                                throughput info. A non-positive value will
                                ignore any time restrictions imposed by this
                                field.
      quiet_mode: If True, do not print status messages (but still process
                  them for analytics reporting as necessary).
      custom_time: If a custom start_time is desired. Used for testing.
      verbose: Tells whether or not the operation is on verbose mode.
      console_width: Width to display on console. This should not adjust the
                     visual output, just the space padding. For proper
                     visualization, we recommend setting this field to at least
                     80.
    """
    self.update_message_period = update_message_period
    self.update_spinner_period = update_spinner_period
    self.sliding_throughput_period = sliding_throughput_period
    self.first_throughput_latency = first_throughput_latency
    self.quiet_mode = quiet_mode
    self.custom_time = custom_time
    self.verbose = verbose
    self.console_width = console_width

    # Initial estimation source for number of objects and total size
    # is through individual FileMessages or individual MetadataMessages,
    # depending on the StatusMessageManager superclass.
    self.num_objects_source = EstimationSource.INDIVIDUAL_MESSAGES
    self.total_size_source = EstimationSource.INDIVIDUAL_MESSAGES
    self.num_objects = 0
    # Only used on data operations. Will remain 0 for metadata operations.
    self.total_size = 0

    # Time at last info update displayed.
    self.refresh_message_time = (self.custom_time
                                 if self.custom_time else time.time())
    self.start_time = self.refresh_message_time
    # Time at last spinner update.
    self.refresh_spinner_time = self.refresh_message_time

    # Measured in objects/second or bytes/second, depending on the superclass.
    self.throughput = 0.0
    # Deque of _ThroughputInformation to help with throughput calculation.
    self.old_progress = deque()
    self.last_progress_time = 0

    self.spinner_char_list = ['/', '-', '\\', '|']
    self.current_spinner_index = 0

    self.objects_finished = 0
    self.num_objects = 0  # Number of objects being processed

    # This overrides time constraints for updating and displaying
    # important information, such as having finished to process an object.
    self.object_report_change = False
    self.final_message = False

  def GetSpinner(self):
    """Returns the current spinner character.

    Returns:
      char_to_print: Char to be printed as the spinner
    """
    return self.spinner_char_list[self.current_spinner_index]

  def UpdateSpinner(self):
    """Updates the current spinner character."""
    self.current_spinner_index = ((self.current_spinner_index + 1) %
                                  len(self.spinner_char_list))

  def _HandleProducerThreadMessage(self, status_message):
    """Handles a ProducerThreadMessage.

    Args:
      status_message: The ProducerThreadMessage to be processed.
    """
    if status_message.finished:
      # This means this was a final ProducerThreadMessage.
      if self.num_objects_source >= EstimationSource.PRODUCER_THREAD_FINAL:
        self.num_objects_source = EstimationSource.PRODUCER_THREAD_FINAL
        self.num_objects = status_message.num_objects
      if (self.total_size_source >= EstimationSource.PRODUCER_THREAD_FINAL and
          status_message.size):
        self.total_size_source = EstimationSource.PRODUCER_THREAD_FINAL
        self.total_size = status_message.size
      return
    if self.num_objects_source >= EstimationSource.PRODUCER_THREAD_ESTIMATE:
      self.num_objects_source = EstimationSource.PRODUCER_THREAD_ESTIMATE
      self.num_objects = status_message.num_objects
    if (self.total_size_source >= EstimationSource.PRODUCER_THREAD_ESTIMATE and
        status_message.size):
      self.total_size_source = EstimationSource.PRODUCER_THREAD_ESTIMATE
      self.total_size = status_message.size

  def _HandleSeekAheadMessage(self, status_message, stream):
    """Handles a SeekAheadMessage.

    Args:
      status_message: The SeekAheadMessage to be processed.
      stream: Stream to print messages.
    """
    estimate_message = ('Estimated work for this command: objects: %s' %
                        status_message.num_objects)
    if status_message.size:
      estimate_message += (', total size: %s' %
                           MakeHumanReadable(status_message.size))
      if self.total_size_source >= EstimationSource.SEEK_AHEAD_THREAD:
        self.total_size_source = EstimationSource.SEEK_AHEAD_THREAD
        self.total_size = status_message.size

    if self.num_objects_source >= EstimationSource.SEEK_AHEAD_THREAD:
      self.num_objects_source = EstimationSource.SEEK_AHEAD_THREAD
      self.num_objects = status_message.num_objects

    estimate_message += '\n'
    if not self.quiet_mode:
      stream.write(estimate_message)

  def _HandlePerformanceSummaryMessage(self, status_message):
    """Handles a PerformanceSummaryMessage.

    Args:
      status_message: The PerformanceSummaryMessage to be processed.
    """
    LogPerformanceSummaryParams(uses_slice=status_message.uses_slice)

  def ShouldTrackThroughput(self, cur_time):
    """Decides whether enough time has passed to start tracking throughput.

    Args:
      cur_time: current time.
    Returns:
      Whether or not we should track the throughput.
    """
    return cur_time - self.start_time >= self.first_throughput_latency

  def ShouldPrintProgress(self, cur_time):
    """Decides whether or not it is time for printing a new progress.

    Args:
      cur_time: current time.
    Returns:
      Whether or not we should print the progress.
    """
    sufficient_time_elapsed = (cur_time - self.refresh_message_time >=
                               self.update_message_period)
    # Don't report if we aren't actually going to do anything (for example,
    # an rsync that will sync 0 objects).
    nonzero_report = self.num_objects
    return (sufficient_time_elapsed or
            self.object_report_change) and (nonzero_report)

  def ShouldPrintSpinner(self, cur_time):
    """Decides whether or not it is time for updating the spinner character.

    Args:
      cur_time: Current time.
    Returns:
      Whether or not we should update and print the spinner.
    """
    return (cur_time - self.refresh_spinner_time > self.update_spinner_period
            and self.total_size)

  def PrintSpinner(self, stream=sys.stderr):
    """Prints a spinner character.

    Args:
      stream: Stream to print messages. Usually sys.stderr, but customizable
              for testing.
    """
    self.UpdateSpinner()
    if not self.quiet_mode:
      stream.write(self.GetSpinner() + '\r')

  def UpdateThroughput(self, cur_time, cur_progress):
    """Updates throughput if the required period for calculation has passed.

    The throughput is calculated by taking all the progress (objects or bytes)
    processed within the last sliding_throughput_period seconds, and dividing
    that by the time period between the oldest progress time within that range
    and the last progress measurement, which are defined by oldest_progress[1]
    and last_progress_time, respectively. Among the pros of this approach,
    a connection break or a sudden change in throughput is quickly noticeable.
    Furthermore, using the last throughput measurement rather than the current
    time allows us to have a better estimation of the actual throughput.

    Args:
      cur_time: Current time to check whether or not it is time for a new
                throughput measurement.
      cur_progress: The current progress, in number of objects finished or in
                    bytes.
    """
    while (
        len(self.old_progress) > 1 and
        cur_time - self.old_progress[0].time > self.sliding_throughput_period):
      self.old_progress.popleft()

    if not self.old_progress:
      return
    oldest_progress = self.old_progress[0]
    if self.last_progress_time == oldest_progress.time:
      self.throughput = 0
      return
    # If old-progress is not empty and the time of oldest_progress does not
    # match the last_progress_time, we can safely calculate the throughput.
    self.throughput = ((cur_progress - oldest_progress.progress) /
                       (self.last_progress_time - oldest_progress.time))
    # Just to avoid -0.00 B/s.
    self.throughput = max(0, self.throughput)

  def PrintFinalSummaryMessage(self, stream=sys.stderr):
    """Prints a final message to indicate operation succeeded.

    Args:
      stream: Stream to print messages. Usually sys.stderr, but customizable
              for testing.
    """
    string_to_print = ('Operation completed over %s objects' %
                       DecimalShort(self.num_objects))
    if self.total_size:
      string_to_print += ('/%s' %
                          HumanReadableWithDecimalPlaces(self.total_size))
    remaining_width = self.console_width - len(string_to_print)
    if not self.quiet_mode:
      stream.write(('\n' + string_to_print + '.' +
                    (max(remaining_width, 0) * ' ') + '\n'))


class MetadataManager(StatusMessageManager):
  """Manages shared state for metadata operations.

  This manager is specific for metadata operations. Among its main functions,
  it receives incoming StatusMessages, storing all necessary data
  about the current and past states of the system necessary to display to the
  UI. It also provides methods for calculating metrics such as throughput and
  estimated time remaining. Finally, it provides methods for displaying messages
  to the UI.
  """

  def __init__(self,
               update_message_period=1,
               update_spinner_period=0.6,
               sliding_throughput_period=5,
               first_throughput_latency=10,
               quiet_mode=False,
               custom_time=None,
               verbose=False,
               console_width=80):
    # pylint: disable=g-doc-args
    """Instantiates a MetadataManager.

    See argument documentation in StatusMessageManager base class.
    """
    # pylint: enable=g-doc-args
    super(MetadataManager,
          self).__init__(update_message_period=update_message_period,
                         update_spinner_period=update_spinner_period,
                         sliding_throughput_period=sliding_throughput_period,
                         first_throughput_latency=first_throughput_latency,
                         quiet_mode=quiet_mode,
                         custom_time=custom_time,
                         verbose=verbose,
                         console_width=console_width)

  def GetProgress(self):
    """Gets the progress for a MetadataManager.

    Returns:
      The number of finished objects.
    """
    return self.objects_finished

  def _HandleMetadataMessage(self, status_message):
    """Handles a MetadataMessage.

    Args:
      status_message: The MetadataMessage to be processed.
    """
    self.objects_finished += 1
    if self.num_objects_source >= EstimationSource.INDIVIDUAL_MESSAGES:
      self.num_objects_source = EstimationSource.INDIVIDUAL_MESSAGES
      self.num_objects += 1
    # Ensures we print periodic progress, and that we send a final message.
    self.object_report_change = True
    self.last_progress_time = status_message.time
    if (self.objects_finished == self.num_objects and
        self.num_objects_source == EstimationSource.PRODUCER_THREAD_FINAL):
      self.final_message = True

  def ProcessMessage(self, status_message, stream):
    """Processes a message from _MainThreadUIQueue or _UIThread.

    Args:
      status_message: The StatusMessage item to be processed.
      stream: Stream to print messages.
    """
    self.object_report_change = False
    if isinstance(status_message, SeekAheadMessage):
      self._HandleSeekAheadMessage(status_message, stream)
    elif isinstance(status_message, ProducerThreadMessage):
      self._HandleProducerThreadMessage(status_message)
    elif isinstance(status_message, MetadataMessage):
      self._HandleMetadataMessage(status_message)
    elif isinstance(status_message, RetryableErrorMessage):
      LogRetryableError(status_message)
    elif isinstance(status_message, PerformanceSummaryMessage):
      self._HandlePerformanceSummaryMessage(status_message)
    self.old_progress.append(
        self._ThroughputInformation(self.objects_finished, status_message.time))

  def PrintProgress(self, stream=sys.stderr):
    """Prints progress and throughput/time estimation.

    Prints total number of objects and number of finished objects with the
    percentage of work done, potentially including the throughput
    (in objects/second) and estimated time remaining.

    Args:
      stream: Stream to print messages. Usually sys.stderr, but customizable
              for testing.
    """
    # Time to update all information
    total_remaining = self.num_objects - self.objects_finished
    if self.throughput:
      time_remaining = total_remaining / self.throughput
    else:
      time_remaining = None

    char_to_print = self.GetSpinner()
    if self.num_objects_source <= EstimationSource.SEEK_AHEAD_THREAD:
      # An example of objects_completed here would be ' [2/3 objects]'.
      objects_completed = ('[' + DecimalShort(self.objects_finished) + '/' +
                           DecimalShort(self.num_objects) + ' objects]')
      if self.num_objects == self.objects_finished:
        percentage = '100'
      else:
        percentage = (
            '%3d' %
            min(99, int(100 * float(self.objects_finished) / self.num_objects)))
      percentage_completed = percentage + '% Done'
    else:
      # An example of objects_completed here would be ' [2 objects]'.
      objects_completed = ('[' + DecimalShort(self.objects_finished) +
                           ' objects]')
      percentage_completed = ''

    if (self.refresh_message_time - self.start_time >
        self.first_throughput_latency):
      # Should also include throughput.
      # An example of throughput here would be '2 objects/s'
      throughput = '%.2f objects/s' % self.throughput
      if (self.num_objects_source <= EstimationSource.PRODUCER_THREAD_ESTIMATE
          and self.throughput):
        # Should also include time remaining.
        # An example of time remaining would be ' ETA 00:00:11'.
        time_remaining_str = 'ETA ' + PrettyTime(time_remaining)
      else:
        time_remaining_str = ''
    else:
      throughput = ''
      time_remaining_str = ''

    format_str = ('{char_to_print} {objects_completed} {percentage_completed}'
                  ' {throughput} {time_remaining_str}')
    string_to_print = format_str.format(
        char_to_print=char_to_print,
        objects_completed=objects_completed,
        percentage_completed=percentage_completed,
        throughput=throughput,
        time_remaining_str=time_remaining_str)
    remaining_width = self.console_width - len(string_to_print)
    if not self.quiet_mode:
      stream.write(string_to_print + (max(remaining_width, 0) * ' ') + '\r')

  def CanHandleMessage(self, status_message):
    """Determines whether this manager is suitable for handling status_message.

    Args:
      status_message: The StatusMessage object to be analyzed.
    Returns:
      True if this message can be properly handled by this manager,
      False otherwise.
    """
    if isinstance(
        status_message,
        (SeekAheadMessage, ProducerThreadMessage, MetadataMessage, FinalMessage,
         RetryableErrorMessage, PerformanceSummaryMessage)):
      return True
    return False


class DataManager(StatusMessageManager):
  """Manages shared state for data operations.

  This manager is specific for data operations. Among its main functions,
  it receives incoming StatusMessages, storing all necessary data
  about the current and past states of the system necessary to display to the
  UI. It also provides methods for calculating metrics such as throughput and
  estimated time remaining. Finally, it provides methods for displaying messages
  to the UI.
  """

  class _ProgressInformation(object):
    """Class that contains all progress information needed for a given file.

    This _ProgressInformation is used as the value associated with a file_name
    in the dict that stores the information about all processed files.
    """

    def __init__(self, size):
      """Constructor of _ProgressInformation.

      Args:
        size: The total size of the file.
      """
      # Sum of all progress obtained in this operation.
      self.new_progress_sum = 0
      # Sum of all progress from previous operations (mainly for resuming
      # uploads or resuming downloads).
      self.existing_progress_sum = 0
      # Dict for tracking the progress for each individual component. Key is
      # of the form (component_num, dst_url) and correspondent element is a
      # tuple which stores the current progress obtained from this operation,
      # and the progress obtained from previous operations.
      self.dict = {}
      # The total size for the file
      self.size = size

  def __init__(self,
               update_message_period=1,
               update_spinner_period=0.6,
               sliding_throughput_period=5,
               first_throughput_latency=10,
               quiet_mode=False,
               custom_time=None,
               verbose=False,
               console_width=None):
    # pylint: disable=g-doc-args
    """Instantiates a DataManager.

    See argument documentation in StatusMessageManager base class.
    """
    # pylint: disable=g-doc-args
    super(DataManager,
          self).__init__(update_message_period=update_message_period,
                         update_spinner_period=update_spinner_period,
                         sliding_throughput_period=sliding_throughput_period,
                         first_throughput_latency=first_throughput_latency,
                         quiet_mode=quiet_mode,
                         custom_time=custom_time,
                         verbose=verbose,
                         console_width=console_width)

    self.first_item = True

    self.total_progress = 0  # Sum of progress for all threads.
    self.new_progress = 0
    self.existing_progress = 0

    # Dict containing individual progress for each file. Key is filename
    # (from src_url). It maps to a _ProgressInformation object.
    self.individual_file_progress = {}

    self.component_total = 0
    self.finished_components = 0
    self.existing_components = 0

  def GetProgress(self):
    """Gets the progress for a DataManager.

    Returns:
      The number of processed bytes in this operation.
    """
    return self.new_progress

  def _HandleFileDescription(self, status_message):
    """Handles a FileMessage that describes a file.

    Args:
      status_message: the FileMessage to be processed.
    """
    if not status_message.finished:
      # File started.
      if self.first_item and not self.custom_time:
        # Set initial time.
        self.refresh_message_time = status_message.time
        self.start_time = self.refresh_message_time
        self.last_throughput_time = self.refresh_message_time
        self.first_item = False

      # Gets file name (from src_url).
      file_name = status_message.src_url.url_string
      status_message.size = status_message.size if status_message.size else 0
      # Creates a new entry on individual_file_progress.
      self.individual_file_progress[file_name] = self._ProgressInformation(
          status_message.size)

      if self.num_objects_source >= EstimationSource.INDIVIDUAL_MESSAGES:
        # This ensures the file has not been counted on SeekAheadThread or
        # in ProducerThread.
        self.num_objects_source = EstimationSource.INDIVIDUAL_MESSAGES
        self.num_objects += 1
      if self.total_size_source >= EstimationSource.INDIVIDUAL_MESSAGES:
        # This ensures the file size has not been counted on SeekAheadThread or
        # in ProducerThread.
        self.total_size_source = EstimationSource.INDIVIDUAL_MESSAGES
        self.total_size += status_message.size

      self.object_report_change = True

    else:
      # File finished.
      self.objects_finished += 1
      file_name = status_message.src_url.url_string
      file_progress = self.individual_file_progress[file_name]
      total_bytes_transferred = (file_progress.new_progress_sum +
                                 file_progress.existing_progress_sum)
      # Ensures total_progress has the right value.
      self.total_progress += file_progress.size - total_bytes_transferred
      self.new_progress += file_progress.size - total_bytes_transferred
      self.last_progress_time = status_message.time
      # Deleting _ProgressInformation object to save memory.
      del self.individual_file_progress[file_name]
      self.object_report_change = True
      if (self.objects_finished == self.num_objects and
          self.num_objects_source == EstimationSource.PRODUCER_THREAD_FINAL):
        self.final_message = True

  def _IsFile(self, file_message):
    """Tells whether or not this FileMessage represent a file.

    This is needed because FileMessage is used by both files and components.

    Args:
      file_message: The FileMessage to be analyzed.
    Returns:
      Whether or not this represents a file.
    """
    message_type = file_message.message_type
    return (message_type == FileMessage.FILE_DOWNLOAD or
            message_type == FileMessage.FILE_UPLOAD or
            message_type == FileMessage.FILE_CLOUD_COPY or
            message_type == FileMessage.FILE_DAISY_COPY or
            message_type == FileMessage.FILE_LOCAL_COPY or
            message_type == FileMessage.FILE_REWRITE or
            message_type == FileMessage.FILE_HASH)

  def _HandleComponentDescription(self, status_message):
    """Handles a FileMessage that describes a component.

    Args:
      status_message: The FileMessage to be processed.
    """
    if (status_message.message_type == FileMessage.EXISTING_COMPONENT and
        not status_message.finished):
      # Existing component: have to ensure total_progress accounts for it.
      self.existing_components += 1

      file_name = status_message.src_url.url_string
      file_progress = self.individual_file_progress[file_name]

      key = (status_message.component_num, status_message.dst_url)
      file_progress.dict[key] = (0, status_message.size)
      file_progress.existing_progress_sum += status_message.size

      self.total_progress += status_message.size
      self.existing_progress += status_message.size

    elif ((status_message.message_type == FileMessage.COMPONENT_TO_UPLOAD or
           status_message.message_type == FileMessage.COMPONENT_TO_DOWNLOAD)):
      if not status_message.finished:
        # Component started.
        self.component_total += 1
        if status_message.message_type == FileMessage.COMPONENT_TO_DOWNLOAD:
          file_name = status_message.src_url.url_string
          file_progress = self.individual_file_progress[file_name]

          file_progress.existing_progress_sum += (
              status_message.bytes_already_downloaded)

          key = (status_message.component_num, status_message.dst_url)
          file_progress.dict[key] = (0, status_message.bytes_already_downloaded)

          self.total_progress += status_message.bytes_already_downloaded
          self.existing_progress += status_message.bytes_already_downloaded

      else:
        # Component finished.
        self.finished_components += 1
        file_name = status_message.src_url.url_string
        file_progress = self.individual_file_progress[file_name]

        key = (status_message.component_num, status_message.dst_url)
        last_update = (file_progress.dict[key] if key in file_progress.dict else
                       (0, 0))
        self.total_progress += status_message.size - sum(last_update)
        self.new_progress += status_message.size - sum(last_update)
        self.last_progress_time = status_message.time
        file_progress.new_progress_sum += (status_message.size -
                                           sum(last_update))
        file_progress.dict[key] = (status_message.size - last_update[1],
                                   last_update[1])

  def _HandleProgressMessage(self, status_message):
    """Handles a ProgressMessage that tracks progress of a file or component.

    Args:
      status_message: The ProgressMessage to be processed.
    """
    # Retrieving index and dict for this file.
    file_name = status_message.src_url.url_string
    file_progress = self.individual_file_progress[file_name]

    # Retrieves last update ((0,0) if no previous update) for this file or
    # component. To ensure uniqueness (among components),
    # we use a (component_num, dst_url) tuple as our key.
    key = (status_message.component_num, status_message.dst_url)
    last_update = (file_progress.dict[key] if key in file_progress.dict else
                   (0, 0))
    status_message.processed_bytes -= last_update[1]
    file_progress.new_progress_sum += (status_message.processed_bytes -
                                       last_update[0])
    # Updates total progress with new update from component.
    self.total_progress += status_message.processed_bytes - last_update[0]
    self.new_progress += status_message.processed_bytes - last_update[0]
    # Updates file_progress.dict on component's key.
    file_progress.dict[key] = (status_message.processed_bytes, last_update[1])
    self.last_progress_time = status_message.time

  def ProcessMessage(self, status_message, stream):
    """Processes a message from _MainThreadUIQueue or _UIThread.

    Args:
      status_message: The StatusMessage item to be processed.
      stream: Stream to print messages. Here only for SeekAheadThread
    """
    self.object_report_change = False
    if isinstance(status_message, ProducerThreadMessage):
      # ProducerThread info.
      self._HandleProducerThreadMessage(status_message)

    elif isinstance(status_message, SeekAheadMessage):
      # SeekAheadThread info.
      self._HandleSeekAheadMessage(status_message, stream)

    elif isinstance(status_message, FileMessage):
      if self._IsFile(status_message):
        # File info.
        self._HandleFileDescription(status_message)
      else:
        # Component info.
        self._HandleComponentDescription(status_message)
      LogPerformanceSummaryParams(file_message=status_message)

    elif isinstance(status_message, ProgressMessage):
      # Progress info.
      self._HandleProgressMessage(status_message)

    elif isinstance(status_message, RetryableErrorMessage):
      LogRetryableError(status_message)

    elif isinstance(status_message, PerformanceSummaryMessage):
      self._HandlePerformanceSummaryMessage(status_message)

    self.old_progress.append(
        self._ThroughputInformation(self.new_progress, status_message.time))

  def PrintProgress(self, stream=sys.stderr):
    """Prints progress and throughput/time estimation.

    If a ProducerThreadMessage or SeekAheadMessage has been provided,
    it outputs the number of files completed, number of total files,
    the current progress, the total size, and the percentage it
    represents.
    If none of those have been provided, it only includes the number of files
    completed, the current progress and total size (which might be updated),
    with no percentage as we do not know if more files are coming.
    It may also include time estimation (available only given
    ProducerThreadMessage or SeekAheadMessage provided) and throughput. For that
    to happen, there is an extra condition of at least first_throughput_latency
    seconds having been passed since the UIController started, and that
    either the ProducerThread or the SeekAheadThread have estimated total
    number of files and total size.

    Args:
      stream: Stream to print messages. Usually sys.stderr, but customizable
              for testing.
    """
    # Time to update all information.
    total_remaining = self.total_size - self.total_progress

    if self.throughput:
      time_remaining = total_remaining / self.throughput
    else:
      time_remaining = None

    char_to_print = self.GetSpinner()

    if self.num_objects_source <= EstimationSource.SEEK_AHEAD_THREAD:
      # An example of objects_completed here would be ' [2/3 files]'.
      objects_completed = ('[' + DecimalShort(self.objects_finished) + '/' +
                           DecimalShort(self.num_objects) + ' files]')
    else:
      # An example of objects_completed here would be ' [2 files]'.
      objects_completed = '[' + DecimalShort(self.objects_finished) + ' files]'

    # An example of bytes_progress would be '[101.0 MiB/1.0 GiB]'.
    bytes_progress = ('[%s/%s]' % (BytesToFixedWidthString(
        self.total_progress), BytesToFixedWidthString(self.total_size)))

    if self.total_size_source <= EstimationSource.SEEK_AHEAD_THREAD:
      if self.num_objects == self.objects_finished:
        percentage = '100'
      else:
        percentage = (
            '%3d' %
            min(99, int(100 * float(self.total_progress) / self.total_size)))
      percentage_completed = percentage + '% Done'
    else:
      percentage_completed = ''

    if (self.refresh_message_time - self.start_time >
        self.first_throughput_latency):
      # Should also include throughput.
      # An example of throughput here would be ' 82.3 MiB/s'
      throughput = BytesToFixedWidthString(self.throughput) + '/s'

      if (self.total_size_source <= EstimationSource.PRODUCER_THREAD_ESTIMATE
          and self.throughput):
        # Should also include time remaining.
        # An example of time remaining would be ' ETA 00:00:11'.
        time_remaining_str = 'ETA ' + PrettyTime(time_remaining)
      else:
        time_remaining_str = ''
    else:
      throughput = ''
      time_remaining_str = ''

    format_str = ('{char_to_print} {objects_completed}{bytes_progress}'
                  ' {percentage_completed} {throughput} {time_remaining_str}')
    string_to_print = format_str.format(
        char_to_print=char_to_print,
        objects_completed=objects_completed,
        bytes_progress=bytes_progress,
        percentage_completed=percentage_completed,
        throughput=throughput,
        time_remaining_str=time_remaining_str)
    remaining_width = self.console_width - len(string_to_print)
    if not self.quiet_mode:
      stream.write(string_to_print + (max(remaining_width, 0) * ' ') + '\r')

  def CanHandleMessage(self, status_message):
    """Determines whether this manager is suitable for handling status_message.

    Args:
      status_message: The StatusMessage object to be analyzed.
    Returns:
      True if this message can be properly handled by this manager,
      False otherwise.
    """
    if isinstance(status_message, (
        SeekAheadMessage,
        ProducerThreadMessage,
        FileMessage,
        ProgressMessage,
        FinalMessage,
        RetryableErrorMessage,
        PerformanceSummaryMessage,
    )):
      return True
    return False


class UIController(object):
  """Controller UI class to integrate _MainThreadUIQueue and _UIThread.

  This class receives messages from _MainThreadUIQueue and _UIThread and send
  them to an appropriate manager, which will then processes and store data about
  them.
  """

  def __init__(self,
               update_message_period=1,
               update_spinner_period=0.6,
               sliding_throughput_period=5,
               first_throughput_latency=10,
               quiet_mode=False,
               custom_time=None,
               verbose=False,
               dump_status_messages_file=None):
    """Instantiates a UIController.

    Args:
      update_message_period: Minimum period for refreshing and  displaying
          new information. A non-positive value will ignore any time
          restrictions imposed by this field.
      update_spinner_period: Minimum period for refreshing and displaying the
          spinner. A non-positive value will ignore any time restrictions
          imposed by this field.
      sliding_throughput_period: Sliding period for throughput calculation. A
          non-positive value will make it impossible to calculate the
          throughput.
      first_throughput_latency: Minimum waiting time before actually displaying
          throughput info. A non-positive value will ignore any time
          restrictions imposed by this field.
      quiet_mode: If True, do not print status messages (but still process
          them for analytics reporting as necessary).
      custom_time: If a custom start_time is desired. Used for testing.
      verbose: Tells whether or not the operation is on verbose mode.
      dump_status_messages_file: File path for logging all received status
          messages, for debugging purposes.
    """
    self.verbose = verbose
    self.update_message_period = update_message_period
    self.update_spinner_period = update_spinner_period
    self.sliding_throughput_period = sliding_throughput_period
    self.first_throughput_latency = first_throughput_latency
    self.manager = None
    self.quiet_mode = quiet_mode
    self.custom_time = custom_time
    self.console_width = 80  # Console width. Passed to manager.
    # List storing all estimation messages from SeekAheadThread or
    # ProducerThread. This is used when we still do not know which manager to
    # use.
    self.early_estimation_messages = []
    self.printed_final_message = False
    self.dump_status_message_fp = None
    if dump_status_messages_file:
      self.dump_status_message_fp = open(dump_status_messages_file, 'ab')

  def _HandleMessage(self, status_message, stream, cur_time=None):
    """Processes a message, updates throughput and prints progress.

    Args:
      status_message: Message to be processed. Could be None if UIThread cannot
                      retrieve message from status_queue.
      stream: stream to print messages. Usually sys.stderr, but customizable
              for testing.
      cur_time: Message time. Used to determine if it is time to refresh
                output, or calculate throughput.
    """
    self.manager.ProcessMessage(status_message, stream)
    if self.manager.ShouldPrintProgress(cur_time):
      if self.manager.ShouldTrackThroughput(cur_time):
        self.manager.UpdateThroughput(cur_time, self.manager.GetProgress())
      self.manager.PrintProgress(stream)
      self.manager.refresh_message_time = cur_time
    if self.manager.ShouldPrintSpinner(cur_time):
      self.manager.PrintSpinner(stream)
      self.manager.refresh_spinner_time = cur_time
    if ((isinstance(status_message, FinalMessage) or self.manager.final_message)
        and self.manager.num_objects and not self.printed_final_message):
      self.printed_final_message = True
      LogPerformanceSummaryParams(
          num_objects_transferred=self.manager.num_objects)
      self.manager.PrintFinalSummaryMessage(stream)

  def Call(self, status_message, stream, cur_time=None):
    """Coordinates UI manager and calls appropriate function to handle message.

    Args:
      status_message: Message to be processed. Could be None if UIThread cannot
                      retrieve message from status_queue.
      stream: Stream to print messages. Usually sys.stderr, but customizable
              for testing.
      cur_time: Message time. Used to determine if it is time to refresh
                output, or calculate throughput.
    """
    if not isinstance(status_message, StatusMessage):
      if status_message == _ZERO_TASKS_TO_DO_ARGUMENT and not self.manager:
        # Create a manager to handle early estimation messages before returning.
        self.manager = (DataManager(
            update_message_period=self.update_message_period,
            update_spinner_period=self.update_spinner_period,
            sliding_throughput_period=self.sliding_throughput_period,
            first_throughput_latency=self.first_throughput_latency,
            quiet_mode=self.quiet_mode,
            custom_time=self.custom_time,
            verbose=self.verbose,
            console_width=self.console_width))
        for estimation_message in self.early_estimation_messages:
          self._HandleMessage(estimation_message,
                              stream,
                              cur_time=estimation_message.time)
      return
    if self.dump_status_message_fp:
      # TODO: Add Unicode support to string methods on message classes.
      # Currently, dump will fail with a UnicodeEncodeErorr if the message
      # class contains a Unicode attribute.
      self.dump_status_message_fp.write(str(status_message))
      self.dump_status_message_fp.write('\n')
    if not cur_time:
      cur_time = status_message.time
    if not self.manager:
      if (isinstance(status_message, SeekAheadMessage) or
          isinstance(status_message, ProducerThreadMessage)):
        self.early_estimation_messages.append(status_message)
        return
      elif isinstance(status_message, MetadataMessage):
        self.manager = (MetadataManager(
            update_message_period=self.update_message_period,
            update_spinner_period=self.update_spinner_period,
            sliding_throughput_period=self.sliding_throughput_period,
            first_throughput_latency=self.first_throughput_latency,
            quiet_mode=self.quiet_mode,
            custom_time=self.custom_time,
            verbose=self.verbose,
            console_width=self.console_width))
        for estimation_message in self.early_estimation_messages:
          self._HandleMessage(estimation_message, stream, cur_time)
      else:
        self.manager = (DataManager(
            update_message_period=self.update_message_period,
            update_spinner_period=self.update_spinner_period,
            sliding_throughput_period=self.sliding_throughput_period,
            first_throughput_latency=self.first_throughput_latency,
            quiet_mode=self.quiet_mode,
            custom_time=self.custom_time,
            verbose=self.verbose,
            console_width=self.console_width))

        for estimation_message in self.early_estimation_messages:
          self._HandleMessage(estimation_message, stream, cur_time)
    if not self.manager.CanHandleMessage(status_message):
      if (isinstance(status_message, FileMessage) or
          isinstance(status_message, ProgressMessage)):
        # We have to create a DataManager to handle this data message. This is
        # to avoid a possible race condition where MetadataMessages are sent
        # before data messages. As such, this means that the DataManager has
        # priority, and whenever a data message is received, we ignore the
        # MetadataManager if one exists, and start a DataManager from scratch.
        # This can be done because we do not need any MetadataMessages to
        # properly handle a data operation. It could be useful to send the
        # early estimation messages, if those are available.
        self.manager = (DataManager(
            update_message_period=self.update_message_period,
            update_spinner_period=self.update_spinner_period,
            sliding_throughput_period=self.sliding_throughput_period,
            first_throughput_latency=self.first_throughput_latency,
            custom_time=self.custom_time,
            verbose=self.verbose,
            console_width=self.console_width))
        for estimation_message in self.early_estimation_messages:
          self._HandleMessage(estimation_message, stream, cur_time)
      else:
        # No need to handle this message.
        return
    self._HandleMessage(status_message, stream, cur_time)


class MainThreadUIQueue(object):
  """Handles status display and processing in the main thread / master process.

  This class emulates a queue to cover main-thread activity before or after
  Apply, as well as for the single-threaded, single-process case, i.e.,
  _SequentialApply. When multiple threads or processes are used during calls
  to Apply, the main thread is waiting for work to complete, and this queue
  must remain unused until Apply returns. Code producing arguments for
  Apply (such as the NameExpansionIterator) must not post messages to this
  queue to avoid race conditions with the UIThread.

  This class sends the messages it receives to UIController, which
  decides the correct course of action.
  """

  def __init__(self, stream, ui_controller):
    """Instantiates a _MainThreadUIQueue.

    Args:
      stream: Stream for printing messages.
      ui_controller: UIController to manage messages.
    """

    super(MainThreadUIQueue, self).__init__()
    self.ui_controller = ui_controller
    self.stream = stream

  # pylint: disable=invalid-name, unused-argument
  def put(self, status_message, timeout=None):
    self.ui_controller.Call(status_message, self.stream)

  # pylint: enable=invalid-name, unused-argument


class UIThread(threading.Thread):
  """Responsible for centralized printing across multiple processes/threads.

  This class pulls status messages that are posted to the centralized status
  queue and coordinates displaying status and progress to the user. It is
  used only during calls to _ParallelApply, which in turn is called only when
  multiple threads and/or processes are used.

  This class sends the messages it receives to UIController, which
  decides the correct course of action.
  """

  def __init__(self, status_queue, stream, ui_controller, timeout=1):
    """Instantiates a _UIThread.

    Args:
      status_queue: Queue for reporting status updates.
      stream: Stream for printing messages.
      ui_controller: UI controller to manage messages.
      timeout: Timeout for getting a message.
    """

    super(UIThread, self).__init__()
    self.status_queue = status_queue
    self.stream = stream
    self.timeout = timeout
    self.ui_controller = ui_controller
    self.start()

  def run(self):
    try:
      while True:
        try:
          status_message = self.status_queue.get(timeout=self.timeout)
        except Queue.Empty:
          status_message = None
          continue
        self.ui_controller.Call(status_message, self.stream)
        if status_message == _ZERO_TASKS_TO_DO_ARGUMENT:
          # Item from MainThread to indicate we are done.
          break
    except Exception as e:  # pylint:disable=broad-except
      self.stream.write('Exception in UIThread: %s\n' % e)
