# Source code for taurus.core.util.codecs

    #!/usr/bin/env python

    # ###########################################################################
    #
    # This file is part of Taurus
    #
    # http://taurus-scada.org
    #
    # Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain
    #
    # Taurus is free software: you can redistribute it and/or modify
    # it under the terms of the GNU Lesser General Public License as published by
    # the Free Software Foundation, either version 3 of the License, or
    # (at your option) any later version.
    #
    # Taurus is distributed in the hope that it will be useful,
    # but WITHOUT ANY WARRANTY; without even the implied warranty of
    # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    # GNU Lesser General Public License for more details.
    #
    # You should have received a copy of the GNU Lesser General Public License
    # along with Taurus.  If not, see <http://www.gnu.org/licenses/>.
    #
    # ###########################################################################
    """
    This module contains a list of codecs for the DEV_ENCODED attribute type.
    All codecs are based on the pair *format, data*. The format is a string
    containing the codec signature and data is a sequence of bytes (string)
    containing the encoded data.

    This module contains a list of codecs capable of decoding several codings like
    bz2, zip and json.

    The :class:`CodecFactory` class allows you to get a codec object for a given
    format and also to register new codecs.
    The :class:`CodecPipeline` is a special codec that is able to code/decode a
    sequence of codecs. This way you can have codecs 'inside' codecs.

    Example::

        >>> from taurus.core.util.codecs import CodecFactory
        >>> cf = CodecFactory()
        >>> json_codec = cf.getCodec('json')
        >>> bz2_json_codec = cf.getCodec('bz2_json')
        >>> data = range(100000)
        >>> f1, enc_d1 = json_codec.encode(('', data))
        >>> f2, enc_d2 = bz2_json_codec.encode(('', data))
        >>> print(len(enc_d1), len(enc_d2))
        688890 123511
        >>>
        >>> f1, dec_d1 = json_codec.decode((f1, enc_d1))
        >>> f2, dec_d2 = bz2_json_codec.decode((f2, enc_d2))

    A Taurus related example::

        >>> # automatically get the data from a DEV_ENCODED attribute
        >>> import taurus
        >>> from taurus.core.util.codecs import CodecFactory
        >>> cf = CodecFactory()
        >>> devenc_attr = taurus.Attribute('a/b/c/devenc_attr')
        >>> v = devenc_attr.read()
        >>> codec = CodecFactory().getCodec(v.format)
        >>> f, d = codec.decode((v.format, v.value))
    """

    import copy

    # need by VideoImageCodec
    import struct
    import sys
    import numpy

    from .singleton import Singleton
    from .log import Logger
    from .containers import CaselessDict

    __all__ = [
        "Codec",
        "NullCodec",
        "ZIPCodec",
        "BZ2Codec",
        "JSONCodec",
        "Utf8Codec",
        "FunctionCodec",
        "PlotCodec",
        "CodecPipeline",
        "CodecFactory",
    ]

    __docformat__ = "restructuredtext"

    class Codec(Logger):
        """The base class for all codecs"""

        def __init__(self):
            """Constructor"""
            Logger.__init__(self, self.__class__.__name__)

        # TODO: similar code exists in encode and decode methods from different
        # codecs. It should be implemented in this base class 'Codec'.

        def encode(self, data, *args, **kwargs):
            """encodes the given data. This method is abstract an therefore must
            be implemented in the subclass.

            :param data: a sequence of two elements where the first item is the
                encoding format of the second item object
            :type data: sequence[str, obj]
            :return: a sequence of two elements where the first item is the
                encoding format of the second item object
            :rtype: sequence[str, obj]
            :raises: NotImplementedError
            """
            raise NotImplementedError("encode cannot be called on abstract Codec")

        def decode(self, data, *args, **kwargs):
            """decodes the given data. This method is abstract an therefore must
            be implemented in the subclass.

            :param data: a sequence of two elements where the first item is the
                encoding format of the second item object
            :type data: sequence[str, obj]
            :return: a sequence of two elements where the first item is the
                encoding format of the second item object
            :rtype: sequence[str, obj]
            :raises: NotImplementedError
            """
            raise NotImplementedError("decode cannot be called on abstract Codec")

        def __str__(self):
            return "%s()" % self.__class__.__name__

        def __repr__(self):
            return "%s()" % self.__class__.__name__

    class NullCodec(Codec):

        def encode(self, data, *args, **kwargs):
            """encodes with Null encoder. Just returns the given data

            :param data: a sequence of two elements where the first item is the
                encoding format of the second item object
            :type data: sequence[str, obj]
            :return: a sequence of two elements where the first item is the
                encoding format of the second item object
            :rtype: sequence[str, obj]
            """
            format = "null"
            if len(data[0]):
                format += "_%s" % data[0]
            return format, data[1]

        def decode(self, data, *args, **kwargs):
            """decodes with Null encoder. Just returns the given data

            :param data: a sequence of two elements where the first item is the
                encoding format of the second item object
            :type data: sequence[str, obj]
            :return: a sequence of two elements where the first item is the
                encoding format of the second item object
            :rtype: sequence[str, obj]
            """
            if not data[0].startswith("null"):
                return data
            format = data[0].partition("_")[2]
            return format, data[1]

    class ZIPCodec(Codec):
        """A codec able to encode/decode to/from gzip format. It uses the
        :mod:`zlib` module

        Example::

            >>> from taurus.core.util.codecs import CodecFactory

            >>> # first encode something
            >>> data = 100 * "Hello world\\n"
            >>> cf = CodecFactory()
            >>> codec = cf.getCodec('zip')
            >>> format, encoded_data = codec.encode(("", data))
            >>> print(len(data), len(encoded_data))
            1200, 31
            >>> format, decoded_data = codec.decode((format, encoded_data))
            >>> print(decoded_data[20])
            'Hello world\\nHello wo'
        """

        def encode(self, data, *args, **kwargs):
            """encodes the given data to gzip bytes. The given data **must** be
            bytes

            :param data: a sequence of two elements where the first item is the
                encoding format of the second item object
            :type data: sequence[str, obj]
            :return: a sequence of two elements where the first item is the
                encoding format of the second item object
            :rtype: sequence[str, obj]
            """
            import zlib

            format = "zip"
            if len(data[0]):
                format += "_%s" % data[0]
            return format, zlib.compress(data[1])

        def decode(self, data, *args, **kwargs):
            """decodes the given data from a gzip bytes.

            :param data: a sequence of two elements where the first item is the
                encoding format of the second item object
            :type data: sequence[str, obj]
            :return: a sequence of two elements where the first item is the
                encoding format of the second item object
            :rtype: sequence[str, obj]
            """
            import zlib

            if not data[0].startswith("zip"):
                return data
            format = data[0].partition("_")[2]
            return format, zlib.decompress(data[1])

    class BZ2Codec(Codec):
        """A codec able to encode/decode to/from BZ2 format. It uses the :mod:`bz2`
        module

        Example::

            >>> from taurus.core.util.codecs import CodecFactory

            >>> # first encode something
            >>> data = 100 * "Hello world\\n"
            >>> cf = CodecFactory()
            >>> codec = cf.getCodec('bz2')
            >>> format, encoded_data = codec.encode(("", data))
            >>> print(len(data), len(encoded_data))
            1200, 68
            >>> format, decoded_data = codec.decode((format, encoded_data))
            >>> print(decoded_data[20])
            'Hello world\\nHello wo'
        """

        def encode(self, data, *args, **kwargs):
            """encodes the given data to bz2 bytes. The given data **must** be
            bytes

            :param data: a sequence of two elements where the first item is the
                encoding format of the second item object
            :type data: sequence[str, obj]
            :return: a sequence of two elements where the first item is the
                encoding format of the second item object
            :rtype: sequence[str, obj]
            """
            import bz2

            format = "bz2"
            if len(data[0]):
                format += "_%s" % data[0]
            return format, bz2.compress(data[1])

        def decode(self, data, *args, **kwargs):
            """decodes the given data from bz2 bytes.

            :param data: a sequence of two elements where the first item is the
                encoding format of the second item object
            :type data: sequence[str, obj]
            :return: a sequence of two elements where the first item is the
                encoding format of the second item object
            :rtype: sequence[str, obj]
            """
            import bz2

            if not data[0].startswith("bz2"):
                return data
            format = data[0].partition("_")[2]
            return format, bz2.decompress(data[1])

    class PickleCodec(Codec):
        """A codec able to encode/decode to/from pickle format. It uses the
        :mod:`pickle` module.

        Example::

            >>> from taurus.core.util.codecs import CodecFactory

            >>> cf = CodecFactory()
            >>> codec = cf.getCodec('pickle')
            >>>
            >>> # first encode something
            >>> data = { 'hello' : 'world', 'goodbye' : 1000 }
            >>> format, encoded_data = codec.encode(("", data))
            >>>
            >>> # now decode it
            >>> format, decoded_data = codec.decode((format, encoded_data))
            >>> print(decoded_data)
            {'hello': 'world', 'goodbye': 1000}
        """

        def encode(self, data, *args, **kwargs):
            """encodes the given data to pickle bytes. The given data **must** be
            a python object that :mod:`pickle` is able to convert.

            :param data: a sequence of two elements where the first item is the
                encoding format of the second item object
            :type data: sequence[str, obj]
            :return: a sequence of two elements where the first item is the
                encoding format of the second item object
            :rtype: sequence[str, obj]
            """
            import pickle

            format = "pickle"
            if len(data[0]):
                format += "_%s" % data[0]
            # make it compact by default
            kwargs["protocol"] = kwargs.get("protocol", pickle.DEFAULT_PROTOCOL)
            return format, pickle.dumps(data[1], *args, **kwargs)

        def decode(self, data, *args, **kwargs):
            """decodes the given data from a pickle string.

            :param data: a sequence of two elements where the first item is the
                encoding format of the second item object
            :type data: sequence[str, obj]
            :return: a sequence of two elements where the first item is the
                encoding format of the second item object
            :rtype: sequence[str, obj]
            """
            import pickle

            if not data[0].startswith("pickle"):
                return data
            format = data[0].partition("_")[2]

            if isinstance(data[1], memoryview):
                data = data[0], bytes(data[1])

            return format, pickle.loads(data[1])

    class JSONCodec(Codec):
        """A codec able to encode/decode to/from json format. It uses the
        :mod:`json` module.

        Example::

            >>> from taurus.core.util.codecs import CodecFactory

            >>> cf = CodecFactory()
            >>> codec = cf.getCodec('json')
            >>>
            >>> # first encode something
            >>> data = { 'hello' : 'world', 'goodbye' : 1000 }
            >>> format, encoded_data = codec.encode(("", data))
            >>> print(encoded_data)
            '{"hello": "world", "goodbye": 1000}'
            >>>
            >>> # now decode it
            >>> format, decoded_data = codec.decode((format, encoded_data))
            >>> print(decoded_data)
            {'hello': 'world', 'goodbye': 1000}
        """

        def encode(self, data, *args, **kwargs):
            """encodes the given data to a json string. The given data **must** be
            a python object that json is able to convert.

            :param data: a sequence of two elements where the first item is the
                encoding format of the second item object
            :type data: sequence[str, obj]
            :return: a sequence of two elements where the first item is the
                encoding format of the second item object
            :rtype: sequence[str, obj]
            """
            import json

            format = "json"
            if len(data[0]):
                format += "_%s" % data[0]
            # make it compact by default
            kwargs["separators"] = kwargs.get("separators", (",", ":"))
            return format, json.dumps(data[1], *args, **kwargs)

        def decode(self, data, *args, **kwargs):
            """decodes the given data from a json string.

            :param data: a sequence of two elements where the first item is the
                encoding format of the second item object
            :type data: sequence[str, obj]
            :return: a sequence of two elements where the first item is the
                encoding format of the second item object
            :rtype: sequence[str, obj]
            """
            import json

            if not data[0].startswith("json"):
                return data
            format = data[0].partition("_")[2]

            ensure_ascii = kwargs.pop("ensure_ascii", False)

            if isinstance(data[1], memoryview):
                data = data[0], str(data[1])

            data = json.loads(data[1])
            if ensure_ascii:
                data = self._transform_ascii(data)
            return format, data

        def _transform_ascii(self, data):
            if isinstance(data, str):
                return data.encode("utf-8")
            elif isinstance(data, dict):
                return self._transform_dict(data)
            elif isinstance(data, list):
                return self._transform_list(data)
            elif isinstance(data, tuple):
                return tuple(self._transform_list(data))
            else:
                return data

        def _transform_list(self, lst):
            return [self._transform_ascii(item) for item in lst]

        def _transform_dict(self, dct):
            newdict = {}
            for k, v in dct.items():
                newdict[self._transform_ascii(k)] = self._transform_ascii(v)
            return newdict

    class Utf8Codec(Codec):
        """A codec able to encode/decode utf8 strings to/from bytes.
        Useful to adapt i/o encodings in a codec pipe.

        Example::

            >>> from taurus.core.util.codecs import CodecFactory

            >>> cf = CodecFactory()
            >>> codec = cf.getCodec('zip_utf8_json')
            >>>
            >>> # first encode something
            >>> data = { 'hello' : 'world', 'goodbye' : 1000 }
            >>> format, encoded_data = codec.encode(("", data))
            >>>
            >>> # now decode it
            >>> _, decoded_data = codec.decode((format, encoded_data))
            >>> print(decoded_data)
        """

        def encode(self, data, *args, **kwargs):
            """
            Encodes the given utf8 string to bytes.

            :param data: a sequence of two elements where the first item is the
                encoding format of the second item object
            :type data: sequence[str, obj]
            :return: a sequence of two elements where the first item is the
                encoding format of the second item object
            :rtype: sequence[str, obj]
            """
            format = "utf8"
            fmt, data = data
            if len(fmt):
                format += "_%s" % fmt
            return format, str(data).encode()

        def decode(self, data, *args, **kwargs):
            """decodes the given data from a bytes.

            :param data: a sequence of two elements where the first item is the
                encoding format of the second item object
            :type data: sequence[str, obj]
            :return: a sequence of two elements where the first item is the
                encoding format of the second item object
            :rtype: sequence[str, obj]
            """
            fmt, data = data
            fmt = fmt.partition("_")[2]
            return fmt, bytes(data).decode()

    class BSONCodec(Codec):
        """A codec able to encode/decode to/from bson format. It uses the
        :mod:`bson` module.

        Example::

            >>> from taurus.core.util.codecs import CodecFactory

            >>> cf = CodecFactory()
            >>> codec = cf.getCodec('bson')
            >>>
            >>> # first encode something
            >>> data = { 'hello' : 'world', 'goodbye' : 1000 }
            >>> format, encoded_data = codec.encode(("", data))
            >>>
            >>> # now decode it
            >>> _, decoded_data = codec.decode((format, encoded_data))
            >>> print(decoded_data)
            {'hello': 'world', 'goodbye': 1000}
        """

        def encode(self, data, *args, **kwargs):
            """encodes the given data to a bson string. The given data **must** be
            a python object that bson is able to convert.

            :param data: a sequence of two elements where the first item is the
                encoding format of the second item object
            :type data: sequence[str, obj]
            :return: a sequence of two elements where the first item is the
                encoding format of the second item object
            :rtype: sequence[str, obj]
            """
            import bson

            format = "bson"
            if len(data[0]):
                format += "_%s" % data[0]
            return format, bson.BSON.encode(data[1], *args, **kwargs)

        def decode(self, data, *args, **kwargs):
            """decodes the given data from a bson string.

            :param data: a sequence of two elements where the first item is the
                encoding format of the second item object
            :type data: sequence[str, obj]
            :return: a sequence of two elements where the first item is the
                encoding format of the second item object
            :rtype: sequence[str, obj]
            """
            import bson

            if not data[0].startswith("bson"):
                return data
            format = data[0].partition("_")[2]
            ensure_ascii = kwargs.pop("ensure_ascii", False)

            data = data[0], bson.BSON(data[1])

            data = self.decode(data[1])
            if ensure_ascii:
                data = self._transform_ascii(data)
            return format, data

        def _transform_ascii(self, data):
            if isinstance(data, str):
                return data.encode("utf-8")
            elif isinstance(data, dict):
                return self._transform_dict(data)
            elif isinstance(data, list):
                return self._transform_list(data)
            elif isinstance(data, tuple):
                return tuple(self._transform_list(data))
            else:
                return data

        def _transform_list(self, lst):
            return [self._transform_ascii(item) for item in lst]

        def _transform_dict(self, dct):
            newdict = {}
            for k, v in dct.items():
                newdict[self._transform_ascii(k)] = self._transform_ascii(v)
            return newdict

    class FunctionCodec(Codec):
        """A generic function codec"""

        def __init__(self, func_name):
            Codec.__init__(self)
            self._func_name = func_name

        def encode(self, data, *args, **kwargs):
            format = self._func_name
            if len(data[0]):
                format += "_%s" % data[0]
            return format, {"type": self._func_name, "data": data[1]}

        def decode(self, data, *args, **kwargs):
            if not data[0].startswith(self._func_name):
                return data
            format = data[0].partition("_")[2]
            return format, data[1]

    class PlotCodec(FunctionCodec):
        """A specialization of the :class:`FunctionCodec` for plot function"""

        def __init__(self):
            FunctionCodec.__init__(self, "plot")

    class VideoImageCodec(Codec):
        """A codec able to encode/decode to/from LImA video_image format.

        Example::

            >>> from taurus.core.util.codecs import CodecFactory
            >>> import PyTango

            >>> # first get an image from a LImA device to decode
            >>> d = PyTango.DeviceProxy(ccdName)
            >>> data = d.read_attribute('video_last_image').value
            >>> cf = CodecFactory()
            >>> codec = cf.getCodec('VIDEO_IMAGE')
            >>> format,decoded_data = codec.decode(data)
            >>> # encode it again to check
            >>> format, encoded_data = codec.encode(("",decoded_data))
            >>> # compare images excluding the header:
            >>> data[1][32:] == encoded_data[32:]
            >>> # The headers can be different in the frameNumber
            >>> struct.unpack('!IHHqiiHHHH',data[1][:32])
            (1447314767, 1, 0, 6868, 1294, 964, 0, 32, 0, 0)
            >>> struct.unpack('!IHHqiiHHHH',encoded_data[:32])
            (1447314767, 1, 0, -1, 1294, 964, 0, 32, 0, 0)
        """

        VIDEO_HEADER_FORMAT = "!IHHqiiHHHH"

        def encode(self, data, *args, **kwargs):
            """encodes the given data to a LImA's video_image. The given data
            **must** be an numpy.array

            :param data: a sequence of two elements where the first item is the
                encoding format of the second item object
            :type data: sequence[str, obj]
            :return: a sequence of two elements where the first item is the
                encoding format of the second item object
            :rtype: sequence[str, obj]
            """

            # TODO: support encoding for colour imgage modes

            fmt = "videoimage"
            if len(data[0]):
                fmt += "_%s" % data[0]
            # imgMode depends on numpy.array dtype
            imgMode = self.__getModeId(str(data[1].dtype))
            # frameNumber, unknown then -1
            height, width = data[1].shape
            header = self.__packHeader(imgMode, -1, width, height)
            buffer = data[1].tobytes()
            return fmt, header + buffer

        def decode(self, data, *args, **kwargs):
            """decodes the given data from a LImA's video_image.

            :param data: a sequence of two elements where the first item is the
                encoding format of the second item object
            :type data: sequence[str, obj]
            :return: a sequence of two elements where the first item is the
                encoding format of the second item object
            :rtype: sequence[str, obj]
            """

            if data[0].startswith("VIDEO_IMAGE"):
                fixedformat = data[0].replace("VIDEO_IMAGE", "videoimage")
                _, _, fmt = fixedformat.partition("_")
            elif data[0].startswith("videoimage"):
                _, _, fmt = data[0].partition("_")
            else:
                return data
            header = self.__unpackHeader(
                data[1][: struct.calcsize(self.VIDEO_HEADER_FORMAT)]
            )

            imgBuffer = data[1][struct.calcsize(self.VIDEO_HEADER_FORMAT) :]
            dtype = self.__getDtypeId(header["imageMode"])

            if header["imageMode"] == 6:
                # RGB24, 3 bytes per pixel
                rgba = numpy.frombuffer(imgBuffer, dtype)
                bbuf = rgba[0::3]
                gbuf = rgba[1::3]
                rbuf = rgba[2::3]
                r = rbuf.reshape(header["height"], header["width"])
                g = gbuf.reshape(header["height"], header["width"])
                b = bbuf.reshape(header["height"], header["width"])
                img2D = numpy.dstack((r, g, b))

            elif header["imageMode"] == 7:
                # RGBA 4 bytes per pixel
                rgba = numpy.frombuffer(imgBuffer, dtype)
                bbuf = rgba[0::4]
                gbuf = rgba[1::4]
                rbuf = rgba[2::4]
                # abuf = rgba[3::4]
                r = rbuf.reshape(header["height"], header["width"])
                g = gbuf.reshape(header["height"], header["width"])
                b = bbuf.reshape(header["height"], header["width"])
                # a = abuf.reshape(header['height'],header['width'])
                img2D = numpy.dstack((r, g, b))

            elif header["imageMode"] == 17:
                # YUV444 3 bytes per pixel
                yuv = numpy.frombuffer(imgBuffer, dtype)
                y = yuv[0::3]
                u = yuv[1::3]
                v = yuv[2::3]

                rbuff, gbuff, bbuff = self.__yuv2rgb(y, u, v)

                # Shape buffers to image size
                r = rbuff.reshape(header["height"], header["width"])
                g = gbuff.reshape(header["height"], header["width"])
                b = bbuff.reshape(header["height"], header["width"])

                # Build the RGB image
                img2D = numpy.dstack((r, g, b))

            elif header["imageMode"] == 16 or header["imageMode"] == 19:
                # YUV422 4 bytes per 2 pixels
                yuv = numpy.frombuffer(imgBuffer, dtype)
                u = yuv[0::4]
                y1 = yuv[1::4]
                v = yuv[2::4]
                y2 = yuv[3::4]

                r1, g1, b1 = self.__yuv2rgb(y1, u, v)
                r2, g2, b2 = self.__yuv2rgb(y2, u, v)

                # Create RGB buffers
                rbuff = numpy.dstack((r1, r2)).reshape(
                    header["height"] * header["width"]
                )
                gbuff = numpy.dstack((g1, g2)).reshape(
                    header["height"] * header["width"]
                )
                bbuff = numpy.dstack((b1, b2)).reshape(
                    header["height"] * header["width"]
                )

                # Shape buffers to image size
                r = rbuff.reshape(header["height"], header["width"])
                g = gbuff.reshape(header["height"], header["width"])
                b = bbuff.reshape(header["height"], header["width"])

                # Build the RGB image
                img2D = numpy.dstack((r, g, b))

            elif header["imageMode"] == 15:
                # YUV411 6 bytes per 4 pixels
                yuv = numpy.frombuffer(imgBuffer, dtype)
                u = yuv[0::6]
                y1 = yuv[1::6]
                y2 = yuv[2::6]
                v = yuv[3::6]
                y3 = yuv[4::6]
                y4 = yuv[5::6]

                r1, g1, b1 = self.__yuv2rgb(y1, u, v)
                r2, g2, b2 = self.__yuv2rgb(y2, u, v)
                r3, g3, b3 = self.__yuv2rgb(y3, u, v)
                r4, g4, b4 = self.__yuv2rgb(y4, u, v)

                # Create RGB buffers
                rbuff = numpy.dstack((r1, r2, r3, r4)).reshape(
                    header["height"] * header["width"]
                )
                gbuff = numpy.dstack((g1, g2, g3, g4)).reshape(
                    header["height"] * header["width"]
                )
                bbuff = numpy.dstack((b1, b2, b3, b4)).reshape(
                    header["height"] * header["width"]
                )

                # Shape buffers to image size
                r = rbuff.reshape(header["height"], header["width"])
                g = gbuff.reshape(header["height"], header["width"])
                b = bbuff.reshape(header["height"], header["width"])

                img2D = numpy.dstack((r, g, b))

            else:
                img1D = numpy.frombuffer(imgBuffer, dtype)
                img2D = img1D.reshape(header["height"], header["width"])

            return fmt, img2D

        def __yuv2rgb(self, y, u, v):
            """YUV444 to RGB888 conversion"""
            Cr = v - 128.0
            Cb = u - 128.0

            R = y + 1.402 * Cr
            G = y - 0.344 * Cb - 0.714 * Cr
            B = y + 1.772 * Cb

            return (
                numpy.clip(R, 0, 255),
                numpy.clip(G, 0, 255),
                numpy.clip(B, 0, 255),
            )

        def __unpackHeader(self, header):
            h = struct.unpack(self.VIDEO_HEADER_FORMAT, header)
            headerDict = {}
            headerDict["magic"] = h[0]
            headerDict["headerVersion"] = h[1]
            headerDict["imageMode"] = h[2]
            headerDict["frameNumber"] = h[3]
            headerDict["width"] = h[4]
            headerDict["height"] = h[5]
            headerDict["endianness"] = h[6]
            headerDict["headerSize"] = h[7]
            headerDict["padding"] = h[8:]
            return headerDict

        def __packHeader(self, imgMode, frameNumber, width, height):
            magic = 0x5644454F
            version = 1
            endian = 0 if sys.byteorder == "little" else 1
            hsize = struct.calcsize(self.VIDEO_HEADER_FORMAT)
            return struct.pack(
                self.VIDEO_HEADER_FORMAT,
                magic,
                version,
                imgMode,
                frameNumber,
                width,
                height,
                endian,
                hsize,
                0,
                0,
            )  # padding

        def __getModeId(self, mode):
            return {  # when encode
                "uint8": 0,  # Core.Y8,
                "uint16": 1,  # Core.Y16,
                "uint32": 2,  # Core.Y32,
                "uint64": 3,  # Core.Y64,
                # when decode
                "Y8": 0,  # Core.Y8,
                "Y16": 1,  # Core.Y16,
                "Y32": 2,  # Core.Y32,
                "Y64": 3,  # Core.Y64,
                # TODO: other modes
                # 'RGB555'     : 4,  # Core.RGB555,
                # 'RGB565'     : 5,  # Core.RGB565,
                "RGB24": 6,  # Core.RGB24,
                "RGB32": 7,  # Core.RGB32,
                # 'BGR24'      : 8,  # Core.BGR24,
                # 'BGR32'      : 9,  # Core.BGR32,
                # 'BAYER RG8'  : 10,  # Core.BAYER_RG8,
                # 'BAYER RG16' : 11,  # Core.BAYER_RG16,
                # 'BAYER BG8'  : 12,  # Core.BAYER_BG8,
                # 'BAYER BG16' : 13,  # Core.BAYER_BG16,
                # 'I420'       : 14,  # Core.I420,
                # 'YUV411'     : 15,  # Core.YUV411,
                "YUV422": 16,  # Core.YUV422,
                # 'YUV444'     : 17,  # Core.YUV444
                "YUV422PACKED": 19,  # Core.YUV422PACKED,
            }[mode]

        def __getFormatId(self, mode):
            return {
                0: "B",
                1: "H",
                2: "I",
                3: "L",
                # 4:"RGB555"  # Core.RGB555,
                # 5:"RGB565"  # Core.RGB565,
                6: "RGB24",  # Core.RGB24,
                7: "RGB32",  # Core.RGB32,
                # 8: "BGR24",  # Core.BGR24,
                # 9: "BGR32"  # Core.BGR32,
                # 10: "BAYER RG8"  # Core.BAYER_RG8,
                # 11: "BAYER RG16"  # Core.BAYER_RG16,
                # 12: "BAYER BG8"  # Core.BAYER_BG8,
                # 13: "BAYER BG16"  #: Core.BAYER_BG16,
                # 14: "I420"  # Core.I420,
                # 15:"YUV411"  # Core.YUV411,
                16: "YUV422",  # Core.YUV422,
                19: "YUV422PACKED",  # Core.YUV422PACKED,
                # 17:"YUV444"  # Core.YUV444
            }[mode]

        def __getDtypeId(self, mode):
            return {
                0: "uint8",
                1: "uint16",
                2: "uint32",
                3: "uint64",
                # "RGB555"     : Core.RGB555,
                # "RGB565"     : Core.RGB565,
                6: "uint8",  # Core.RGB24,
                7: "uint8",  # Core.RGB32,
                # "BGR24"      : Core.BGR24,
                # "BGR32"      : Core.BGR32,
                # "BAYER RG8"  : Core.BAYER_RG8,
                # "BAYER RG16" : Core.BAYER_RG16,
                # "BAYER BG8"  : Core.BAYER_BG8,
                # "BAYER BG16" : Core.BAYER_BG16,
                # "I420"       : Core.I420,
                # "YUV411"     : Core.YUV411,
                16: "uint8",  # Core.YUV422,
                19: "uint8",  # Core.YUV422PACKED,
                # "YUV444"     : Core.YUV444
            }[mode]

    class CodecPipeline(Codec, list):
        """The codec class used when encoding/decoding data with multiple encoders

        Example usage::

            >>> from taurus.core.util.codecs import CodecPipeline

            >>> data = range(100000)
            >>> codec = CodecPipeline('bz2_json')
            >>> format, encoded_data = codec.encode(("", data))

            # decode it
            format, decoded_data = codec.decode((format, encoded_data))
            print(decoded_data)
        """

        def __init__(self, format):
            """Constructor. The CodecPipeline object will be created using
            the :class:`CodecFactory` to search for format(s) given in the format
            parameter.

            :param format: a string representing the format.
            :type format: str
            """

            Codec.__init__(self)
            list.__init__(self)

            f = CodecFactory()
            for i in format.split("_"):
                codec = f.getCodec(i)
                self.debug("Appending %s => %s" % (i, codec))
                if codec is None:
                    raise TypeError(
                        "Unsupported codec %s (namely %s)" % (format, i)
                    )
                self.append(codec)
            self.debug("Done")

        def encode(self, data, *args, **kwargs):
            """encodes the given data.

            :param data: a sequence of two elements where the first item is the
                encoding format of the second item object
            :type data: sequence[str, obj]
            :return: a sequence of two elements where the first item is the
                encoding format of the second item object
            :rtype: sequence[str, obj]
            """
            for codec in reversed(self):
                data = codec.encode(data, *args, **kwargs)
            return data

        def decode(self, data, *args, **kwargs):
            """decodes the given data.

            :param data: a sequence of two elements where the first item is the
                encoding format of the second item object
            :type data: sequence[str, obj]
            :return: a sequence of two elements where the first item is the
                encoding format of the second item object
            :rtype: sequence[str, obj]
            """
            for codec in self:
                data = codec.decode(data, *args, **kwargs)
            return data

    class CodecFactory(Singleton, Logger):
        """The singleton CodecFactory class.

        To get the singleton object do::

            from taurus.core.util.codecs import CodecFactory
            f = CodecFactory()

        The :class:`CodecFactory` class allows you to get a codec object for a
        given format and also to register new codecs.

        The :class:`CodecPipeline` is a special codec that is able to code/decode a
        sequence of codecs. This way you can have codecs 'inside' codecs.

        Example::

            >>> from taurus.core.util.codecs import CodecFactory
            >>> cf = CodecFactory()
            >>> json_codec = cf.getCodec('json')
            >>> bz2_json_codec = cf.getCodec('bz2_json')
            >>> data = range(100000)
            >>> f1, enc_d1 = json_codec.encode(('', data))
            >>> f2, enc_d2 = bz2_json_codec.encode(('', data))
            >>> print(len(enc_d1), len(enc_d2))
            688890 123511
            >>>
            >>> f1, dec_d1 = json_codec.decode((f1, enc_d1))
            >>> f2, dec_d2 = bz2_json_codec.decode((f2, enc_d2))

        A Taurus related example::

            >>> # automatically get the data from a DEV_ENCODED attribute
            >>> import taurus
            >>> from taurus.core.util.codecs import CodecFactory
            >>> cf = CodecFactory()
            >>> devenc_attr = taurus.Attribute('a/b/c/devenc_attr')
            >>> v = devenc_attr.read()
            >>> codec = CodecFactory().getCodec(v.format)
            >>> f, d = codec.decode((v.format, v.value))
        """

        #: Default minimum map of registered codecs
        CODEC_MAP = CaselessDict(
            {
                "json": JSONCodec,
                "utf8": Utf8Codec,
                "bson": BSONCodec,
                "bz2": BZ2Codec,
                "zip": ZIPCodec,
                "pickle": PickleCodec,
                "plot": PlotCodec,
                "VIDEO_IMAGE": VideoImageCodec,  # deprecated
                "videoimage": VideoImageCodec,
                "null": NullCodec,
                "none": NullCodec,
                "": NullCodec,
            }
        )

        def __init__(self):
            """Initialization. Nothing to be done here for now."""
            pass

        def init(self, *args, **kwargs):
            """Singleton instance initialization."""
            name = self.__class__.__name__
            self.call__init__(Logger, name)

            self._codec_klasses = copy.copy(CodecFactory.CODEC_MAP)

            # dict<str, Codec>
            # where:
            #  - key is the codec format
            #  - value is the codec object that supports the format
            self._codecs = CaselessDict()

        def registerCodec(self, format, klass):
            """Registers a new codec. If a codec already exists for the given
            format it is removed.

            :param format: the codec id
            :type format: str
            :param klass: the class that handles the format
            :type klass: Codec class
            """
            self._codec_klasses[format] = klass

            # del old codec if exists
            if format in self._codecs:
                del self._codecs[format]

        def unregisterCodec(self, format):
            """Unregisters the given format. If the format does not exist an
            exception is thrown.

            :param format: the codec id
            :type format: str

            :raises: KeyError
            """
            if format in self._codec_klasses:
                del self._codec_klasses[format]

            if format in self._codecs:
                del self._codecs[format]

        def getCodec(self, format):
            """Returns the codec object for the given format or None if no suitable
            codec is found

            :param format: the codec id
            :type format: str
            :return: the codec object for the given format
            :rtype: Codec or None
            """
            codec = self._codecs.get(format)
            if codec is None:
                codec = self._getNewCodec(format)
                if codec is not None:
                    self._codecs[format] = codec
            return codec

        def _getNewCodec(self, format):
            klass = self._codec_klasses.get(format)
            if klass is not None:
                ret = klass()
            else:
                try:
                    ret = CodecPipeline(format)
                except Exception:
                    ret = self._codec_klasses.get("")()
            return ret

        def decode(self, data, *args, **kwargs):
            while len(data[0]):
                data = self.getCodec(data[0]).decode(data, *args, **kwargs)
            return data[1]

        def encode(self, format, data, *args, **kwargs):
            return self.getCodec(format).encode(data, *args, **kwargs)