# Copyright (C) 2016-present the asyncpg authors and contributors
# <see AUTHORS file>
#
# This module is part of asyncpg and is released under
# the Apache 2.0 License: http://www.apache.org/licenses/LICENSE-2.0


from collections.abc import Mapping as MappingABC

import asyncpg
from asyncpg import exceptions


cdef void* binary_codec_map[(MAXSUPPORTEDOID + 1) * 2]
cdef void* text_codec_map[(MAXSUPPORTEDOID + 1) * 2]
cdef dict EXTRA_CODECS = {}


@cython.final
cdef class Codec:

    def __cinit__(self, uint32_t oid):
        self.oid = oid
        self.type = CODEC_UNDEFINED

    cdef init(
        self,
        str name,
        str schema,
        str kind,
        CodecType type,
        ServerDataFormat format,
        ClientExchangeFormat xformat,
        encode_func c_encoder,
        decode_func c_decoder,
        Codec base_codec,
        object py_encoder,
        object py_decoder,
        Codec element_codec,
        tuple element_type_oids,
        object element_names,
        list element_codecs,
        Py_UCS4 element_delimiter,
    ):

        self.name = name
        self.schema = schema
        self.kind = kind
        self.type = type
        self.format = format
        self.xformat = xformat
        self.c_encoder = c_encoder
        self.c_decoder = c_decoder
        self.base_codec = base_codec
        self.py_encoder = py_encoder
        self.py_decoder = py_decoder
        self.element_codec = element_codec
        self.element_type_oids = element_type_oids
        self.element_codecs = element_codecs
        self.element_delimiter = element_delimiter
        self.element_names = element_names

        if base_codec is not None:
            if c_encoder != NULL or c_decoder != NULL:
                raise exceptions.InternalClientError(
                    'base_codec is mutually exclusive with c_encoder/c_decoder'
                )

        if element_names is not None:
            self.record_desc = record.ApgRecordDesc_New(
                element_names, tuple(element_names))
        else:
            self.record_desc = None

        if type == CODEC_C:
            self.encoder = <codec_encode_func>&self.encode_scalar
            self.decoder = <codec_decode_func>&self.decode_scalar
        elif type == CODEC_ARRAY:
            if format == PG_FORMAT_BINARY:
                self.encoder = <codec_encode_func>&self.encode_array
                self.decoder = <codec_decode_func>&self.decode_array
            else:
                self.encoder = <codec_encode_func>&self.encode_array_text
                self.decoder = <codec_decode_func>&self.decode_array_text
        elif type == CODEC_RANGE:
            if format != PG_FORMAT_BINARY:
                raise exceptions.UnsupportedClientFeatureError(
                    'cannot decode type "{}"."{}": text encoding of '
                    'range types is not supported'.format(schema, name))
            self.encoder = <codec_encode_func>&self.encode_range
            self.decoder = <codec_decode_func>&self.decode_range
        elif type == CODEC_MULTIRANGE:
            if format != PG_FORMAT_BINARY:
                raise exceptions.UnsupportedClientFeatureError(
                    'cannot decode type "{}"."{}": text encoding of '
                    'range types is not supported'.format(schema, name))
            self.encoder = <codec_encode_func>&self.encode_multirange
            self.decoder = <codec_decode_func>&self.decode_multirange
        elif type == CODEC_COMPOSITE:
            if format != PG_FORMAT_BINARY:
                raise exceptions.UnsupportedClientFeatureError(
                    'cannot decode type "{}"."{}": text encoding of '
                    'composite types is not supported'.format(schema, name))
            self.encoder = <codec_encode_func>&self.encode_composite
            self.decoder = <codec_decode_func>&self.decode_composite
        elif type == CODEC_PY:
            self.encoder = <codec_encode_func>&self.encode_in_python
            self.decoder = <codec_decode_func>&self.decode_in_python
        else:
            raise exceptions.InternalClientError(
                'unexpected codec type: {}'.format(type))

    cdef Codec copy(self):
        cdef Codec codec

        codec = Codec(self.oid)
        codec.init(self.name, self.schema, self.kind,
                   self.type, self.format, self.xformat,
                   self.c_encoder, self.c_decoder, self.base_codec,
                   self.py_encoder, self.py_decoder,
                   self.element_codec,
                   self.element_type_oids, self.element_names,
                   self.element_codecs, self.element_delimiter)

        return codec

    cdef encode_scalar(self, ConnectionSettings settings, WriteBuffer buf,
                       object obj):
        self.c_encoder(settings, buf, obj)

    cdef encode_array(self, ConnectionSettings settings, WriteBuffer buf,
                      object obj):
        array_encode(settings, buf, obj, self.element_codec.oid,
                     codec_encode_func_ex,
                     <void*>(<cpython.PyObject>self.element_codec))

    cdef encode_array_text(self, ConnectionSettings settings, WriteBuffer buf,
                           object obj):
        return textarray_encode(settings, buf, obj,
                                codec_encode_func_ex,
                                <void*>(<cpython.PyObject>self.element_codec),
                                self.element_delimiter)

    cdef encode_range(self, ConnectionSettings settings, WriteBuffer buf,
                      object obj):
        range_encode(settings, buf, obj, self.element_codec.oid,
                     codec_encode_func_ex,
                     <void*>(<cpython.PyObject>self.element_codec))

    cdef encode_multirange(self, ConnectionSettings settings, WriteBuffer buf,
                           object obj):
        multirange_encode(settings, buf, obj, self.element_codec.oid,
                          codec_encode_func_ex,
                          <void*>(<cpython.PyObject>self.element_codec))

    cdef encode_composite(self, ConnectionSettings settings, WriteBuffer buf,
                          object obj):
        cdef:
            WriteBuffer elem_data
            int i
            list elem_codecs = self.element_codecs
            ssize_t count
            ssize_t composite_size
            tuple rec

        if isinstance(obj, MappingABC):
            # Input is dict-like, form a tuple
            composite_size = len(self.element_type_oids)
            rec = cpython.PyTuple_New(composite_size)

            for i in range(composite_size):
                cpython.Py_INCREF(None)
                cpython.PyTuple_SET_ITEM(rec, i, None)

            for field in obj:
                try:
                    i = self.element_names[field]
                except KeyError:
                    raise ValueError(
                        '{!r} is not a valid element of composite '
                        'type {}'.format(field, self.name)) from None

                item = obj[field]
                cpython.Py_INCREF(item)
                cpython.PyTuple_SET_ITEM(rec, i, item)

            obj = rec

        count = len(obj)
        if count > _MAXINT32:
            raise ValueError('too many elements in composite type record')

        elem_data = WriteBuffer.new()
        i = 0
        for item in obj:
            elem_data.write_int32(<int32_t>self.element_type_oids[i])
            if item is None:
                elem_data.write_int32(-1)
            else:
                (<Codec>elem_codecs[i]).encode(settings, elem_data, item)
            i += 1

        record_encode_frame(settings, buf, elem_data, <int32_t>count)

    cdef encode_in_python(self, ConnectionSettings settings, WriteBuffer buf,
                          object obj):
        data = self.py_encoder(obj)
        if self.xformat == PG_XFORMAT_OBJECT:
            if self.format == PG_FORMAT_BINARY:
                pgproto.bytea_encode(settings, buf, data)
            elif self.format == PG_FORMAT_TEXT:
                pgproto.text_encode(settings, buf, data)
            else:
                raise exceptions.InternalClientError(
                    'unexpected data format: {}'.format(self.format))
        elif self.xformat == PG_XFORMAT_TUPLE:
            if self.base_codec is not None:
                self.base_codec.encode(settings, buf, data)
            else:
                self.c_encoder(settings, buf, data)
        else:
            raise exceptions.InternalClientError(
                'unexpected exchange format: {}'.format(self.xformat))

    cdef encode(self, ConnectionSettings settings, WriteBuffer buf,
                object obj):
        return self.encoder(self, settings, buf, obj)

    cdef decode_scalar(self, ConnectionSettings settings, FRBuffer *buf):
        return self.c_decoder(settings, buf)

    cdef decode_array(self, ConnectionSettings settings, FRBuffer *buf):
        return array_decode(settings, buf, codec_decode_func_ex,
                            <void*>(<cpython.PyObject>self.element_codec))

    cdef decode_array_text(self, ConnectionSettings settings,
                           FRBuffer *buf):
        return textarray_decode(settings, buf, codec_decode_func_ex,
                                <void*>(<cpython.PyObject>self.element_codec),
                                self.element_delimiter)

    cdef decode_range(self, ConnectionSettings settings, FRBuffer *buf):
        return range_decode(settings, buf, codec_decode_func_ex,
                            <void*>(<cpython.PyObject>self.element_codec))

    cdef decode_multirange(self, ConnectionSettings settings, FRBuffer *buf):
        return multirange_decode(settings, buf, codec_decode_func_ex,
                                 <void*>(<cpython.PyObject>self.element_codec))

    cdef decode_composite(self, ConnectionSettings settings,
                          FRBuffer *buf):
        cdef:
            object result
            ssize_t elem_count
            ssize_t i
            int32_t elem_len
            uint32_t elem_typ
            uint32_t received_elem_typ
            Codec elem_codec
            FRBuffer elem_buf

        elem_count = <ssize_t><uint32_t>hton.unpack_int32(frb_read(buf, 4))
        if elem_count != len(self.element_type_oids):
            raise exceptions.OutdatedSchemaCacheError(
                'unexpected number of attributes of composite type: '
                '{}, expected {}'
                    .format(
                        elem_count,
                        len(self.element_type_oids),
                    ),
                schema=self.schema,
                data_type=self.name,
            )
        result = record.ApgRecord_New(asyncpg.Record, self.record_desc, elem_count)
        for i in range(elem_count):
            elem_typ = self.element_type_oids[i]
            received_elem_typ = <uint32_t>hton.unpack_int32(frb_read(buf, 4))

            if received_elem_typ != elem_typ:
                raise exceptions.OutdatedSchemaCacheError(
                    'unexpected data type of composite type attribute {}: '
                    '{!r}, expected {!r}'
                        .format(
                            i,
                            BUILTIN_TYPE_OID_MAP.get(
                                received_elem_typ, received_elem_typ),
                            BUILTIN_TYPE_OID_MAP.get(
                                elem_typ, elem_typ)
                        ),
                    schema=self.schema,
                    data_type=self.name,
                    position=i,
                )

            elem_len = hton.unpack_int32(frb_read(buf, 4))
            if elem_len == -1:
                elem = None
            else:
                elem_codec = self.element_codecs[i]
                elem = elem_codec.decode(
                    settings, frb_slice_from(&elem_buf, buf, elem_len))

            cpython.Py_INCREF(elem)
            record.ApgRecord_SET_ITEM(result, i, elem)

        return result

    cdef decode_in_python(self, ConnectionSettings settings,
                          FRBuffer *buf):
        if self.xformat == PG_XFORMAT_OBJECT:
            if self.format == PG_FORMAT_BINARY:
                data = pgproto.bytea_decode(settings, buf)
            elif self.format == PG_FORMAT_TEXT:
                data = pgproto.text_decode(settings, buf)
            else:
                raise exceptions.InternalClientError(
                    'unexpected data format: {}'.format(self.format))
        elif self.xformat == PG_XFORMAT_TUPLE:
            if self.base_codec is not None:
                data = self.base_codec.decode(settings, buf)
            else:
                data = self.c_decoder(settings, buf)
        else:
            raise exceptions.InternalClientError(
                'unexpected exchange format: {}'.format(self.xformat))

        return self.py_decoder(data)

    cdef inline decode(self, ConnectionSettings settings, FRBuffer *buf):
        return self.decoder(self, settings, buf)

    cdef inline has_encoder(self):
        cdef Codec elem_codec

        if self.c_encoder is not NULL or self.py_encoder is not None:
            return True

        elif (
            self.type == CODEC_ARRAY
            or self.type == CODEC_RANGE
            or self.type == CODEC_MULTIRANGE
        ):
            return self.element_codec.has_encoder()

        elif self.type == CODEC_COMPOSITE:
            for elem_codec in self.element_codecs:
                if not elem_codec.has_encoder():
                    return False
            return True

        else:
            return False

    cdef has_decoder(self):
        cdef Codec elem_codec

        if self.c_decoder is not NULL or self.py_decoder is not None:
            return True

        elif (
            self.type == CODEC_ARRAY
            or self.type == CODEC_RANGE
            or self.type == CODEC_MULTIRANGE
        ):
            return self.element_codec.has_decoder()

        elif self.type == CODEC_COMPOSITE:
            for elem_codec in self.element_codecs:
                if not elem_codec.has_decoder():
                    return False
            return True

        else:
            return False

    cdef is_binary(self):
        return self.format == PG_FORMAT_BINARY

    def __repr__(self):
        return '<Codec oid={} elem_oid={} core={}>'.format(
            self.oid,
            'NA' if self.element_codec is None else self.element_codec.oid,
            has_core_codec(self.oid))

    @staticmethod
    cdef Codec new_array_codec(uint32_t oid,
                               str name,
                               str schema,
                               Codec element_codec,
                               Py_UCS4 element_delimiter):
        cdef Codec codec
        codec = Codec(oid)
        codec.init(name, schema, 'array', CODEC_ARRAY, element_codec.format,
                   PG_XFORMAT_OBJECT, NULL, NULL, None, None, None,
                   element_codec, None, None, None, element_delimiter)
        return codec

    @staticmethod
    cdef Codec new_range_codec(uint32_t oid,
                               str name,
                               str schema,
                               Codec element_codec):
        cdef Codec codec
        codec = Codec(oid)
        codec.init(name, schema, 'range', CODEC_RANGE, element_codec.format,
                   PG_XFORMAT_OBJECT, NULL, NULL, None, None, None,
                   element_codec, None, None, None, 0)
        return codec

    @staticmethod
    cdef Codec new_multirange_codec(uint32_t oid,
                                    str name,
                                    str schema,
                                    Codec element_codec):
        cdef Codec codec
        codec = Codec(oid)
        codec.init(name, schema, 'multirange', CODEC_MULTIRANGE,
                   element_codec.format, PG_XFORMAT_OBJECT, NULL, NULL, None,
                   None, None, element_codec, None, None, None, 0)
        return codec

    @staticmethod
    cdef Codec new_composite_codec(uint32_t oid,
                                   str name,
                                   str schema,
                                   ServerDataFormat format,
                                   list element_codecs,
                                   tuple element_type_oids,
                                   object element_names):
        cdef Codec codec
        codec = Codec(oid)
        codec.init(name, schema, 'composite', CODEC_COMPOSITE,
                   format, PG_XFORMAT_OBJECT, NULL, NULL, None, None, None,
                   None, element_type_oids, element_names, element_codecs, 0)
        return codec

    @staticmethod
    cdef Codec new_python_codec(uint32_t oid,
                                str name,
                                str schema,
                                str kind,
                                object encoder,
                                object decoder,
                                encode_func c_encoder,
                                decode_func c_decoder,
                                Codec base_codec,
                                ServerDataFormat format,
                                ClientExchangeFormat xformat):
        cdef Codec codec
        codec = Codec(oid)
        codec.init(name, schema, kind, CODEC_PY, format, xformat,
                   c_encoder, c_decoder, base_codec, encoder, decoder,
                   None, None, None, None, 0)
        return codec


# Encode callback for arrays
cdef codec_encode_func_ex(ConnectionSettings settings, WriteBuffer buf,
                          object obj, const void *arg):
    return (<Codec>arg).encode(settings, buf, obj)


# Decode callback for arrays
cdef codec_decode_func_ex(ConnectionSettings settings, FRBuffer *buf,
                          const void *arg):
    return (<Codec>arg).decode(settings, buf)


cdef uint32_t pylong_as_oid(val) except? 0xFFFFFFFFl:
    cdef:
        int64_t oid = 0
        bint overflow = False

    try:
        oid = cpython.PyLong_AsLongLong(val)
    except OverflowError:
        overflow = True

    if overflow or (oid < 0 or oid > UINT32_MAX):
        raise OverflowError('OID value too large: {!r}'.format(val))

    return <uint32_t>val


cdef class DataCodecConfig:
    def __init__(self):
        # Codec instance cache for derived types:
        # composites, arrays, ranges, domains and their combinations.
        self._derived_type_codecs = {}
        # Codec instances set up by the user for the connection.
        self._custom_type_codecs = {}

    def add_types(self, types):
        cdef:
            Codec elem_codec
            list comp_elem_codecs
            ServerDataFormat format
            ServerDataFormat elem_format
            bint has_text_elements
            Py_UCS4 elem_delim

        for ti in types:
            oid = ti['oid']

            if self.get_codec(oid, PG_FORMAT_ANY) is not None:
                continue

            name = ti['name']
            schema = ti['ns']
            array_element_oid = ti['elemtype']
            range_subtype_oid = ti['range_subtype']
            if ti['attrtypoids']:
                comp_type_attrs = tuple(ti['attrtypoids'])
            else:
                comp_type_attrs = None
            base_type = ti['basetype']

            if array_element_oid:
                # Array type (note, there is no separate 'kind' for arrays)

                # Canonicalize type name to "elemtype[]"
                if name.startswith('_'):
                    name = name[1:]
                name = '{}[]'.format(name)

                elem_codec = self.get_codec(array_element_oid, PG_FORMAT_ANY)
                if elem_codec is None:
                    elem_codec = self.declare_fallback_codec(
                        array_element_oid, ti['elemtype_name'], schema)

                elem_delim = <Py_UCS4>ti['elemdelim'][0]

                self._derived_type_codecs[oid, elem_codec.format] = \
                    Codec.new_array_codec(
                        oid, name, schema, elem_codec, elem_delim)

            elif ti['kind'] == b'c':
                # Composite type

                if not comp_type_attrs:
                    raise exceptions.InternalClientError(
                        f'type record missing field types for composite {oid}')

                comp_elem_codecs = []
                has_text_elements = False

                for typoid in comp_type_attrs:
                    elem_codec = self.get_codec(typoid, PG_FORMAT_ANY)
                    if elem_codec is None:
                        raise exceptions.InternalClientError(
                            f'no codec for composite attribute type {typoid}')
                    if elem_codec.format is PG_FORMAT_TEXT:
                        has_text_elements = True
                    comp_elem_codecs.append(elem_codec)

                element_names = collections.OrderedDict()
                for i, attrname in enumerate(ti['attrnames']):
                    element_names[attrname] = i

                # If at least one element is text-encoded, we must
                # encode the whole composite as text.
                if has_text_elements:
                    elem_format = PG_FORMAT_TEXT
                else:
                    elem_format = PG_FORMAT_BINARY

                self._derived_type_codecs[oid, elem_format] = \
                    Codec.new_composite_codec(
                        oid, name, schema, elem_format, comp_elem_codecs,
                        comp_type_attrs, element_names)

            elif ti['kind'] == b'd':
                # Domain type

                if not base_type:
                    raise exceptions.InternalClientError(
                        f'type record missing base type for domain {oid}')

                elem_codec = self.get_codec(base_type, PG_FORMAT_ANY)
                if elem_codec is None:
                    elem_codec = self.declare_fallback_codec(
                        base_type, ti['basetype_name'], schema)

                self._derived_type_codecs[oid, elem_codec.format] = elem_codec

            elif ti['kind'] == b'r':
                # Range type

                if not range_subtype_oid:
                    raise exceptions.InternalClientError(
                        f'type record missing base type for range {oid}')

                elem_codec = self.get_codec(range_subtype_oid, PG_FORMAT_ANY)
                if elem_codec is None:
                    elem_codec = self.declare_fallback_codec(
                        range_subtype_oid, ti['range_subtype_name'], schema)

                self._derived_type_codecs[oid, elem_codec.format] = \
                    Codec.new_range_codec(oid, name, schema, elem_codec)

            elif ti['kind'] == b'm':
                # Multirange type

                if not range_subtype_oid:
                    raise exceptions.InternalClientError(
                        f'type record missing base type for multirange {oid}')

                elem_codec = self.get_codec(range_subtype_oid, PG_FORMAT_ANY)
                if elem_codec is None:
                    elem_codec = self.declare_fallback_codec(
                        range_subtype_oid, ti['range_subtype_name'], schema)

                self._derived_type_codecs[oid, elem_codec.format] = \
                    Codec.new_multirange_codec(oid, name, schema, elem_codec)

            elif ti['kind'] == b'e':
                # Enum types are essentially text
                self._set_builtin_type_codec(oid, name, schema, 'scalar',
                                             TEXTOID, PG_FORMAT_ANY)
            else:
                self.declare_fallback_codec(oid, name, schema)

    def add_python_codec(self, typeoid, typename, typeschema, typekind,
                         typeinfos, encoder, decoder, format, xformat):
        cdef:
            Codec core_codec = None
            encode_func c_encoder = NULL
            decode_func c_decoder = NULL
            Codec base_codec = None
            uint32_t oid = pylong_as_oid(typeoid)
            bint codec_set = False

        # Clear all previous overrides (this also clears type cache).
        self.remove_python_codec(typeoid, typename, typeschema)

        if typeinfos:
            self.add_types(typeinfos)

        if format == PG_FORMAT_ANY:
            formats = (PG_FORMAT_TEXT, PG_FORMAT_BINARY)
        else:
            formats = (format,)

        for fmt in formats:
            if xformat == PG_XFORMAT_TUPLE:
                if typekind == "scalar":
                    core_codec = get_core_codec(oid, fmt, xformat)
                    if core_codec is None:
                        continue
                    c_encoder = core_codec.c_encoder
                    c_decoder = core_codec.c_decoder
                elif typekind == "composite":
                    base_codec = self.get_codec(oid, fmt)
                    if base_codec is None:
                        continue

            self._custom_type_codecs[typeoid, fmt] = \
                Codec.new_python_codec(oid, typename, typeschema, typekind,
                                       encoder, decoder, c_encoder, c_decoder,
                                       base_codec, fmt, xformat)
            codec_set = True

        if not codec_set:
            raise exceptions.InterfaceError(
                "{} type does not support the 'tuple' exchange format".format(
                    typename))

    def remove_python_codec(self, typeoid, typename, typeschema):
        for fmt in (PG_FORMAT_BINARY, PG_FORMAT_TEXT):
            self._custom_type_codecs.pop((typeoid, fmt), None)
        self.clear_type_cache()

    def _set_builtin_type_codec(self, typeoid, typename, typeschema, typekind,
                                alias_to, format=PG_FORMAT_ANY):
        cdef:
            Codec codec
            Codec target_codec
            uint32_t oid = pylong_as_oid(typeoid)
            uint32_t alias_oid = 0
            bint codec_set = False

        if format == PG_FORMAT_ANY:
            formats = (PG_FORMAT_BINARY, PG_FORMAT_TEXT)
        else:
            formats = (format,)

        if isinstance(alias_to, int):
            alias_oid = pylong_as_oid(alias_to)
        else:
            alias_oid = BUILTIN_TYPE_NAME_MAP.get(alias_to, 0)

        for format in formats:
            if alias_oid != 0:
                target_codec = self.get_codec(alias_oid, format)
            else:
                target_codec = get_extra_codec(alias_to, format)

            if target_codec is None:
                continue

            codec = target_codec.copy()
            codec.oid = typeoid
            codec.name = typename
            codec.schema = typeschema
            codec.kind = typekind

            self._custom_type_codecs[typeoid, format] = codec
            codec_set = True

        if not codec_set:
            if format == PG_FORMAT_BINARY:
                codec_str = 'binary'
            elif format == PG_FORMAT_TEXT:
                codec_str = 'text'
            else:
                codec_str = 'text or binary'

            raise exceptions.InterfaceError(
                f'cannot alias {typename} to {alias_to}: '
                f'there is no {codec_str} codec for {alias_to}')

    def set_builtin_type_codec(self, typeoid, typename, typeschema, typekind,
                               alias_to, format=PG_FORMAT_ANY):
        self._set_builtin_type_codec(typeoid, typename, typeschema, typekind,
                                     alias_to, format)
        self.clear_type_cache()

    def clear_type_cache(self):
        self._derived_type_codecs.clear()

    def declare_fallback_codec(self, uint32_t oid, str name, str schema):
        cdef Codec codec

        if oid <= MAXBUILTINOID:
            # This is a BKI type, for which asyncpg has no
            # defined codec.  This should only happen for newly
            # added builtin types, for which this version of
            # asyncpg is lacking support.
            #
            raise exceptions.UnsupportedClientFeatureError(
                f'unhandled standard data type {name!r} (OID {oid})')
        else:
            # This is a non-BKI type, and as such, has no
            # stable OID, so no possibility of a builtin codec.
            # In this case, fallback to text format.  Applications
            # can avoid this by specifying a codec for this type
            # using Connection.set_type_codec().
            #
            self._set_builtin_type_codec(oid, name, schema, 'scalar',
                                         TEXTOID, PG_FORMAT_TEXT)

            codec = self.get_codec(oid, PG_FORMAT_TEXT)

        return codec

    cdef inline Codec get_codec(self, uint32_t oid, ServerDataFormat format,
                                bint ignore_custom_codec=False):
        cdef Codec codec

        if format == PG_FORMAT_ANY:
            codec = self.get_codec(
                oid, PG_FORMAT_BINARY, ignore_custom_codec)
            if codec is None:
                codec = self.get_codec(
                    oid, PG_FORMAT_TEXT, ignore_custom_codec)
            return codec
        else:
            if not ignore_custom_codec:
                codec = self.get_custom_codec(oid, PG_FORMAT_ANY)
                if codec is not None:
                    if codec.format != format:
                        # The codec for this OID has been overridden by
                        # set_{builtin}_type_codec with a different format.
                        # We must respect that and not return a core codec.
                        return None
                    else:
                        return codec

            codec = get_core_codec(oid, format)
            if codec is not None:
                return codec
            else:
                try:
                    return self._derived_type_codecs[oid, format]
                except KeyError:
                    return None

    cdef inline Codec get_custom_codec(
        self,
        uint32_t oid,
        ServerDataFormat format
    ):
        cdef Codec codec

        if format == PG_FORMAT_ANY:
            codec = self.get_custom_codec(oid, PG_FORMAT_BINARY)
            if codec is None:
                codec = self.get_custom_codec(oid, PG_FORMAT_TEXT)
        else:
            codec = self._custom_type_codecs.get((oid, format))

        return codec


cdef inline Codec get_core_codec(
        uint32_t oid, ServerDataFormat format,
        ClientExchangeFormat xformat=PG_XFORMAT_OBJECT):
    cdef:
        void *ptr = NULL

    if oid > MAXSUPPORTEDOID:
        return None
    if format == PG_FORMAT_BINARY:
        ptr = binary_codec_map[oid * xformat]
    elif format == PG_FORMAT_TEXT:
        ptr = text_codec_map[oid * xformat]

    if ptr is NULL:
        return None
    else:
        return <Codec>ptr


cdef inline Codec get_any_core_codec(
        uint32_t oid, ServerDataFormat format,
        ClientExchangeFormat xformat=PG_XFORMAT_OBJECT):
    """A version of get_core_codec that accepts PG_FORMAT_ANY."""
    cdef:
        Codec codec

    if format == PG_FORMAT_ANY:
        codec = get_core_codec(oid, PG_FORMAT_BINARY, xformat)
        if codec is None:
            codec = get_core_codec(oid, PG_FORMAT_TEXT, xformat)
    else:
        codec = get_core_codec(oid, format, xformat)

    return codec


cdef inline int has_core_codec(uint32_t oid):
    return binary_codec_map[oid] != NULL or text_codec_map[oid] != NULL


cdef register_core_codec(uint32_t oid,
                         encode_func encode,
                         decode_func decode,
                         ServerDataFormat format,
                         ClientExchangeFormat xformat=PG_XFORMAT_OBJECT):

    if oid > MAXSUPPORTEDOID:
        raise exceptions.InternalClientError(
            'cannot register core codec for OID {}: it is greater '
            'than MAXSUPPORTEDOID ({})'.format(oid, MAXSUPPORTEDOID))

    cdef:
        Codec codec
        str name
        str kind

    name = BUILTIN_TYPE_OID_MAP[oid]
    kind = 'array' if oid in ARRAY_TYPES else 'scalar'

    codec = Codec(oid)
    codec.init(name, 'pg_catalog', kind, CODEC_C, format, xformat,
               encode, decode, None, None, None, None, None, None, None, 0)
    cpython.Py_INCREF(codec)  # immortalize

    if format == PG_FORMAT_BINARY:
        binary_codec_map[oid * xformat] = <void*>codec
    elif format == PG_FORMAT_TEXT:
        text_codec_map[oid * xformat] = <void*>codec
    else:
        raise exceptions.InternalClientError(
            'invalid data format: {}'.format(format))


cdef register_extra_codec(str name,
                          encode_func encode,
                          decode_func decode,
                          ServerDataFormat format):
    cdef:
        Codec codec
        str kind

    kind = 'scalar'

    codec = Codec(INVALIDOID)
    codec.init(name, None, kind, CODEC_C, format, PG_XFORMAT_OBJECT,
               encode, decode, None, None, None, None, None, None, None, 0)
    EXTRA_CODECS[name, format] = codec


cdef inline Codec get_extra_codec(str name, ServerDataFormat format):
    return EXTRA_CODECS.get((name, format))
