__revision__ = '$Revision$'

__all__ = ['Bundles',
           'Comments',
           'Duplicates',
           'InfoXML',
           'MergeFiles',
           'Pkgmgr',
           'RequiredAttrs',
           'Validate',
           'Genshi',
           'Deltas']

import logging
import os
import sys
from copy import copy
import textwrap
import lxml.etree
import Bcfg2.Logger
import fcntl
import termios
import struct

def _ioctl_GWINSZ(fd):
    try:
        cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234'))
    except:
        return None
    return cr

def get_termsize():
    """ get a tuple of (width, height) giving the size of the terminal """
    if not sys.stdout.isatty():
        return None
    cr = _ioctl_GWINSZ(0) or _ioctl_GWINSZ(1) or _ioctl_GWINSZ(2)
    if not cr:
        try:
            fd = os.open(os.ctermid(), os.O_RDONLY)
            cr = _ioctl_GWINSZ(fd)
            os.close(fd)
        except:
            pass
    if not cr:
        try:
            cr = (os.environ['LINES'], os.environ['COLUMNS'])
        except KeyError:
            return None
    return int(cr[1]), int(cr[0])

class Plugin (object):
    """ base class for ServerlessPlugin and ServerPlugin """

    def __init__(self, config, errorhandler=None, files=None):
        self.files = files
        self.config = config
        self.logger = logging.getLogger('bcfg2-lint')
        if errorhandler is None:
            self.errorHandler = ErrorHandler()
        else:
            self.errorHandler = errorhandler

    def Run(self):
        """ run the plugin.  must be overloaded by child classes """
        pass

    def HandlesFile(self, fname):
        """ returns true if the given file should be handled by the
        plugin according to the files list, false otherwise """
        return (self.files is None or
                fname in self.files or
                os.path.join(self.config['repo'], fname) in self.files or
                os.path.abspath(fname) in self.files or
                os.path.abspath(os.path.join(self.config['repo'],
                                             fname)) in self.files)

    def LintError(self, err, msg):
        self.errorHandler.dispatch(err, msg)
    
    def RenderXML(self, element):
        """render an XML element for error output -- line number
        prefixed, no children"""
        xml = None
        if len(element) or element.text:
            el = copy(element)
            if el.text:
                el.text = '...'
            [el.remove(c) for c in el.iterchildren()]
            xml = lxml.etree.tostring(el).strip()
        else:
            xml = lxml.etree.tostring(element).strip()
        return "   line %s: %s" % (element.sourceline, xml)


class ErrorHandler (object):
    # how to handle different errors by default
    _errors = {"no-infoxml":"warning",
               "paranoid-false":"warning",
               "bundle-not-found":"error",
               "inconsistent-bundle-name":"warning",
               "group-tag-not-allowed":"error",
               "unexpanded-keywords":"warning",
               "keywords-not-found":"warning",
               "comments-not-found":"warning",
               "broken-xinclude-chain":"warning",
               "duplicate-client":"error",
               "duplicate-group":"error",
               "duplicate-package":"error",
               "multiple-default-groups":"error",
               "required-infoxml-attrs-missing":"error",
               "unknown-entry-type":"error",
               "required-attrs-missing":"error",
               "extra-attrs":"warning",
               "schema-failed-to-parse":"warning",
               "properties-schema-not-found":"warning",
               "xml-failed-to-parse":"error",
               "xml-failed-to-read":"error",
               "xml-failed-to-verify":"error",
               "merge-cfg":"warning",
               "merge-probes":"warning",
               "input-output-error":"error",
               "genshi-syntax-error":"error",
               "pattern-fails-to-initialize":"error",
               "cat-file-used":"warning",
               "diff-file-used":"warning"}

    def __init__(self, config=None):
        self.errors = 0
        self.warnings = 0

        self.logger = logging.getLogger('bcfg2-lint')

        termsize = get_termsize()
        if termsize is not None:
            twrap = textwrap.TextWrapper(initial_indent="  ",
                                        subsequent_indent="  ",
                                        width=termsize[0])
            self._wrapper = twrap.wrap
        else:
            self._wrapper = lambda s: [s]

        self._handlers = {}
        if config is not None:
            for err, action in config.items():
                if "warn" in action:
                    self._handlers[err] = self.warn
                elif "err" in action:
                    self._handlers[err] = self.error
                else:
                    self._handlers[err] = self.debug

        for err, action in self._errors.items():
            if err not in self._handlers:
                if "warn" in action:
                    self._handlers[err] = self.warn
                elif "err" in action:
                    self._handlers[err] = self.error
                else:
                    self._handlers[err] = self.debug

    def dispatch(self, err, msg):
        if err in self._handlers:
            self._handlers[err](msg)
            self.logger.debug("    (%s)" % err)
        else:
            # assume that it's an error, but complain
            self.error(msg)
            self.logger.warning("Unknown error %s" % err)

    def error(self, msg):
        """ log an error condition """
        self.errors += 1
        self._log(msg, self.logger.error, prefix="ERROR: ")

    def warn(self, msg):
        """ log a warning condition """
        self.warnings += 1
        self._log(msg, self.logger.warning, prefix="WARNING: ")

    def debug(self, msg):
        """ log a silent/debug condition """
        self._log(msg, self.logger.debug)

    def _log(self, msg, logfunc, prefix=""):
        # a message may itself consist of multiple lines.  wrap() will
        # elide them all into a single paragraph, which we don't want.
        # so we split the message into its paragraphs and wrap each
        # paragraph individually.  this means, unfortunately, that we
        # lose textwrap's built-in initial indent functionality,
        # because we want to only treat the very first line of the
        # first paragraph specially.  so we do some silliness.
        rawlines = msg.splitlines()
        firstline = True
        for rawline in rawlines:
            lines = self._wrapper(rawline)
            for line in lines:
                if firstline:
                    logfunc(prefix + line.lstrip())
                    firstline = False
                else:
                    logfunc(line)


class ServerlessPlugin (Plugin):
    """ base class for plugins that are run before the server starts
    up (i.e., plugins that check things that may prevent the server
    from starting up) """
    pass


class ServerPlugin (Plugin):
    """ base class for plugins that check things that require the
    running Bcfg2 server """
    def __init__(self, lintCore, config, **kwargs):
        Plugin.__init__(self, config, **kwargs)
        self.core = lintCore
        self.logger = self.core.logger
        self.metadata = self.core.metadata
