"""
PyLint integration for Wing.

Copyright (c) 2006-2007 Markus Meyer <meyer@mesw.de>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

----------------------------------------------------------------------------
Change Log
----------------------------------------------------------------------------

Version 1.0 (2006-10-12)

* First release

Version 1.1 (2007-03-19)

* Fix compatibility issues with newer PyLint versions
* Fix error message when PyLint returns no errors

Version 1.2 (2008-06-05)  Modifications by Wingware:

* Pass configured environment to the pylint sub-process
  (includes also PYTHONPATH if it's set in project)
* Use presence of a settings file to enable/disable for easier upgrades
* Removed obsolete example args
* Renamed "preferences" to "configure" in context menu to avoid confusion
* Renamed command execute_pylint to pylint_execute and pylint_preferences
  to pylint_configure
* Changed execution of pylint to be asyncronous

Version 1.3 (2008-09-30)  Modifications by Wingware:

* Added ability to process whole packages, if current file is in
  a package

Version 1.4 (2010-06-03)  Modifications by Jan Nabbefeld:

* Added ability to use per project pylintpanel.cfg (copy the file from
  your user settings directory into the same directory as your project file)

Version 1.5 (2011-09-07)  Modifications by Ronan Le Gallic and Wingware:

* Support for pylint 0.24+ column number output (Le Gallic)
* Avoid reporting empty output in pylint 0.24+ as an error (Le Gallic)
* Fix failure to set parseable output flag (Wingware)
* Don't assume scope names are in all pylint messages (Wingware)
* Better handle multiple pylint views (Wingware)

Version 1.6 (2013-10-29)  Modifications by Wingware:

* Support for pylint 1.0.0+
* Rescan pylint version when the configuration file is saved
* Added use_project_env configuration option to allow using a pylint
  that cannot run in the project-defined environment
* Print a warning to the log if --output-format or --msg-template
  are used as args in configuration since these break output parsing
* Try to decode pylint output so non-ascii characters are correct
* Always show PyLint tool when processing a new file
* Works in Wing 5

Version 1.7 (2016-02-04)  Modifications by Steve Barnes <gadgetsteve@hotmail.com>:

* Add pkg_timeout configuration item to allow longer timeouts on package parsing

Version 1.8 (2016-03-08)  Modifications by Wingware

* Use longer delimiter in msg template to avoid problems parsing output 

Version 1.9 (2021-03-16)  Modifications by Wingware

* Use PYTHONIOENCODING=utf-8 if pylint runs under Python 3.4+


"""

from __future__ import print_function

import os
import wingapi
import sys
import time

PYLINTPANEL_VERSION = "1.9"

import re
_AI = wingapi.CArgInfo

# This script is disabled and unsupported.  It has been replaced with the
# Code Warnings tool in Wing Pro and is retained here as a scripting example.
_ignore_scripts = 1

# Scripts can be internationalized with gettext.  Strings to be translated
# are sent to _() as in the code below.
import gettext
from wingbase import mime
from wingbase import pyutils
if sys.version_info < (3, 0):
  _ = gettext.translation('scripts_pylintpanel', fallback=True).ugettext
else:
  _ = gettext.translation('scripts_pylintpanel', fallback=True).gettext

# This special attribute is used so that the script manager can translate
# also docstrings for the commands found here
_i18n_module = 'scripts_pylintpanel'

######################################################################
# Utilities

gMessageCategories = [
  ("errors", _("Errors"), _("Errors that must be fixed")),
  ("warnings", _("Warnings"), _("Warnings that could indicate problems")),
  ("information", _("Info"), _("General informative messages"))
]

# Currently only supporting one view; will be set later
gViews = [None]


######################################################################
# Configuration file support

class PylintConfig:
  def __init__(self):
    def force_rescan(*args):
      self.rescan_pylint(True)
    wingapi.gApplication.Connect('project-open', force_rescan)
    from proj import attribs
    wingapi.gApplication.fSingletons.fFileAttribMgr.ConnectToValue(attribs.kEnvVars,
                                                                   force_rescan,
                                                                   wingapi.gApplication)

    def doc_opened(doc):
      fn = doc.GetFilename()
      if fn.startswith('unknown:'):
        return
      fn = os.path.normpath(fn)
      cfg_fn = os.path.normpath(self._get_config_file_name())
      if fn == cfg_fn:
        doc.Connect('save-point', self.rescan_pylint)
    wingapi.gApplication.Connect('document-open', doc_opened)

  def get(self, name, default=""):
    """
    Get configuration option from configuration file
    """
    proj = wingapi.gApplication.GetProject()
    try:
      lines = open(self._get_config_file_name(), "rt").readlines()
    except IOError:
      return default
    for line in lines:
      if proj is not None:
        line = proj.ExpandEnvVars(line)
      words = line.split("=", 1)
      if len(words) == 2:
        key = words[0].strip()
        value = words[1].strip()
        if key == name:
          return value
    return default

  def edit(self):
    """
    Edit and possibly create configuration file. Currently, this will just
    open the configuration file inside Wing.
    """
    try:
      # Check if config file can be opened
      cfgfile = open(self._get_config_file_name(), "rt")
    except IOError:
      # The config file does not yet exist, create it
      cfgfile = open(self._get_config_file_name(), "wt")
      cfgfile.writelines([
        '#\n',
        '# PyLint Panel Configuration\n',
        '#\n',
        '\n',
        '# Full path to PyLint executable\n',
        'command = \n',
        '#command = /usr/bin/pylint\n',
        '#command = C:\\Python27\\Scripts\\pylint.exe\n',
        '\n',
        '# Additional args to give to PyLint (may be blank)\n',
        '# Note: Setting --output-format or --msg-template will break output parsing\n',
        'args = \n',
        '\n',
        '# Timeout for execution of pylint command on a single file\n',
        'timeout = 30\n',
        '\n',
        '# Timeout for execution of pylint command on a package\n',
        'pkg_timeout = 60\n',
        '\n',
        '# Save files before running PyLint (0=no, 1=current file only, 2=save all files)\n',
        'autosave = 1\n'
        '\n'
        '# Set to 0 to use startup environment instead of\n'
        '# the environment defined in the project (avoids\n'
        '# conflicts between selected pylint and project env\n'
        '# but may also prevent resolving imports\n'
        'use_project_env = 1\n'
        ])
    cfgfile.close()

    ed = wingapi.gApplication.OpenEditor(self._get_config_file_name())

  def _get_config_file_name(self):
    """
    Get full name and path of config file
    """
    # Check for pylintpanel.cfg file in current project directory
    # before using the one in the users settings directory
    app = wingapi.gApplication
    proj = app.GetProject()
    if proj is not None:
      proj_dir = os.path.dirname(proj.GetFilename())
      cfg = os.path.join(proj_dir, "pylintpanel.cfg")
      if os.path.exists(cfg):
        return cfg

    dir = wingapi.gApplication.GetUserSettingsDir()
    return os.path.join(dir, "pylintpanel.cfg")

  def rescan_pylint(self, save_point):
    if save_point and _init_pylint_version is not None:
      print("RESCAN")
      global kPyLintVersion
      kPyLintVersion = '0.0.0'
      _init_pylint_version()

gTheConfig = PylintConfig()

# Used only for pylint < 1.0.0
kResultParseExpr = re.compile("(?P<type>[^:]+):[ ]*(?P<line>[0-9,]+):[ ]*(?P<descr>.*)")
kParseableResultParseExpr = re.compile("(?P<path>[^:]+):(?P<line>[0-9,]+):[ ]*\[(?P<type>.*)\][ ]*(?P<descr>.*)")

# Used for pylint >= 1.0.0 with --msg-template arg to specify output format
kDelimiter = ':::::'
kNewStyleMsgTemplate = '{msg_id}%s{abspath}%s{line:d}%s{column:d}%s{msg}' % (kDelimiter, kDelimiter, kDelimiter, kDelimiter)

######################################################################
# Commands

def pylint_configure():
  """Show the pylint configuration file so it can be edited"""
  gTheConfig.edit()

def pylint_copy_results():
  """ Copies all results from the displayed pylint results list. """
  n = gViews[0].fGtkWidget.get_current_page()
  view = gViews[0].fGtkWidget.get_nth_page(n)
  if view is None:
    return

  catkey = gMessageCategories[n][0]
  tree, sview = gViews[0].fTrees[catkey]
  if wgtk.Qt:
    visible = []
    for i in range(0, 3):
      if tree.columnWidth(i) > 0:
        visible.append(i)
  else:
    visible = [i for i, col in enumerate(tree.get_columns()) if col.get_visible()]

  text_lines = []
  for row in tree.GetContent():
    line = ''
    for i, col_i in enumerate(visible):
      if i != 0:
        line += '\t'
      if col_i == 0:
        line += _("Line") + ' '
      elif col_i == 1:
        line += _("Column") + ' '
      line += row[col_i]

    text_lines.append(line)

  eol = ('\n' if sys.platform != 'win32' else '\r\n')
  text = eol.join(text_lines)

  wingapi.gApplication.SetClipboard(text)

def pylint_copy_selected_results():
  """ Copy the selected pylint results to the clipboard. """
  selectedIndexes = _get_selected_indexes()
  if not selectedIndexes:
    return

  line = ''
  for i, indx in enumerate(selectedIndexes):
    if i != 0:
      line += '\t'
    if i == 0:
      line += _("Line") + ' ' + indx.data()
    elif i == 1:
      line += _("Column") + ' ' + indx.data()
    else:
      line += indx.data()

  wingapi.gApplication.SetClipboard(line)

def pylint_copy_selected_line_number():
  """ Copy the line number for the currently selected pylint result. """
  selectedIndexes = _get_selected_indexes()
  if not selectedIndexes:
    return

  msg = None
  for i, indx in enumerate(selectedIndexes):
    if indx.column() == 0:
      msg = indx.data()
      break

  wingapi.gApplication.SetClipboard(msg)

def pylint_copy_selected_message():
  """ Copy the currently selected pylint result message. """
  selectedIndexes = _get_selected_indexes()
  if not selectedIndexes:
    return

  msg = None
  for i, indx in enumerate(selectedIndexes):
    if indx.column() == 2:
      msg = indx.data()
      break

  wingapi.gApplication.SetClipboard(msg)

def _get_selected_indexes():
  """ Retrieves list of currently selected indexes """
  n = gViews[0].fGtkWidget.get_current_page()
  view = gViews[0].fGtkWidget.get_nth_page(n)
  if view is None:
    return

  catkey = gMessageCategories[n][0]
  tree, sview = gViews[0].fTrees[catkey]

  return view.selectedIndexes()

def pylint_execute(show_panel=True):
  """Execute pylint for the current editor"""
  if show_panel:
    wingapi.gApplication.ShowTool(_kPanelID)
  filenames = _get_selected_python_files()
  _pylint_execute(filenames)

def _IsAvailable_pylint_execute():
  python_files = _get_selected_python_files()
  return len(python_files) > 0

pylint_execute.available = _IsAvailable_pylint_execute

def pylint_package_execute(show_panel=True):
  """Execute pylint on all files in the package to which the
  file in the current editor belongs"""

  if show_panel:
    wingapi.gApplication.ShowTool(_kPanelID)
  packages = _get_selected_packages()
  _pylint_execute(packages)

def _IsAvailable_pylint_package_execute():
  packages = _get_selected_packages()
  return len(packages) > 0

pylint_package_execute.available = _IsAvailable_pylint_package_execute

######################################################################
# XXX This is advanced scripting that accesses Wing internals, which
# XXX are subject to change from version to version without notice.

from wingbase import location
from wingbase import mime
from guiutils import wgtk
from guiutils import dockview
from guiutils import wingview
from guiutils import winmgr

from command import commandmgr
import guimgr.menus

def pylint_show_docs():
  """Show the Wing documentation section for the PyLint integration"""
  wingapi.gApplication.ExecuteCommand('show-document', section='edit/pylint')

def _get_selected_packages():
  app = wingapi.gApplication
  filenames = app.GetCurrentFiles()
  packages = []
  for filename in filenames:
    dirname = os.path.dirname(filename)
    if os.path.exists(os.path.join(dirname, '__init__.py')):
      if not dirname in packages:
        packages.append(dirname)
  return packages

def _get_selected_python_files():
  app = wingapi.gApplication
  filenames = app.GetCurrentFiles()
  if not filenames:
    return []
  python_files = []
  for filename in filenames:
    mimetype = _GetMimeType(filename)
    if mimetype == 'text/x-python':
      python_files.append(filename)
  return python_files

kPyLintVersion = '0.0.0'
kPythonVersionTriple = (2, 7, 0)
def _init_pylint_version():

  if kPyLintVersion != '0.0.0':
    return kPyLintVersion

  app = wingapi.gApplication

  pylint_command = gTheConfig.get("command", None)
  if pylint_command is None:
    return None

  # Execute PyLint asyncronously
  cmd = pylint_command
  args = ('--version',)
  start_time = time.time()
  timeout = 10
  print('getting pylint version')
  import config
  handler = app.AsyncExecuteCommandLineE(cmd, os.getcwd(), config.gStartupEnv, *args)

  def poll():
    global kPyLintVersion
    global kPythonVersionTriple

    if handler.Iterate():
      stdout, stderr, err, status = handler.Terminate()
      if err:
        print('err=', err)
        print("stderr:\n", stderr.rstrip())
      else:
        if len(stderr.strip()) > 0:
          print("stderr:\n", stderr.rstrip())
        print("stdout:\n", stdout.rstrip())
        for line in stdout.splitlines():
          if line.startswith('pylint') or line.startswith('python3-pylint'):
            parts = line.split()
            if len(parts) >= 2:
              version = parts[1].strip()
              if version.endswith(','):
                version = version[:-1]
              kPyLintVersion = version
              print('set pylint version=', version)
          elif line.startswith('Python'):
            parts = line.strip().split(' ')
            if len(parts) >= 2:
              pyversion_parts = parts[1].strip().split('.')
              if len(pyversion_parts) == 3:
                major, minor, micro = pyversion_parts
                if not micro.isdigit():
                  micro = '0'
                try:
                  kPythonVersionTriple = (int(major), int(minor), int(micro))
                except:
                  kPythonVersionTriple = (2, 7, 0)
              
      global _gPendingExecute
      if kPyLintVersion == '0.0.0':
        print('failed to set pylint version!')
        if _gPendingExecute is not None:
          print('could not execute pending pylint update')
      if _gPendingExecute is not None:
        def do_pending(pending=_gPendingExecute):
          _pylint_execute(pending)
        wingapi.gApplication.InstallTimeout(1, do_pending)
        _gPendingExecute = None
      return False
    elif time.time() > start_time + timeout:
      print('timeout')
      if _gPendingExecute is not None:
        print('could not execute pending pylint update')
      return False
    else:
      return True

  wingapi.gApplication.InstallTimeout(100, poll)

_gPendingExecute = None
def _pylint_execute(filenames):

  # Wait until Pylint version is known
  if kPyLintVersion == '0.0.0':
    print("Waiting for PyLint version detection to complete...")
    global _gPendingExecute
    _gPendingExecute = filenames
    return

  if gViews[0] is None:
    # Panel is not visible
    return

  print('-' * 60)

  view = gViews[0]
  app = wingapi.gApplication

  # Parse config
  pylint_command = gTheConfig.get("command", None)
  pylint_args = gTheConfig.get("args", "")
  pylint_timeout = gTheConfig.get("timeout", "10000")
  pylint_pkg_timeout = gTheConfig.get("pkg_timeout", "10000")
  pylint_autosave = gTheConfig.get("autosave", "1")
  try:
    pylint_use_project_env = int(gTheConfig.get("use_project_env", "1").strip())
  except:
    pylint_use_project_env = 1

  # Look for project-specific .pylintrc file
  if '--rcfile=' not in pylint_args:
    proj = app.GetProject()
    proj_dir = os.path.dirname(proj.GetFilename())
    pylintrc = os.path.join(proj_dir, '.pylintrc')
    if os.path.isfile(pylintrc):
      pylint_args = '--rcfile="%s" %s' % (pylintrc, pylint_args)

  if pylint_command is None:
    app.ShowMessageDialog(_('Error'), _('PyLint panel configuration file not found. '
                          'Choose "Configure" from the context menu to edit '
                          'the configuration file.  You will need to install pylint '
                          'separately.'))
    return

  try:
    timeout = int(pylint_timeout)
    pkg_timeout = int(pylint_pkg_timeout)
    autosave = int(pylint_autosave)
  except ValueError:
    app.ShowMessageDialog(_("Error"), _("Invalid values specified in configuration file"))
    return

  # Guess at run directory
  rundir = os.path.dirname(filenames[0])
  if len(filenames) == 1:
    if os.path.isdir(filenames[0]):
      base_msg = _("Updating for package %s") % os.path.basename(filenames[0])
      use_timeout = pkg_timeout
    else:
      base_msg = _("Updating for %s") % os.path.basename(filenames[0])
      use_timeout = timeout
  else:
    base_msg = _("Updating for %i items") % len(filenames)

  use_pythonioencoding_utf8 = (kPythonVersionTriple >= (3, 4))
  if use_pythonioencoding_utf8:
    encoding = 'utf-8'
  else:
    # Guess encoding that pylint will be outputting, based on encoding of the file(s)
    encoding_loc = location.CreateFromName(filenames[0])
    encoding, source = mime.GuessTextFileEncoding(encoding_loc)
    if encoding is None:
      import config
      encoding = config.kFileSystemEncoding

  # Save active document before executing PyLint
  if autosave == 1 and app.CommandAvailable("save"):
    app.ExecuteCommand("save")
  elif autosave == 2 and app.CommandAvailable("save-all"):
    app.ExecuteCommand("save-all")

  # Completion routine for simplified new style result parsing
  if kPyLintVersion >= '1.0.0':
    def _update_tree(result):
      try:
        result = pyutils.unicode(result, encoding)
      except:
        print("Could not decode pylint output (tried %s)" % encoding)
      resultLines = result.split('\n')

      tree_contents = [ [], [], [], [] ]

      for line in resultLines:

        parts = line.split(kDelimiter)
        if len(parts) != 5:
          continue
        msg_type, msg_path, msg_line, msg_col, msg_descr = parts
        if msg_type[0] == 'F' or msg_type[0] == 'E':
          msg_index = 0
        elif msg_type[0] == 'W':
          msg_index = 1
        else:
          msg_index = 2
        if len(filenames) > 1 or os.path.isdir(filenames[0]):
          msg_disp_line = os.path.basename(msg_path) + ':' + msg_line
        else:
          msg_disp_line = msg_line
        tree_contents[msg_index].append(
          ((msg_disp_line,
            msg_col,
            msg_type + ": " + msg_descr,
            msg_path,
            msg_line),))

      view.set_tree_contents(tree_contents)

  # Completion routine for pylint versions < 1.0.0
  else:
    def _update_tree(result):
      try:
        result = pyutils.unicode(result, encoding)
      except:
        print("Could not decode pylint output (tried %s)" % encoding)
      resultLines = result.split('\n')

      tree_contents = [ [], [], [], [] ]

      for line in resultLines:

        parts = line.split(':')
        # Form output with --output-format=parseable (used w/ >1 file or package name)
        if len(parts) >= 3 and parts[2].strip().startswith('['):
          matchobj = kParseableResultParseExpr.match(line)
          if matchobj is not None:

            msg_type  = matchobj.group('type').strip()
            msg_line, msg_col = (matchobj.group('line') + ',0').split(',')[0:2]
            msg_descr = matchobj.group('descr').strip()

            if ',' in msg_type:
              type_parts = msg_type.split(',', 1)
              msg_type = type_parts[0].strip()
              msg_descr = type_parts[1].strip() + ': ' + msg_descr

            if msg_type[0] == 'F' or msg_type[0] == 'E':
              msg_index = 0
            elif msg_type[0] == 'W':
              msg_index = 1
            else:
              msg_index = 2
            fullpath = os.path.join(rundir, matchobj.group('path'))
            tree_contents[msg_index].append(
              ((os.path.basename(fullpath) + ':' + msg_line,
                msg_col,
                msg_type + ": " + msg_descr,
                fullpath,
                msg_line),))

        # Default output format (only used w/ one file in filenames list)
        else:
          matchobj = kResultParseExpr.match(line)
          if matchobj is not None:

            msg_type = matchobj.group('type').strip()
            msg_line, msg_col = (matchobj.group('line') + ',0').split(',')[0:2]
            msg_descr = matchobj.group('descr').strip()

            if msg_type[0] == 'F' or msg_type[0] == 'E':
              msg_index = 0
            elif msg_type[0] == 'W':
              msg_index = 1
            else:
              msg_index = 2
            tree_contents[msg_index].append(
              ((msg_line,
                msg_col,
                msg_type + ": " + msg_descr,
                filenames[0],
                msg_line),))

      view.set_tree_contents(tree_contents)

  # Show pending execution message in tree column title
  view._ShowStatusMessage(base_msg)

  from wingbase import spawn
  import config

  # Execute PyLint asyncronously
  cmd = pylint_command
  if kPyLintVersion >= '1.0.0':
    args = ["--reports=n", "--msg-template='%s'" % kNewStyleMsgTemplate]
  elif kPyLintVersion > '0.15':
    args = []
  else:
    args = ['--reports=n', '--include-ids=yes']
  # For Pylint < 1.0.0: Column info (and possibly other things) is not
  # available w/ parseable output so only use it where we need to get the
  # file name b/c we're scanning multiple files or a package directory
  if kPyLintVersion < '1.0.0':
    if len(filenames) > 1:
      args.append('--output-format=parseable')
    elif os.path.isdir(filenames[0]):
      args.append('--output-format=parseable')
  user_args = spawn.ParseCmdArgs(pylint_args, ' ')
  for user_arg in user_args:
    if user_arg.startswith('--output-format') or \
       user_arg.startswith('--msg-template'):
      if kPyLintVersion < '1.0.0':
        args.append(user_arg)
        print("WARNING: Argument may break output parsing:", user_arg)
      else:
        print("WARNING: Removing argument that breaks output parsing:", user_arg)
    else:
      args.append(user_arg)
  for filename in filenames:
    args.append(filename.encode(config.kFileSystemEncoding))
  args = tuple(args)
  if pylint_use_project_env:
    env = app.GetProject().GetEnvironment(filenames[0], set_pypath=True)
  else:
    env = config.gStartupEnv
    
  if use_pythonioencoding_utf8:
    env = env.copy()
    env['PYTHONIOENCODING'] = 'utf-8'
    
  start_time = time.time()
  print('pylint exec in directory', rundir)
  if pylint_use_project_env:
    print('env(project)=', env)
  else:
    print('env(startup)=', env)
  print(cmd, args)
  handler = app.AsyncExecuteCommandLineE(cmd, rundir, env, *args)
  last_dot = [int(start_time)]
  dots = []

  def poll():
    if handler.Iterate():
      view._ShowStatusMessage('')
      stdout, stderr, err, status = handler.Terminate()
      if err or (not stdout and (kPyLintVersion < '0.24')):
        app.ShowMessageDialog(_("PyLint Failed"), _("Error executing PyLint:  Command failed with error=%s, status=%s; stderr:\n%s") % (err, status, stderr))
      else:
        if len(stderr.strip()) > 0:
          print("pylint stderr:\n", stderr.rstrip())
        print("pylint stdout:\n", stdout.rstrip())
        _update_tree(stdout)
      print('-' * 60)
      return False
    elif time.time() > start_time + use_timeout:
      view._ShowStatusMessage('')
      stdout, stderr, err, status = handler.Terminate()
      app.ShowMessageDialog(_("PyLint Timed Out"), _("PyLint timed out:  Command did not complete within timeout of %i seconds.  Right click on the PyLint tool to configure this value.  Output from PyLint:\n\n%s") % (use_timeout, stderr + stdout))
      print('-' * 60)
      return False
    else:
      if int(time.time()) > last_dot[0]:
        dots.append('.')
        if len(dots) > 3:
          while dots:
            dots.pop()
        view._ShowStatusMessage(base_msg + ''.join(dots))
        last_dot[0] = int(time.time())
      return True

  wingapi.gApplication.InstallTimeout(100, poll)

def _GetMimeType(filename):
  loc = location.CreateFromName(filename)
  return wingapi.gApplication.fSingletons.fFileAttribMgr.GetProbableMimeType(loc)

# Note that panel IDs must be globally unique so all user-provided panels
# MUST add a random uniquifier after '#'.  The panel can still be referred
# to by the portion of the name before '#' and Wing will warn when there
# are multiple panel definitions with the same base name (in which case
# Wing-defined panels win over user-defined panels and otherwise the
# last user-defined panel type wins when referred to w/o the uniquifier).
_kPanelID = 'pylintpanel#02EFWRQK9X24'

class _CPylintPanelDefn(dockview.CPanelDefn):
  """Panel definition for the project manager"""

  def __init__(self, singletons):
    self.fSingletons = singletons
    dockview.CPanelDefn.__init__(self, self.fSingletons.fPanelMgr,
                                 _kPanelID, 'tall', 0)
    winmgr.CWindowConfig(self.fSingletons.fWinMgr, 'panel:%s' % _kPanelID,
                         size=(350, 1000))

  def _CreateView(self):
    self.fSingletons.fUseMgr.Count('create-panel.%s' % self.fName)
    return _CPylintView(self.fSingletons)

  def _GetLabel(self, panel_instance):
    """Get (label, prefix, suffix) for the display label to use for the given
    panel instance"""

    return _('PyLint'), '', ''

  def _GetTitle(self, panel_instance):
    """Get full title for the given panel instance"""

    return _('PyLint Panel')

class _CPylintViewCommands(commandmgr.CClassCommandMap):

  kDomain = 'user'
  kPackage = 'pylintpanel'

  def __init__(self, singletons, view):
    commandmgr.CClassCommandMap.__init__(self, i18n_module=_i18n_module)
    assert isinstance(view, _CPylintView)

    self.fSingletons = singletons
    self.__fView = view

class _CPylintView(wingview.CViewController):
  """A single template manager view"""

  def __init__(self, singletons):
    """ Constructor """

    # Init inherited
    wingview.CViewController.__init__(self, ())

    # External managers
    self.fSingletons = singletons

    self.__fCmdMap = _CPylintViewCommands(self.fSingletons, self)

    self.fTrees = {}
    self.fLabels = {}

    self.__CreateGui()

    _init_pylint_version()

    # Remember that this is the default view now
    if gViews[0] is None:
      gViews[0] = self
    else:
      gViews.append(self)

  def _destroy_impl(self):
    for tree, sview in list(self.fTrees.values()):
      if wgtk is not None:
        wgtk.Destroy(tree)

    # Life-cycle oddity when script is being reloaded
    if gViews is None:
      return
    
    try:
      gViews.remove(self)
    except ValueError:
      pass
    if len(gViews) == 0:
      gViews.append(None)

  def set_tree_contents(self, tree_contents):
    idx = 0
    for catkey, labeltext, tooltip in gMessageCategories:
      label = gViews[0].fLabels[catkey]
      tb = gViews[0].fGtkWidget.tabBar()
      tb.setTabText(idx, '(%i)' % len(tree_contents[idx]))
      tree, sview = gViews[0].fTrees[catkey]
      tree.set_contents(tree_contents[idx])

      tree.setColumnHidden(1, True)
      for row in tree_contents[idx]:
        if row[0][1] != '0':
          tree.setColumnHidden(1, False)
          break
      # Needed so horizontal scrollbar shows up
      tree.header().setStretchLastSection(False)

      # XXX Not yet implemented (see below)
      #self.set_tooltips(tree, tree_contents[idx])

      idx += 1

  def set_tooltips(self, tree, tree_contents):
    for row, data in enumerate(tree_contents):
      lineno, col, msg = data[0][:3]
      code = msg.split(':')[0]
      tip = self._get_tooltip(code)
      if tip:
        tree.set_cell_tooltip(row, 2, tip)

  def _get_tooltip(self, code):
    # Should use precomputed/cached data, with cache file in
    # a file within wingapi.gApplication.GetUserSettingsDir()
    # May want to add a config option to PyLintConfig and a
    # context menu item to prebuild the cache if there's a way
    # to get all valid pylint codes.  Should invoke pylint
    # as in _pylint_execute but with --help-msg=<code> and
    # different completion/result handling.
    return None

  ##########################################################################
  # Inherited calls from wingview.CViewController
  ##########################################################################

  def GetDisplayTitle(self):
    """ Returns the title of this view suitable for display. """

    return _("PyLint Panel")

  def GetCommandMap(self):
    """ Get the command map object for this view. """

    return self.__fCmdMap

  def BecomeActive(self):
    pass

  ##########################################################################
  # Popup menu and actions
  ##########################################################################

  def __CreateGui(self):
    notebook = wgtk.PopupNotebook()

    for catkey, label, tooltip in gMessageCategories:
      tree = wgtk.SimpleTree([_("Line"), _("Column"), _("Message"), _("Full Path"), _("Line Number")], plain_text=True)
      tree.setColumnHidden(1, True)
      tree.setColumnHidden(3, True)
      tree.setColumnHidden(4, True)

      wgtk.gui_connect(tree, 'button-press-event', self.__CB_ButtonPress)
      sel_model = tree.selectionModel()
      sel_model.selectionChanged.connect(self.__CB_SelectionChanged)
      wgtk.InitialShow(tree)

      tab_label = wgtk.QLabel(label)
      tab_label.setToolTip(wgtk.GetTipRichText(tooltip))
      notebook.append_page(tree, tab_label)
      sview = None

      self.__fNotebook = notebook

      self.fTrees[catkey] = (tree, sview)
      self.fLabels[catkey] = tab_label

    def popup_menu(w, event):
      menu = self.CreateContextMenu(event)
      menu.Popup(event)
    wgtk.gui_connect(notebook, 'popup', popup_menu)
    notebook.set_current_page(0)
    self._SetGtkWidget(notebook)

  def CreateContextMenu(self, event):
    """Construct popup menu for this object."""

    self.fSingletons.fUseMgr.Count('pylint.context-menu')
    
    update_label = _("Update")
    update_label2 = _("Update for Package")
    app = wingapi.gApplication
    ed = app.GetActiveEditor()
    if ed:
      filename = ed.GetDocument().GetFilename()
      update_label = _("Update for %s") % os.path.basename(filename)
      dirname = os.path.dirname(filename)
      if os.path.exists(os.path.join(dirname, '__init__.py')):
        update_label2 = _("Update for Package %s") % os.path.basename(dirname)


    n = gViews[0].fGtkWidget.get_current_page()
    if n == 2:
      copy_label = _("Copy All Info Messages")
    elif n == 1:
      copy_label = _("Copy All Warnings")
    else:
      copy_label = _("Copy All Errors")

    kPopupDefn = [
      ( update_label, 'pylint-execute', {'uscoremagic': False } ),
      ( update_label2, 'pylint-package-execute', {'uscoremagic': False } ),
      ( copy_label, 'pylint-copy-results'),
      ( _("Copy Selected Results"), 'pylint-copy-selected-results'),
      ( _("Copy Selected Line Number"), 'pylint-copy-selected-line-number'),
      ( _("Copy Selected Message"), 'pylint-copy-selected-message'),
      None,
      ( _("Configure..."), 'pylint-configure' ),
      ( _("Show PyLint Tool Documentation"), 'pylint-show-docs' ),
    ]

    # Create menu
    defnlist = guimgr.menus.GetMenuDefnList(kPopupDefn, self.fSingletons.fGuiMgr,
                                            self.__fCmdMap, is_popup=1, static=1)
    menu = guimgr.menus.CMenu(_("PyLint"), self.fSingletons.fGuiMgr,
                              defnlist, can_tearoff=0, is_popup=1)
    gViews[0] = self
    return menu

  def __CB_SelectionChanged(self, *args):
    pos = self.__fNotebook.get_current_page()
    catkey, label, tdir = gMessageCategories[pos]
    tree, sview = self.fTrees[catkey]
    rows = tree.GetSelectedContent()

  def __CB_ButtonPress(self, tree, event):
    app = wingapi.gApplication
    x, y, x_root, y_root, button, double = wgtk.GetButtonEventData(event)

    # Always select the row that the pointer is over
    tree.SelectAtClick(x, y)

    selected = tree.GetSelectedContent()
    if selected is not None and len(selected) != 0:
      filename = selected[0][3]
      line = int(selected[0][4])
      if button == wgtk.kLeftButton and not double:
        doc = app.OpenEditor(filename)
        doc.ScrollToLine(lineno=line-1, pos='center', select=1)

  def _ShowStatusMessage(self, msg):
    for tree, sview in self.fTrees.values():
      title_list = list(tree.column_titles)
      if msg:
          title_list[2] = _("Message: %s") % msg.replace('_', '__')
      else:
          title_list[2] = _("Message")
      tree.set_titles(title_list)

# Register this panel type:  Note that this needs to be at the
# very end of the module so that all the classes defined here
# are already available
if not _ignore_scripts:
  _CPylintPanelDefn(wingapi.gApplication.fSingletons)

