import os
import urllib2
import urlparse
import shutil
import sys
import zipfile

LATEST_REVISION_URL = 'http://build.chromium.org/buildbot/continuous/LATEST/REVISION'
LATEST_CHROMIUM_URL = 'http://build.chromium.org/buildbot/continuous/LATEST/chrome-win32.zip'
LATEST_SYMBOLS_URL = 'http://build.chromium.org/buildbot/continuous/LATEST/chrome-win32-syms.zip'

def Help():
  print """
Latest.py - Download and unpack the latest Chromium binaries and symbols from 
            the build server.

Syntax:
    latest.py path [Options]

Options:
  path
    Where to store the latest Chrome binaries and symbols.
    (This option is required.)
  --clean
    Clean the destination folder before downloading.
  --nosymbols
    Download the binaries only and not the symbols.

Example:
  latest.py "C:\Chromium-latest" --clean
    Download the Last Known Good binaries and symbols into C:\Chromium-latest
    after deleting the current contents of that folder.
"""

def GetLatestRevision():
  return int(urllib2.urlopen(LATEST_REVISION_URL).read())

def PipeIO(in_file, out_file, expected_size=None, status=""):
  read_bytes = 0
  while True:
    buffer = in_file.read(0x1000)
    if buffer:
      read_bytes += len(buffer)
      if expected_size:
        print '\r%s %d bytes (%d%%)' % (status, expected_size, 
            int(100 * read_bytes / expected_size)),
      else:
        print '\r%s %d bytes' % (status, read_bytes),
      out_file.write(buffer)
    else:
      break
  if expected_size:
    if read_bytes < expected_size:
      print 'failed: smaller than expected.'
      return False
    if read_bytes > expected_size:
      print 'failed: larger than expected.'
      return False
    print 'ok.'
  else:
    print 'ok (size cannot be confirmed).'
  in_file.close()
  out_file.close()
  return True

def Download(url, file_path, expected_content_type=None):
  print ' => Opening "%s"...' % (url,),
  socket = urllib2.urlopen(url)
  real_url = socket.geturl()
  if real_url != url:
    print 'redirected.'
    print ' => Opening "%s"...' % (real_url,),
  info = socket.info()
  content_type = info.getheader('content-type')
  if expected_content_type:
    if not content_type:
      print 'failed: content type not specified.'
      return False
    if content_type != expected_content_type:
      print 'failed: unexpected content type.'
      print '  (Expected: "%s", got: "%s")' % (expected_content_type, 
          content_type)
      return False
  content_size = info.getheader('content-length')
  if content_size is not None:
    content_size = int(content_size)
  print 'ok.'
  status = ' + "%s"...' % (file_path,)
  output_file = open(file_path, 'wb')
  if not PipeIO(socket, output_file, content_size, status):
    print 'Download failed.'
    return False
  return True

def CreateFolder(path):
  print ' + "%s"...' % (path,),
  try:
    os.makedirs(path)
  except:
    print 'failed.'
    return False
  print 'ok.'
  return True

def Unzip(zip_file_path, folder, remove_sub_folder):
  if not zipfile.is_zipfile(zip_file_path):
    print ' - File is not a valid zip file.'
    return False
  zip = zipfile.ZipFile(zip_file_path, 'r')
  for zipinfo in zip.infolist():
    file_windows_path_in_zip = zipinfo.filename.replace('/', '\\')
    if remove_sub_folder:
      sub_folder_end = file_windows_path_in_zip.find('\\')
      if sub_folder_end == -1:
        print ' - File found in root: "%s"' % (file_windows_path_in_zip,)
        return False
      file_path_in_folder = os.path.join(folder, 
          file_windows_path_in_zip[sub_folder_end + 1:])
    else:
      file_path_in_folder = os.path.join(folder, file_windows_path_in_zip)
    file_folder = os.path.dirname(file_path_in_folder)
    if not os.path.isdir(file_folder):
      CreateFolder(file_folder)
    status = ' + "%s"...' % (file_path_in_folder,)
    input_file = zip.open(zipinfo, 'r')
    output_file = open(file_path_in_folder, 'wb')
    if not PipeIO(input_file, output_file, zipinfo.file_size, status):
      return False
  return True

def DeleteFile(path):
  if not os.path.exists(path):
    return True
  print ' - "%s"...' % (path,),
  try:
    os.remove(path)
  except:
    print 'failed.'
    return False
  print 'ok.'
  return True

def DeleteFolder(path):
  if not os.path.exists(path):
    return True
  print ' - "%s"...' % (path,),
  try:
    os.rmdir(path)
  except:
    print 'failed.'
    return False
  print 'ok.'
  return True

def DeleteTree(path):
  if not os.path.exists(path):
    return True
  paths = [path]
  while paths:
    path = paths[-1]
    if path[:-1] != '\\':
      path += '\\'
    files = []
    folders = []
    for entry in os.listdir(path):
      full_entry = os.path.join(path, entry)
      if os.path.isdir(full_entry):
        folders.append(full_entry)
      if os.path.isfile(full_entry):
        files.append(full_entry)
    while files:
      if not DeleteFile(files.pop(0)):
        return False
    if folders:
      paths.extend(folders)
    else:
      DeleteFolder(paths.pop())
  return True

def GetChromiumLatest(destination_folder, clean=False, symbols=True):
  cleanup_files = []
  cleanup_folders = []
  zip_mimetype = 'application/zip'
  revision_file_name = os.path.join(destination_folder, 'revision.txt')
  remote_revision = GetLatestRevision()
  if os.path.exists(revision_file_name):
    revision_file = open(revision_file_name)
    try:
      local_revision = int(revision_file.read())
    except:
      print 'Local revision file corrupt and ignored.'
      revision_file.close()
    else:
      revision_file.close()
      if local_revision == remote_revision:
        print 'You already have the latest revision (%d).' % remote_revision
        return True
  if clean and os.path.exists(destination_folder):
    cleanup_folders.append(destination_folder)
  try:
    while True:
      # Remove previously downloaded files:
      if cleanup_files or cleanup_folders:
        print "Cleaning ..."
        while cleanup_files:
          if not DeleteFile(cleanup_files.pop(0)):
            return False
        while cleanup_folders:
          if not DeleteTree(cleanup_folders.pop(0)):
            return False
      # Create destination folder:
      if not os.path.exists(destination_folder):
        print 'Creating destination folder ...'
        if not CreateFolder(destination_folder):
          return False
      # Download chromium executable
      print 'Downloading Chromium binaries ...'
      chrome_file_path = os.path.join(destination_folder, 
          'chrome-win32[%d].zip' % remote_revision)
      if not Download(LATEST_CHROMIUM_URL, chrome_file_path, zip_mimetype):
        return False
      cleanup_files.append(chrome_file_path)
      if symbols:
        print 'Downloading Chromium symbols ...'
        # Download chromium symbols
        symbol_file_path = os.path.join(destination_folder, 
            'chrome-win32-symbols[%d].zip' % remote_revision)
        if not Download(LATEST_SYMBOLS_URL, symbol_file_path, zip_mimetype):
          return False
        cleanup_files.append(symbol_file_path)
        # Check if revision has changed while downloading...
        end_remote_revision = GetLatestRevision()
        if remote_revision == end_remote_revision:
          break
        else:
          print ('Revision changed from %d to %d while downloading, '
            'restarting...') % (remote_revision, end_remote_revision)
          remote_revision = end_remote_revision
      else:
        # If we only download one zip, there cannot be a revision mismatch.
        break
    # Unzip chromium executable
    print 'Unzipping Chromium binaries ...'
    cleanup_folders.append(destination_folder)
    if not Unzip(chrome_file_path, destination_folder, True):
      return False
    if symbols:
      print 'Unzipping Chromium symbols ...'
      # Unzip chromium symbols
      if not Unzip(symbol_file_path, destination_folder, True):
        return False
    cleanup_folders.remove(destination_folder)
    # Do a bit of cleanup before we tell the user we're done:
    if cleanup_files or cleanup_folders:
      print "Cleaning ..."
      while cleanup_files:
        if not DeleteFile(cleanup_files.pop(0)):
          return False
      while cleanup_folders:
        if not DeleteTree(cleanup_folders.pop(0)):
          return False
    # Done
    revision_file = open(revision_file_name, 'w')
    revision_file.write(str(remote_revision))
    revision_file.close()
    print 'Success: you have been updated to revision %d.' % remote_revision
    return True
  finally:
    if cleanup_files or cleanup_folders:
      print "Cleaning ..."
    for file_path in cleanup_files:
      DeleteFile(file_path)
    for folder_path in cleanup_folders:
      DeleteTree(folder_path)

def Main(my_name, *args):
  destination_folder = None
  clean = False
  symbols = True
  for arg in args:
    if arg in ['-h', '-?', '/?', '/h', '--help']:
      Help()
      return 0
    if arg.startswith('--'):
      split_at = arg.find('=')
      if split_at == -1:
        arg_name = arg[2:].lower()
        arg_value = ''
      else:
        arg_name = arg[2:split_at].lower()
        arg_value = arg[split_at + 1:]
      if arg_name == 'clean':
        clean = True
      elif arg_name == 'nosymbols':
        symbols = False
      else:
        print 'Unknown switch: "%s".' % arg_name
        Help()
        return -1
    elif not destination_folder:
      destination_folder = arg
    else:
      print 'Surplus argument: "%s".' % arg
      Help()
      return -1
  if destination_folder is None:
    destination_folder = os.getcwd()
  GetChromiumLatest(destination_folder, clean, symbols)
  
if __name__ == "__main__":
  exit(Main(*sys.argv))
