GSSAPI="BASE"  # This ensures that a full module is generated by Cython

import locale  # for decoding error messages

from gssapi.raw.cython_types cimport *
from gssapi.raw.cython_converters cimport c_create_oid_set
from gssapi.raw.names cimport Name
from gssapi.raw.oids cimport OID

from gssapi.raw.types import MechType


cdef extern from "python_gssapi.h":
    OM_uint32 gss_display_status(OM_uint32 *minor_status,
                                 OM_uint32 status_value,
                                 int status_type,
                                 const gss_OID mech_type,
                                 OM_uint32 *message_context,
                                 gss_buffer_t status_string)

    OM_uint32 gss_indicate_mechs(OM_uint32 *minor_status,
                                 gss_OID_set *mech_set)

    OM_uint32 gss_inquire_names_for_mech(OM_uint32 *minor_status,
                                         const gss_OID mech_type,
                                         gss_OID_set *name_types)

    OM_uint32 gss_inquire_mechs_for_name(OM_uint32 *minor_status,
                                         const gss_name_t input_name,
                                         gss_OID_set *mech_types)


def indicate_mechs():
    cdef gss_OID_set mech_set

    cdef OM_uint32 maj_stat, min_stat

    maj_stat = gss_indicate_mechs(&min_stat, &mech_set)

    if maj_stat == GSS_S_COMPLETE:
        return c_create_oid_set(mech_set)
    else:
        raise GSSError(maj_stat, min_stat)


def inquire_names_for_mech(OID mech not None):
    cdef gss_OID_set name_types

    cdef OM_uint32 maj_stat, min_stat

    maj_stat = gss_inquire_names_for_mech(&min_stat, &mech.raw_oid,
                                          &name_types)

    if maj_stat == GSS_S_COMPLETE:
        return c_create_oid_set(name_types)
    else:
        raise GSSError(maj_stat, min_stat)


def inquire_mechs_for_name(Name name not None):
    cdef gss_OID_set mech_types

    cdef OM_uint32 maj_stat, min_stat

    maj_stat = gss_inquire_mechs_for_name(&min_stat, name.raw_name,
                                          &mech_types)

    if maj_stat == GSS_S_COMPLETE:
        return c_create_oid_set(mech_types)
    else:
        raise GSSError(maj_stat, min_stat)


def _display_status(unsigned int error_code, bint is_major_code,
                    OID mech=None, unsigned int message_context=0):
    cdef int status_type
    cdef gss_OID c_mech_type

    if is_major_code:
        status_type = GSS_C_GSS_CODE
    else:
        status_type = GSS_C_MECH_CODE

    if mech is None:
        c_mech_type = GSS_C_NO_OID
    else:
        c_mech_type = &mech.raw_oid

    cdef OM_uint32 maj_stat
    cdef OM_uint32 min_stat
    cdef OM_uint32 msg_ctx_out = message_context
    cdef gss_buffer_desc msg_buff

    maj_stat = gss_display_status(&min_stat, error_code, status_type,
                                  c_mech_type, &msg_ctx_out, &msg_buff)

    if maj_stat == GSS_S_COMPLETE:
        call_again = bool(msg_ctx_out)
        msg_out = (<char*>msg_buff.value)[:msg_buff.length]
        gss_release_buffer(&min_stat, &msg_buff)
        return (msg_out, msg_ctx_out, call_again)
    else:
        # This hides whatever error gss_display_status is complaining about,
        # but obviates infinite recursion into stack exhaustion.  The
        # exception raised here is handled by get_all_statuses(), which prints
        # the code.
        raise ValueError("gss_display_status call returned failure "
                         "(major {0}, minor {1}).".format(maj_stat, min_stat))


class GSSErrorRegistry(type):
    __registry = {}

    def __init__(cls, name, bases, attributes):
        calling_code = getattr(cls, 'CALLING_CODE', None)
        routine_code = getattr(cls, 'ROUTINE_CODE', None)
        supplementary_code = getattr(cls, 'SUPPLEMENTARY_CODE', None)

        # NB(directxman12): we ignore minor code since it's mech-specific

        if any([calling_code, routine_code, supplementary_code]):
            if calling_code not in cls.__registry:
                cls.__registry[calling_code] = {}

            call_reg = cls.__registry[calling_code]

            if routine_code not in call_reg:
                call_reg[routine_code] = {}

            routine_reg = call_reg[routine_code]

            routine_reg[supplementary_code] = cls

    @staticmethod
    def __get_registry(code, parent_reg):
        return parent_reg.get(code, parent_reg.get(None, {}))

    def __find_error(cls, maj_code):
        codes = cls._parse_major_code(maj_code)
        calling_code, routine_code, suppl_code = codes

        call_reg = cls.__get_registry(calling_code, cls.__registry)
        routine_reg = cls.__get_registry(routine_code, call_reg)

        return routine_reg.get(suppl_code, routine_reg.get(None, None))

    def __call__(cls, maj_code, min_code, *args, **kwargs):
        new_cls = cls.__find_error(maj_code) or cls

        return super(GSSErrorRegistry, new_cls).__call__(maj_code, min_code,
                                                         *args, **kwargs)


# NB(directxman12): this needs to be here (and not in another file)
#                   so that display_status can use it
class GSSError(Exception, metaclass=GSSErrorRegistry):

    MESSAGE = u"Major ({maj_stat}): {maj_str}, Minor ({min_stat}): {min_str}"

    @classmethod
    def _parse_major_code(cls, maj_code):
        # major status codes consist of
        # calling error | routine error | supplementary info
        # in non-overlapping bits

        calling_code = GSS_CALLING_ERROR(maj_code) or None
        routine_code = GSS_ROUTINE_ERROR(maj_code) or None
        supplementary_code = GSS_SUPPLEMENTARY_INFO(maj_code) or None

        return (calling_code, routine_code, supplementary_code)

    def __init__(self, maj_code, min_code, token=None):
        self.maj_code = maj_code
        self.min_code = min_code

        self.token = token

        split_codes = self._parse_major_code(maj_code)
        self.calling_code = split_codes[0]
        self.routine_code = split_codes[1]
        self.supplementary_code = split_codes[2]

        super(GSSError, self).__init__(self.gen_message())

    def get_all_statuses(self, code, is_maj):
        try:
            msg_encoding = locale.getlocale(locale.LC_MESSAGES)[1] or 'UTF-8'
        except AttributeError:  # Windows doesn't have LC_MESSAGES
            msg_encoding = 'UTF-8'

        res = []
        try:
            msg, ctx, cont = _display_status(code, is_maj)
            res.append(msg.decode(msg_encoding))
        except ValueError as e:
            res.append(u'{0}  Decoding code: {1}'.format(e, code))
            cont = False

        while cont:
            try:
                msg, ctx, cont = _display_status(code, is_maj,
                                                 message_context=ctx)
                res.append(msg.decode(msg_encoding))
            except ValueError as e:
                res.append(u'{0}  Decoding code: {1}'.format(e, code))
                cont = False

        return res

    def gen_message(self):
        maj_statuses = self.get_all_statuses(self.maj_code, True)
        min_statuses = self.get_all_statuses(self.min_code, False)

        maj_str = u' -- '.join(maj_statuses)
        min_str = u' -- '.join(min_statuses)

        return self.MESSAGE.format(maj_stat=self.maj_code,
                                   maj_str=maj_str,
                                   min_stat=self.min_code,
                                   min_str=min_str)
