#!/usr/bin/env python
#
# Copyright (c) 2012 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.

import optparse
import os
import re
import shlex
import subprocess
import sys


def _PrintMessage(warnings, title, action, known_bugs_file):
  if warnings:
    print
    print '*' * 80
    print '%s warnings.' % title
    print '%s %s' % (action, known_bugs_file)
    print '-' * 80
    for warning in warnings:
      print warning
    print '-' * 80
    print


def _StripLineNumbers(current_warnings):
  re_line = r':\[line.*?\]$'
  return [re.sub(re_line, '', x) for x in current_warnings]


def _DiffKnownWarnings(current_warnings_set, known_bugs_file):
  with open(known_bugs_file, 'r') as known_bugs:
    known_bugs_set = set(known_bugs.read().splitlines())

  new_warnings = current_warnings_set - known_bugs_set
  _PrintMessage(sorted(new_warnings), 'New', 'Please fix, or perhaps add to',
                known_bugs_file)

  obsolete_warnings = known_bugs_set - current_warnings_set
  _PrintMessage(sorted(obsolete_warnings), 'Obsolete', 'Please remove from',
                known_bugs_file)

  count = len(new_warnings) + len(obsolete_warnings)
  if count:
    print '*** %d FindBugs warning%s! ***' % (count, 's' * (count > 1))
    if len(new_warnings):
      print '*** %d: new ***' % len(new_warnings)
    if len(obsolete_warnings):
      print '*** %d: obsolete ***' % len(obsolete_warnings)
    print
    print 'Alternatively,  rebaseline with --rebaseline command option'
    print
  else:
    print 'No new FindBugs warnings.'
  print
  return count


def _Rebaseline(current_warnings_set, known_bugs_file):
  with file(known_bugs_file, 'w') as known_bugs:
    for warning in sorted(current_warnings_set):
      print >>known_bugs, warning
  return 0


def _GetChromeClasses(release_version):
  chrome_src = os.getenv('CHROME_SRC')
  version = 'Debug'
  if release_version:
    version = 'Release'
  path = os.path.join(chrome_src, 'out', version)
  cmd = 'find %s -name "*.class"' % path
  proc = subprocess.Popen(shlex.split(cmd),
                          stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  out, err = proc.communicate()

  if not out:
    print 'No classes found in %s' % path
  return out


def _Run(exclude, known_bugs, classes_to_analyze, auxiliary_classes,
        rebaseline, release_version, findbug_args):
  """Run the FindBugs.

  Args:
    exclude: the exclude xml file, refer to FindBugs's -exclude command option.
    known_bugs: the text file of known bugs. The bugs in it will not be
                reported.
    classes_to_analyze: the list of classes need to analyze, refer to FindBug's
                        -onlyAnalyze command line option.
    auxiliary_classes: the classes help to analyze, refer to FindBug's
                       -auxclasspath command line option.
    rebaseline: True if the known_bugs file needs rebaseline.
    release_version: True if the release version needs check, otherwise check
                     debug version.
    findbug_args: addtional command line options needs pass to Findbugs.
  """

  chrome_src = os.getenv('CHROME_SRC')
  sdk_root = os.getenv('ANDROID_SDK_ROOT')
  sdk_version = os.getenv('ANDROID_SDK_VERSION')

  system_classes = []
  system_classes.append(os.path.join(sdk_root, 'platforms',
                                     'android-%s' % sdk_version, 'android.jar'))
  if auxiliary_classes:
    for classes in auxiliary_classes:
      system_classes.append(os.path.abspath(classes))

  cmd = '%s -textui -sortByClass ' % os.path.join(chrome_src, 'third_party',
                                                  'findbugs', 'bin', 'findbugs')
  cmd = '%s -pluginList %s' % (cmd, os.path.join(chrome_src, 'tools', 'android',
                                                 'findbugs_plugin', 'lib',
                                                 'chromiumPlugin.jar'))
  if len(system_classes):
    cmd = '%s -auxclasspath %s ' % (cmd, ':'.join(system_classes))

  if classes_to_analyze:
    cmd = '%s -onlyAnalyze %s ' % (cmd, classes_to_analyze)

  if exclude:
    cmd = '%s -exclude %s ' % (cmd, os.path.abspath(exclude))

  if findbug_args:
    cmd = '%s %s ' % (cmd, fingbug_args)


  chrome_classes = _GetChromeClasses(release_version)
  if not chrome_classes:
    return 1
  cmd = '%s %s ' % (cmd, chrome_classes)

  proc = subprocess.Popen(shlex.split(cmd),
                          stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  out, err = proc.communicate()
  current_warnings_set = set(_StripLineNumbers(filter(None, out.splitlines())))

  if rebaseline:
    return _Rebaseline(current_warnings_set, known_bugs)
  else:
    return _DiffKnownWarnings(current_warnings_set, known_bugs)

def Run(options):
  exclude_file = None
  known_bugs_file = None

  if options.exclude:
    exclude_file = options.exclude
  elif options.base_dir:
    exclude_file = os.path.join(options.base_dir, 'findbugs_exclude.xml')

  if options.known_bugs:
    known_bugs_file = options.known_bugs
  elif options.base_dir:
    known_bugs_file = os.path.join(options.base_dir, 'findbugs_known_bugs.txt')

  auxclasspath = None
  if options.auxclasspath:
    auxclasspath = options.auxclasspath.split(':')
  return _Run(exclude_file, known_bugs_file, options.only_analyze, auxclasspath,
              options.rebaseline, options.release_build, options.findbug_args)


def GetCommonParser():
  parser = optparse.OptionParser()
  parser.add_option('-r',
                    '--rebaseline',
                    action='store_true',
                    dest='rebaseline',
                    help='Rebaseline known findbugs issues.')

  parser.add_option('-a',
                    '--auxclasspath',
                    action='store',
                    default=None,
                    dest='auxclasspath',
                    help='Set aux classpath for analysis.')

  parser.add_option('-o',
                    '--only-analyze',
                    action='store',
                    default=None,
                    dest='only_analyze',
                    help='Only analyze the given classes and packages.')

  parser.add_option('-e',
                    '--exclude',
                    action='store',
                    default=None,
                    dest='exclude',
                    help='Exclude bugs matching given filter.')

  parser.add_option('-k',
                    '--known-bugs',
                    action='store',
                    default=None,
                    dest='known_bugs',
                    help='Not report the bugs in the given file.')

  parser.add_option('-l',
                    '--release-build',
                    action='store_true',
                    dest='release_build',
                    help='Analyze release build instead of debug.')

  parser.add_option('-f',
                    '--findbug-args',
                    action='store',
                    default=None,
                    dest='findbug_args',
                    help='Additional findbug arguments.')

  parser.add_option('-b',
                    '--base-dir',
                    action='store',
                    default=None,
                    dest='base_dir',
                    help='Base directory for configuration file.')

  return parser

def CheckEnvironment():
  if not (os.getenv('CHROME_SRC') and os.getenv('ANDROID_SDK_ROOT') and
          os.getenv('ANDROID_SDK_VERSION')):
    print 'Your build environment is not set up correctly.'
    print 'Please source build/android/envsetup.sh.'
    return False
  return True

def main(argv):
  parser = GetCommonParser()
  options, _ = parser.parse_args()

  return Run(options)

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