# 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 logging
import os
import xml.dom.minidom

from devil.utils import cmd_helper
from pylib import constants
from pylib.constants import host_paths


_FINDBUGS_HOME = os.path.join(host_paths.DIR_SOURCE_ROOT, 'third_party',
                              'findbugs')
_FINDBUGS_JAR = os.path.join(_FINDBUGS_HOME, 'lib', 'findbugs.jar')
_FINDBUGS_MAX_HEAP = 768
_FINDBUGS_PLUGIN_PATH = os.path.join(
    host_paths.DIR_SOURCE_ROOT, 'tools', 'android', 'findbugs_plugin', 'lib',
    'chromiumPlugin.jar')


def _ParseXmlResults(results_doc):
  warnings = set()
  for en in (n for n in results_doc.documentElement.childNodes
             if n.nodeType == xml.dom.Node.ELEMENT_NODE):
    if en.tagName == 'BugInstance':
      warnings.add(_ParseBugInstance(en))
  return warnings


def _GetMessage(node):
  for c in (n for n in node.childNodes
            if n.nodeType == xml.dom.Node.ELEMENT_NODE):
    if c.tagName == 'Message':
      if (len(c.childNodes) == 1
          and c.childNodes[0].nodeType == xml.dom.Node.TEXT_NODE):
        return c.childNodes[0].data
  return None


def _ParseBugInstance(node):
  bug = FindBugsWarning(node.getAttribute('type'))
  msg_parts = []
  for c in (n for n in node.childNodes
            if n.nodeType == xml.dom.Node.ELEMENT_NODE):
    if c.tagName == 'Class':
      msg_parts.append(_GetMessage(c))
    elif c.tagName == 'Method':
      msg_parts.append(_GetMessage(c))
    elif c.tagName == 'Field':
      msg_parts.append(_GetMessage(c))
    elif c.tagName == 'SourceLine':
      bug.file_name = c.getAttribute('sourcefile')
      if c.hasAttribute('start'):
        bug.start_line = int(c.getAttribute('start'))
      if c.hasAttribute('end'):
        bug.end_line = int(c.getAttribute('end'))
      msg_parts.append(_GetMessage(c))
    elif (c.tagName == 'ShortMessage' and len(c.childNodes) == 1
          and c.childNodes[0].nodeType == xml.dom.Node.TEXT_NODE):
      msg_parts.append(c.childNodes[0].data)
  bug.message = tuple(m for m in msg_parts if m)
  return bug


class FindBugsWarning(object):

  def __init__(self, bug_type='', end_line=0, file_name='', message=None,
               start_line=0):
    self.bug_type = bug_type
    self.end_line = end_line
    self.file_name = file_name
    if message is None:
      self.message = tuple()
    else:
      self.message = message
    self.start_line = start_line

  def __cmp__(self, other):
    return (cmp(self.file_name, other.file_name)
            or cmp(self.start_line, other.start_line)
            or cmp(self.end_line, other.end_line)
            or cmp(self.bug_type, other.bug_type)
            or cmp(self.message, other.message))

  def __eq__(self, other):
    return self.__dict__ == other.__dict__

  def __hash__(self):
    return hash((self.bug_type, self.end_line, self.file_name, self.message,
                 self.start_line))

  def __ne__(self, other):
    return not self == other

  def __str__(self):
    return '%s: %s' % (self.bug_type, '\n  '.join(self.message))


def Run(exclude, classes_to_analyze, auxiliary_classes, output_file,
        findbug_args, jars):
  """Run FindBugs.

  Args:
    exclude: the exclude xml file, refer to FindBugs's -exclude command option.
    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.
    output_file: An optional path to dump XML results to.
    findbug_args: A list of addtional command line options to pass to Findbugs.
  """
  # TODO(jbudorick): Get this from the build system.
  system_classes = [
    os.path.join(constants.ANDROID_SDK_ROOT, 'platforms',
                 'android-%s' % constants.ANDROID_SDK_VERSION, 'android.jar')
  ]
  system_classes.extend(os.path.abspath(classes)
                        for classes in auxiliary_classes or [])

  cmd = ['java',
         '-classpath', '%s:' % _FINDBUGS_JAR,
         '-Xmx%dm' % _FINDBUGS_MAX_HEAP,
         '-Dfindbugs.home="%s"' % _FINDBUGS_HOME,
         '-jar', _FINDBUGS_JAR,
         '-textui', '-sortByClass',
         '-pluginList', _FINDBUGS_PLUGIN_PATH, '-xml:withMessages']
  if system_classes:
    cmd.extend(['-auxclasspath', ':'.join(system_classes)])
  if classes_to_analyze:
    cmd.extend(['-onlyAnalyze', classes_to_analyze])
  if exclude:
    cmd.extend(['-exclude', os.path.abspath(exclude)])
  if output_file:
    cmd.extend(['-output', output_file])
  if findbug_args:
    cmd.extend(findbug_args)
  cmd.extend(os.path.abspath(j) for j in jars or [])

  if output_file:
    _, _, stderr = cmd_helper.GetCmdStatusOutputAndError(cmd)

    results_doc = xml.dom.minidom.parse(output_file)
  else:
    _, raw_out, stderr = cmd_helper.GetCmdStatusOutputAndError(cmd)
    results_doc = xml.dom.minidom.parseString(raw_out)

  for line in stderr.splitlines():
    logging.debug('  %s', line)

  current_warnings_set = _ParseXmlResults(results_doc)

  return (' '.join(cmd), current_warnings_set)