##############################################################################
#
# Copyright (c) 2003 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Class advice.

This module was adapted from 'protocols.advice', part of the Python
Enterprise Application Kit (PEAK).  Please notify the PEAK authors
(pje@telecommunity.com and tsarna@sarna.org) if bugs are found or
Zope-specific changes are required, so that the PEAK version of this module
can be kept in sync.

PEAK is a Python application framework that interoperates with (but does
not require) Zope 3 and Twisted.  It provides tools for manipulating UML
models, object-relational persistence, aspect-oriented programming, and more.
Visit the PEAK home page at http://peak.telecommunity.com for more information.
"""

from types import FunctionType


__all__ = [
    'determineMetaclass',
    'getFrameInfo',
    'isClassAdvisor',
    'minimalBases',
]

import sys


def getFrameInfo(frame):
    """Return (kind,module,locals,globals) for a frame

    'kind' is one of "exec", "module", "class", "function call", or "unknown".
    """

    f_locals = frame.f_locals
    f_globals = frame.f_globals

    sameNamespace = f_locals is f_globals
    hasModule = '__module__' in f_locals
    hasName = '__name__' in f_globals

    sameName = hasModule and hasName
    sameName = sameName and f_globals['__name__'] == f_locals['__module__']

    module = hasName and sys.modules.get(f_globals['__name__']) or None

    namespaceIsModule = module and module.__dict__ is f_globals

    if not namespaceIsModule:
        # some kind of funky exec
        kind = "exec"
    elif sameNamespace and not hasModule:
        kind = "module"
    elif sameName and not sameNamespace:
        kind = "class"
    elif not sameNamespace:
        kind = "function call"
    else:  # pragma: no cover
        # How can you have f_locals is f_globals, and have '__module__'
        # set?  # This is probably module-level code, but with a
        # '__module__' variable.
        kind = "unknown"
    return kind, module, f_locals, f_globals


def isClassAdvisor(ob):
    """True if 'ob' is a class advisor function"""
    return isinstance(ob, FunctionType) and hasattr(ob, 'previousMetaclass')


def determineMetaclass(bases, explicit_mc=None):
    """Determine metaclass from 1+ bases and optional explicit __metaclass__"""

    meta = [getattr(b, '__class__', type(b)) for b in bases]

    if explicit_mc is not None:
        # The explicit metaclass needs to be verified for compatibility
        # as well, and allowed to resolve the incompatible bases, if any
        meta.append(explicit_mc)

    if len(meta) == 1:
        # easy case
        return meta[0]

    candidates = minimalBases(meta)  # minimal set of metaclasses

    if len(candidates) > 1:
        # We could auto-combine, but for now we won't...
        raise TypeError("Incompatible metatypes", bases)

    # Just one, return it
    return candidates[0]


def minimalBases(classes):
    """Reduce a list of base classes to its ordered minimum equivalent"""
    candidates = []

    for m in classes:
        for n in classes:
            if issubclass(n, m) and m is not n:
                break
        else:
            # m has no subclasses in 'classes'
            if m in candidates:
                candidates.remove(m)    # ensure that we're later in the list
            candidates.append(m)

    return candidates
