#!/usr/bin/python
#
# Copyright 2010 Google Inc. All Rights Reserved.

import datetime
import optparse
import os
import smtplib
import sys
import time
from email.mime.text import MIMEText

from autotest_gatherer import AutotestGatherer as AutotestGatherer
from autotest_run import AutotestRun as AutotestRun
from machine_manager_singleton import MachineManagerSingleton as MachineManagerSingleton
from cros_utils import logger
from cros_utils.file_utils import FileUtils


def CanonicalizeChromeOSRoot(chromeos_root):
  chromeos_root = os.path.expanduser(chromeos_root)
  if os.path.isfile(os.path.join(chromeos_root, 'src/scripts/enter_chroot.sh')):
    return chromeos_root
  else:
    return None


class Autotest(object):

  def __init__(self, autotest_string):
    self.name = None
    self.iterations = None
    self.args = None
    fields = autotest_string.split(',', 1)
    self.name = fields[0]
    if len(fields) > 1:
      autotest_string = fields[1]
      fields = autotest_string.split(',', 1)
    else:
      return
    self.iterations = int(fields[0])
    if len(fields) > 1:
      self.args = fields[1]
    else:
      return

  def __str__(self):
    return '\n'.join([self.name, self.iterations, self.args])


def CreateAutotestListFromString(autotest_strings, default_iterations=None):
  autotest_list = []
  for autotest_string in autotest_strings.split(':'):
    autotest = Autotest(autotest_string)
    if default_iterations and not autotest.iterations:
      autotest.iterations = default_iterations

    autotest_list.append(autotest)
  return autotest_list


def CreateAutotestRuns(images,
                       autotests,
                       remote,
                       board,
                       exact_remote,
                       rerun,
                       rerun_if_failed,
                       main_chromeos_root=None):
  autotest_runs = []
  for image in images:
    logger.GetLogger().LogOutput('Computing md5sum of: %s' % image)
    image_checksum = FileUtils().Md5File(image)
    logger.GetLogger().LogOutput('md5sum %s: %s' % (image, image_checksum))
    ###    image_checksum = "abcdefghi"

    chromeos_root = main_chromeos_root
    if not main_chromeos_root:
      image_chromeos_root = os.path.join(
          os.path.dirname(image), '../../../../..')
      chromeos_root = CanonicalizeChromeOSRoot(image_chromeos_root)
      assert chromeos_root, 'chromeos_root: %s invalid' % image_chromeos_root
    else:
      chromeos_root = CanonicalizeChromeOSRoot(main_chromeos_root)
      assert chromeos_root, 'chromeos_root: %s invalid' % main_chromeos_root

    # We just need a single ChromeOS root in the MachineManagerSingleton. It is
    # needed because we can save re-image time by checking the image checksum at
    # the beginning and assigning autotests to machines appropriately.
    if not MachineManagerSingleton().chromeos_root:
      MachineManagerSingleton().chromeos_root = chromeos_root

    for autotest in autotests:
      for iteration in range(autotest.iterations):
        autotest_run = AutotestRun(autotest,
                                   chromeos_root=chromeos_root,
                                   chromeos_image=image,
                                   board=board,
                                   remote=remote,
                                   iteration=iteration,
                                   image_checksum=image_checksum,
                                   exact_remote=exact_remote,
                                   rerun=rerun,
                                   rerun_if_failed=rerun_if_failed)
        autotest_runs.append(autotest_run)
  return autotest_runs


def GetNamesAndIterations(autotest_runs):
  strings = []
  for autotest_run in autotest_runs:
    strings.append('%s:%s' % (autotest_run.autotest.name,
                              autotest_run.iteration))
  return ' %s (%s)' % (len(strings), ' '.join(strings))


def GetStatusString(autotest_runs):
  status_bins = {}
  for autotest_run in autotest_runs:
    if autotest_run.status not in status_bins:
      status_bins[autotest_run.status] = []
    status_bins[autotest_run.status].append(autotest_run)

  status_strings = []
  for key, val in status_bins.items():
    status_strings.append('%s: %s' % (key, GetNamesAndIterations(val)))
  return 'Thread Status:\n%s' % '\n'.join(status_strings)


def GetProgressBar(num_done, num_total):
  ret = 'Done: %s%%' % int(100.0 * num_done / num_total)
  bar_length = 50
  done_char = '>'
  undone_char = ' '
  num_done_chars = bar_length * num_done / num_total
  num_undone_chars = bar_length - num_done_chars
  ret += ' [%s%s]' % (num_done_chars * done_char,
                      num_undone_chars * undone_char)
  return ret


def GetProgressString(start_time, num_remain, num_total):
  current_time = time.time()
  elapsed_time = current_time - start_time
  try:
    eta_seconds = float(num_remain) * elapsed_time / (num_total - num_remain)
    eta_seconds = int(eta_seconds)
    eta = datetime.timedelta(seconds=eta_seconds)
  except ZeroDivisionError:
    eta = 'Unknown'
  strings = []
  strings.append('Current time: %s Elapsed: %s ETA: %s' %
                 (datetime.datetime.now(),
                  datetime.timedelta(seconds=int(elapsed_time)), eta))
  strings.append(GetProgressBar(num_total - num_remain, num_total))
  return '\n'.join(strings)


def RunAutotestRunsInParallel(autotest_runs):
  start_time = time.time()
  active_threads = []
  for autotest_run in autotest_runs:
    # Set threads to daemon so program exits when ctrl-c is pressed.
    autotest_run.daemon = True
    autotest_run.start()
    active_threads.append(autotest_run)

  print_interval = 30
  last_printed_time = time.time()
  while active_threads:
    try:
      active_threads = [t for t in active_threads
                        if t is not None and t.isAlive()]
      for t in active_threads:
        t.join(1)
      if time.time() - last_printed_time > print_interval:
        border = '=============================='
        logger.GetLogger().LogOutput(border)
        logger.GetLogger().LogOutput(GetProgressString(start_time, len(
            [t for t in autotest_runs if t.status not in ['SUCCEEDED', 'FAILED']
            ]), len(autotest_runs)))
        logger.GetLogger().LogOutput(GetStatusString(autotest_runs))
        logger.GetLogger().LogOutput('%s\n' %
                                     MachineManagerSingleton().AsString())
        logger.GetLogger().LogOutput(border)
        last_printed_time = time.time()
    except KeyboardInterrupt:
      print 'C-c received... cleaning up threads.'
      for t in active_threads:
        t.terminate = True
      return 1
  return 0


def RunAutotestRunsSerially(autotest_runs):
  for autotest_run in autotest_runs:
    retval = autotest_run.Run()
    if retval:
      return retval


def ProduceTables(autotest_runs, full_table, fit_string):
  l = logger.GetLogger()
  ags_dict = {}
  for autotest_run in autotest_runs:
    name = autotest_run.full_name
    if name not in ags_dict:
      ags_dict[name] = AutotestGatherer()
    ags_dict[name].runs.append(autotest_run)
    output = ''
  for b, ag in ags_dict.items():
    output += 'Benchmark: %s\n' % b
    output += ag.GetFormattedMainTable(percents_only=not full_table,
                                       fit_string=fit_string)
    output += '\n'

  summary = ''
  for b, ag in ags_dict.items():
    summary += 'Benchmark Summary Table: %s\n' % b
    summary += ag.GetFormattedSummaryTable(percents_only=not full_table,
                                           fit_string=fit_string)
    summary += '\n'

  output += summary
  output += ('Number of re-images performed: %s' %
             MachineManagerSingleton().num_reimages)
  l.LogOutput(output)

  if autotest_runs:
    board = autotest_runs[0].board
  else:
    board = ''

  subject = '%s: %s' % (board, ', '.join(ags_dict.keys()))

  if any(autotest_run.run_completed for autotest_run in autotest_runs):
    SendEmailToUser(subject, summary)


def SendEmailToUser(subject, text_to_send):
  # Email summary to the current user.
  msg = MIMEText(text_to_send)

  # me == the sender's email address
  # you == the recipient's email address
  me = os.path.basename(__file__)
  you = os.getlogin()
  msg['Subject'] = '[%s] %s' % (os.path.basename(__file__), subject)
  msg['From'] = me
  msg['To'] = you

  # Send the message via our own SMTP server, but don't include the
  # envelope header.
  s = smtplib.SMTP('localhost')
  s.sendmail(me, [you], msg.as_string())
  s.quit()


def Main(argv):
  """The main function."""
  # Common initializations
  ###  command_executer.InitCommandExecuter(True)
  l = logger.GetLogger()

  parser = optparse.OptionParser()
  parser.add_option('-t',
                    '--tests',
                    dest='tests',
                    help=('Tests to compare.'
                          'Optionally specify per-test iterations by:'
                          '<test>,<iter>:<args>'))
  parser.add_option('-c',
                    '--chromeos_root',
                    dest='chromeos_root',
                    help='A *single* chromeos_root where scripts can be found.')
  parser.add_option('-n',
                    '--iterations',
                    dest='iterations',
                    help='Iterations to run per benchmark.',
                    default=1)
  parser.add_option('-r',
                    '--remote',
                    dest='remote',
                    help='The remote chromeos machine.')
  parser.add_option('-b', '--board', dest='board', help='The remote board.')
  parser.add_option('--full_table',
                    dest='full_table',
                    help='Print full tables.',
                    action='store_true',
                    default=True)
  parser.add_option('--exact_remote',
                    dest='exact_remote',
                    help='Run tests on the exact remote.',
                    action='store_true',
                    default=False)
  parser.add_option('--fit_string',
                    dest='fit_string',
                    help='Fit strings to fixed sizes.',
                    action='store_true',
                    default=False)
  parser.add_option('--rerun',
                    dest='rerun',
                    help='Re-run regardless of cache hit.',
                    action='store_true',
                    default=False)
  parser.add_option('--rerun_if_failed',
                    dest='rerun_if_failed',
                    help='Re-run if previous run was a failure.',
                    action='store_true',
                    default=False)
  parser.add_option('--no_lock',
                    dest='no_lock',
                    help='Do not lock the machine before running the tests.',
                    action='store_true',
                    default=False)
  l.LogOutput(' '.join(argv))
  [options, args] = parser.parse_args(argv)

  if options.remote is None:
    l.LogError('No remote machine specified.')
    parser.print_help()
    return 1

  if not options.board:
    l.LogError('No board specified.')
    parser.print_help()
    return 1

  remote = options.remote
  tests = options.tests
  board = options.board
  exact_remote = options.exact_remote
  iterations = int(options.iterations)

  autotests = CreateAutotestListFromString(tests, iterations)

  main_chromeos_root = options.chromeos_root
  images = args[1:]
  fit_string = options.fit_string
  full_table = options.full_table
  rerun = options.rerun
  rerun_if_failed = options.rerun_if_failed

  MachineManagerSingleton().no_lock = options.no_lock

  # Now try creating all the Autotests
  autotest_runs = CreateAutotestRuns(images, autotests, remote, board,
                                     exact_remote, rerun, rerun_if_failed,
                                     main_chromeos_root)

  try:
    # At this point we have all the autotest runs.
    for machine in remote.split(','):
      MachineManagerSingleton().AddMachine(machine)

    retval = RunAutotestRunsInParallel(autotest_runs)
    if retval:
      return retval

    # Now print tables
    ProduceTables(autotest_runs, full_table, fit_string)
  finally:
    # not sure why this isn't called at the end normally...
    MachineManagerSingleton().__del__()

  return 0


if __name__ == '__main__':
  sys.exit(Main(sys.argv))