# -*- coding: utf-8 -*-
import socket
import struct

from py_zipkin.encoding._types import Kind
from py_zipkin.util import unsigned_hex_to_signed_int
try:
    from py_zipkin.encoding.protobuf import zipkin_pb2
except ImportError:  # pragma: no cover
    zipkin_pb2 = None

def installed():
    """Checks whether the protobud library is installed and can be used.

    :return: True if everything's fine, False otherwise
    :rtype: bool
    """
    return zipkin_pb2 is not None

def encode_pb_list(pb_spans):
    """Encode list of protobuf Spans to binary.

    :param pb_spans: list of protobuf Spans.
    :type pb_spans: list of zipkin_pb2.Span
    :return: encoded list.
    :rtype: bytes
    """
    pb_list = zipkin_pb2.ListOfSpans()
    pb_list.spans.extend(pb_spans)
    return pb_list.SerializeToString()

def create_protobuf_span(span):
    """Converts a py_zipkin Span in a protobuf Span.

    :param span: py_zipkin Span to convert.
    :type span: py_zipkin.encoding.Span
    :return: protobuf's Span
    :rtype: zipkin_pb2.Span
    """

    # Protobuf's composite types (i.e. Span's local_endpoint) are immutable.
    # So we can't create a zipkin_pb2.Span here and then set the appropriate
    # fields since `pb_span.local_endpoint = zipkin_pb2.Endpoint` fails.
    # Instead we just create the kwargs and pass them in to the Span constructor.
    pb_kwargs = {}

    pb_kwargs['trace_id'] = _hex_to_bytes(span.trace_id)

    if span.parent_id:
        pb_kwargs['parent_id'] = _hex_to_bytes(span.parent_id)

    pb_kwargs['id'] = _hex_to_bytes(span.span_id)

    pb_kind = _get_protobuf_kind(span.kind)
    if pb_kind:
        pb_kwargs['kind'] = pb_kind

    if span.name:
        pb_kwargs['name'] = span.name
    if span.timestamp:
        pb_kwargs['timestamp'] = int(span.timestamp * 1000 * 1000)
    if span.duration:
        pb_kwargs['duration'] = int(span.duration * 1000 * 1000)

    if span.local_endpoint:
        pb_kwargs['local_endpoint'] = _convert_endpoint(span.local_endpoint)

    if span.remote_endpoint:
        pb_kwargs['remote_endpoint'] = _convert_endpoint(span.remote_endpoint)

    if len(span.annotations) > 0:
        pb_kwargs['annotations'] = _convert_annotations(span.annotations)

    if len(span.tags) > 0:
        pb_kwargs['tags'] = span.tags

    if span.debug:
        pb_kwargs['debug'] = span.debug

    if span.shared:
        pb_kwargs['shared'] = span.shared

    return zipkin_pb2.Span(**pb_kwargs)

def _hex_to_bytes(hex_id):
    """Encodes to hexadecimal ids to big-endian binary.

    :param hex_id: hexadecimal id to encode.
    :type hex_id: str
    :return: binary representation.
    :type: bytes
    """
    if len(hex_id) <= 16:
        int_id = unsigned_hex_to_signed_int(hex_id)
        return struct.pack('>q', int_id)
    else:
        # There's no 16-bytes encoding in Python's struct. So we convert the
        # id as 2 64 bit ids and then concatenate the result.

        # NOTE: we count 16 chars from the right (:-16) rather than the left so
        # that ids with less than 32 chars will be correctly pre-padded with 0s.
        high_id = unsigned_hex_to_signed_int(hex_id[:-16])
        high_bin = struct.pack('>q', high_id)

        low_id = unsigned_hex_to_signed_int(hex_id[-16:])
        low_bin = struct.pack('>q', low_id)

        return high_bin + low_bin

def _get_protobuf_kind(kind):
    """Converts py_zipkin's Kind to Protobuf's Kind.

    :param kind: py_zipkin's Kind.
    :type kind: py_zipkin.Kind
    :return: correcponding protobuf's kind value.
    :rtype: zipkin_pb2.Span.Kind
    """
    if kind == Kind.CLIENT:
        return zipkin_pb2.Span.CLIENT
    elif kind == Kind.SERVER:
        return zipkin_pb2.Span.SERVER
    elif kind == Kind.PRODUCER:
        return zipkin_pb2.Span.PRODUCER
    elif kind == Kind.CONSUMER:
        return zipkin_pb2.Span.CONSUMER
    return None

def _convert_endpoint(endpoint):
    """Converts py_zipkin's Endpoint to Protobuf's Endpoint.

    :param endpoint: py_zipkins' endpoint to convert.
    :type endpoint: py_zipkin.encoding.Endpoint
    :return: corresponding protobuf's endpoint.
    :rtype: zipkin_pb2.Endpoint
    """
    pb_endpoint = zipkin_pb2.Endpoint()

    if endpoint.service_name:
        pb_endpoint.service_name = endpoint.service_name
    if endpoint.port and endpoint.port != 0:
        pb_endpoint.port = endpoint.port
    if endpoint.ipv4:
        pb_endpoint.ipv4 = socket.inet_pton(socket.AF_INET, endpoint.ipv4)
    if endpoint.ipv6:
        pb_endpoint.ipv6 = socket.inet_pton(socket.AF_INET6, endpoint.ipv6)

    return pb_endpoint

def _convert_annotations(annotations):
    """Converts py_zipkin's annotations dict to protobuf.

    :param annotations: annotations dict.
    :type annotations: dict
    :return: corresponding protobuf's list of annotations.
    :rtype: list
    """
    pb_annotations = []
    for value, ts in annotations.items():
        pb_annotations.append(zipkin_pb2.Annotation(
            timestamp=int(ts * 1000 * 1000),
            value=value,
        ))
    return pb_annotations


# -*- coding: utf-8 -*-
import os
import socket
import struct

import thriftpy2
from thriftpy2.protocol.binary import TBinaryProtocol
from thriftpy2.protocol.binary import write_list_begin
from thriftpy2.thrift import TType
from thriftpy2.transport import TMemoryBuffer

from py_zipkin.util import unsigned_hex_to_signed_int

thrift_filepath = os.path.join(os.path.dirname(__file__), 'zipkinCore.thrift')
zipkin_core = thriftpy2.load(thrift_filepath, module_name="zipkinCore_thrift")

SERVER_ADDR_VAL = '\x01'
LIST_HEADER_SIZE = 5  # size in bytes of the encoded list header

dummy_endpoint = zipkin_core.Endpoint()

def create_annotation(timestamp, value, host):
    """
    Create a zipkin annotation object

    :param timestamp: timestamp of when the annotation occured in microseconds
    :param value: name of the annotation, such as 'sr'
    :param host: zipkin endpoint object

    :returns: zipkin annotation object
    """
    return zipkin_core.Annotation(timestamp=timestamp, value=value, host=host)

def create_binary_annotation(key, value, annotation_type, host):
    """
    Create a zipkin binary annotation object

    :param key: name of the annotation, such as 'http.uri'
    :param value: value of the annotation, such as a URI
    :param annotation_type: type of annotation, such as AnnotationType.I32
    :param host: zipkin endpoint object

    :returns: zipkin binary annotation object
    """
    return zipkin_core.BinaryAnnotation(
        key=key,
        value=value,
        annotation_type=annotation_type,
        host=host,
    )

def create_endpoint(port=0, service_name='unknown', ipv4=None, ipv6=None):
    """Create a zipkin Endpoint object.

    An Endpoint object holds information about the network context of a span.

    :param port: int value of the port. Defaults to 0
    :param service_name: service name as a str. Defaults to 'unknown'
    :param ipv4: ipv4 host address
    :param ipv6: ipv6 host address
    :returns: thrift Endpoint object
    """
    ipv4_int = 0
    ipv6_binary = None

    # Convert ip address to network byte order
    if ipv4:
        ipv4_int = struct.unpack('!i', socket.inet_pton(socket.AF_INET, ipv4))[0]

    if ipv6:
        ipv6_binary = socket.inet_pton(socket.AF_INET6, ipv6)

    # Zipkin passes unsigned values in signed types because Thrift has no
    # unsigned types, so we have to convert the value.
    port = struct.unpack('h', struct.pack('H', port))[0]
    return zipkin_core.Endpoint(
        ipv4=ipv4_int,
        ipv6=ipv6_binary,
        port=port,
        service_name=service_name,
    )

def copy_endpoint_with_new_service_name(endpoint, service_name):
    """Copies a copy of a given endpoint with a new service name.
    This should be very fast, on the order of several microseconds.

    :param endpoint: existing zipkin_core.Endpoint object
    :param service_name: str of new service name
    :returns: zipkin Endpoint object
    """
    return zipkin_core.Endpoint(
        ipv4=endpoint.ipv4,
        port=endpoint.port,
        service_name=service_name,
    )

def annotation_list_builder(annotations, host):
    """
    Reformat annotations dict to return list of corresponding zipkin_core objects.

    :param annotations: dict containing key as annotation name,
                        value being timestamp in seconds(float).
    :type host: :class:`zipkin_core.Endpoint`
    :returns: a list of annotation zipkin_core objects
    :rtype: list
    """
    return [
        create_annotation(int(timestamp * 1000000), key, host)
        for key, timestamp in annotations.items()
    ]

def binary_annotation_list_builder(binary_annotations, host):
    """
    Reformat binary annotations dict to return list of zipkin_core objects. The
    value of the binary annotations MUST be in string format.

    :param binary_annotations: dict with key, value being the name and value
                               of the binary annotation being logged.
    :type host: :class:`zipkin_core.Endpoint`
    :returns: a list of binary annotation zipkin_core objects
    :rtype: list
    """
    # TODO: Remove the type hard-coding of STRING to take it as a param option.
    ann_type = zipkin_core.AnnotationType.STRING
    return [
        create_binary_annotation(key, str(value), ann_type, host)
        for key, value in binary_annotations.items()
    ]

def create_span(
    span_id,
    parent_span_id,
    trace_id,
    span_name,
    annotations,
    binary_annotations,
    timestamp_s,
    duration_s,
):
    """Takes a bunch of span attributes and returns a thriftpy2 representation
    of the span. Timestamps passed in are in seconds, they're converted to
    microseconds before thrift encoding.
    """
    # Check if trace_id is 128-bit. If so, record trace_id_high separately.
    trace_id_length = len(trace_id)
    trace_id_high = None
    if trace_id_length > 16:
        assert trace_id_length == 32
        trace_id, trace_id_high = trace_id[16:], trace_id[:16]

    if trace_id_high:
        trace_id_high = unsigned_hex_to_signed_int(trace_id_high)

    span_dict = {
        'trace_id': unsigned_hex_to_signed_int(trace_id),
        'name': span_name,
        'id': unsigned_hex_to_signed_int(span_id),
        'annotations': annotations,
        'binary_annotations': binary_annotations,
        'timestamp': int(timestamp_s * 1000000) if timestamp_s else None,
        'duration': int(duration_s * 1000000) if duration_s else None,
        'trace_id_high': trace_id_high,
    }
    if parent_span_id:
        span_dict['parent_id'] = unsigned_hex_to_signed_int(parent_span_id)
    return zipkin_core.Span(**span_dict)

def span_to_bytes(thrift_span):
    """
    Returns a TBinaryProtocol encoded Thrift span.

    :param thrift_span: thrift object to encode.
    :returns: thrift object in TBinaryProtocol format bytes.
    """
    transport = TMemoryBuffer()
    protocol = TBinaryProtocol(transport)
    thrift_span.write(protocol)

    return bytes(transport.getvalue())

def encode_bytes_list(binary_thrift_obj_list):  # pragma: no cover
    """
    Returns a TBinaryProtocol encoded list of Thrift objects.

    :param binary_thrift_obj_list: list of TBinaryProtocol objects to encode.
    :returns: bynary object representing the encoded list.
    """
    transport = TMemoryBuffer()
    write_list_begin(transport, TType.STRUCT, len(binary_thrift_obj_list))
    for thrift_bin in binary_thrift_obj_list:
        transport.write(thrift_bin)

    return bytes(transport.getvalue())


# -*- coding: utf-8 -*-
import json

import six

from py_zipkin.encoding._decoders import get_decoder
from py_zipkin.encoding._encoders import get_encoder
from py_zipkin.encoding._helpers import Endpoint  # noqa: F401
from py_zipkin.encoding._helpers import Span  # noqa: F401
from py_zipkin.encoding._types import Encoding
from py_zipkin.exception import ZipkinError

_V2_ATTRIBUTES = ["tags", "localEndpoint", "remoteEndpoint", "shared", "kind"]

def detect_span_version_and_encoding(message):
    """Returns the span type and encoding for the message provided.

    The logic in this function is a Python port of
    https://github.com/openzipkin/zipkin/blob/master/zipkin/src/main/java/zipkin/internal/DetectingSpanDecoder.java

    :param message: span to perform operations on.
    :type message: byte array
    :returns: span encoding.
    :rtype: Encoding
    """
    # In case message is sent in as non-bytearray format,
    # safeguard convert to bytearray before handling
    if isinstance(message, six.string_types):
        # Even six.b is not enough to handle the py2/3 difference since
        # it uses latin-1 as default encoding and not utf-8.
        if six.PY2:
            message = six.b(message)  # pragma: no cover
        else:
            message = message.encode('utf-8')  # pragma: no cover

    if len(message) < 2:
        raise ZipkinError("Invalid span format. Message too short.")

    # Check for binary format
    if six.byte2int(message) <= 16:
        if six.byte2int(message) == 10 and six.byte2int(message[1:2]) != 0:
            return Encoding.V2_PROTO3
        return Encoding.V1_THRIFT

    str_msg = message.decode('utf-8')

    # JSON case for list of spans
    if str_msg[0] == '[':
        span_list = json.loads(str_msg)
        if len(span_list) > 0:
            # Assumption: All spans in a list are the same version
            # Logic: Search for identifying fields in all spans, if any span can
            # be strictly identified to a version, return that version.
            # Otherwise, if no spans could be strictly identified, default to V2.
            for span in span_list:
                if any(word in span for word in _V2_ATTRIBUTES):
                    return Encoding.V2_JSON
                elif (
                    'binaryAnnotations' in span or
                    (
                        'annotations' in span and
                        'endpoint' in span['annotations']
                    )
                ):
                    return Encoding.V1_JSON
            return Encoding.V2_JSON

    raise ZipkinError("Unknown or unsupported span encoding")

def convert_spans(spans, output_encoding, input_encoding=None):
    """Converts encoded spans to a different encoding.

    param spans: encoded input spans.
    type spans: byte array
    param output_encoding: desired output encoding.
    type output_encoding: Encoding
    param input_encoding: optional input encoding. If this is not specified, it'll
        try to understand the encoding automatically by inspecting the input spans.
    type input_encoding: Encoding
    :returns: encoded spans.
    :rtype: byte array
    """
    if not isinstance(input_encoding, Encoding):
        input_encoding = detect_span_version_and_encoding(message=spans)

    if input_encoding == output_encoding:
        return spans

    decoder = get_decoder(input_encoding)
    encoder = get_encoder(output_encoding)
    decoded_spans = decoder.decode_spans(spans)
    output_spans = []

    # Encode each indivicual span
    for span in decoded_spans:
        output_spans.append(encoder.encode_span(span))

    # Outputs from encoder.encode_span() can be easily concatenated in a list
    return encoder.encode_queue(output_spans)


# -*- coding: utf-8 -*-
import logging

from py_zipkin.storage import get_default_tracer

log = logging.getLogger('py_zipkin.thread_local')

def get_thread_local_zipkin_attrs():
    """A wrapper to return _thread_local.zipkin_attrs

    Returns a list of ZipkinAttrs objects, used for intra-process context
    propagation.

    .. deprecated::
       Use the Tracer interface which offers better multi-threading support.
       get_thread_local_zipkin_attrs will be removed in version 1.0.

    :returns: list that may contain zipkin attribute tuples
    :rtype: list
    """
    log.warning('get_thread_local_zipkin_attrs is deprecated. See DEPRECATIONS.rst'
                ' for details on how to migrate to using Tracer.')
    return get_default_tracer()._context_stack._storage

def get_thread_local_span_storage():
    """A wrapper to return _thread_local.span_storage

    Returns a SpanStorage object used to temporarily store all spans created in
    the current process. The transport handlers will pull from this storage when
    they emit the spans.

    .. deprecated::
       Use the Tracer interface which offers better multi-threading support.
       get_thread_local_span_storage will be removed in version 1.0.

    :returns: SpanStore object containing all non-root spans.
    :rtype: py_zipkin.storage.SpanStore
    """
    log.warning('get_thread_local_span_storage is deprecated. See DEPRECATIONS.rst'
                ' for details on how to migrate to using Tracer.')
    return get_default_tracer()._span_storage

def get_zipkin_attrs():
    """Get the topmost level zipkin attributes stored.

    .. deprecated::
       Use the Tracer interface which offers better multi-threading support.
       get_zipkin_attrs will be removed in version 1.0.

    :returns: tuple containing zipkin attrs
    :rtype: :class:`zipkin.ZipkinAttrs`
    """
    from py_zipkin.storage import ThreadLocalStack
    log.warning('get_zipkin_attrs is deprecated. See DEPRECATIONS.rst for'
                'details on how to migrate to using Tracer.')
    return ThreadLocalStack().get()

def pop_zipkin_attrs():
    """Pop the topmost level zipkin attributes, if present.

    .. deprecated::
       Use the Tracer interface which offers better multi-threading support.
       pop_zipkin_attrs will be removed in version 1.0.

    :returns: tuple containing zipkin attrs
    :rtype: :class:`zipkin.ZipkinAttrs`
    """
    from py_zipkin.storage import ThreadLocalStack
    log.warning('pop_zipkin_attrs is deprecated. See DEPRECATIONS.rst for'
                'details on how to migrate to using Tracer.')
    return ThreadLocalStack().pop()

def push_zipkin_attrs(zipkin_attr):
    """Stores the zipkin attributes to thread local.

    .. deprecated::
       Use the Tracer interface which offers better multi-threading support.
       push_zipkin_attrs will be removed in version 1.0.

    :param zipkin_attr: tuple containing zipkin related attrs
    :type zipkin_attr: :class:`zipkin.ZipkinAttrs`
    """
    from py_zipkin.storage import ThreadLocalStack
    log.warning('push_zipkin_attrs is deprecated. See DEPRECATIONS.rst for'
                'details on how to migrate to using Tracer.')
    return ThreadLocalStack().push(zipkin_attr)


# -*- coding: utf-8 -*-
import random
import struct
import time

def generate_random_64bit_string():
    """Returns a 64 bit UTF-8 encoded string. In the interests of simplicity,
    this is always cast to a `str` instead of (in py2 land) a unicode string.
    Certain clients (I'm looking at you, Twisted) don't enjoy unicode headers.

    :returns: random 16-character string
    """
    return '{:016x}'.format(random.getrandbits(64))

def generate_random_128bit_string():
    """Returns a 128 bit UTF-8 encoded string. Follows the same conventions
    as generate_random_64bit_string().

    The upper 32 bits are the current time in epoch seconds, and the
    lower 96 bits are random. This allows for AWS X-Ray `interop
    <https://github.com/openzipkin/zipkin/issues/1754>`_

    :returns: 32-character hex string
    """
    t = int(time.time())
    lower_96 = random.getrandbits(96)
    return '{:032x}'.format((t << 96) | lower_96)

def unsigned_hex_to_signed_int(hex_string):
    """Converts a 64-bit hex string to a signed int value.

    This is due to the fact that Apache Thrift only has signed values.

    Examples:
        '17133d482ba4f605' => 1662740067609015813
        'b6dbb1c2b362bf51' => -5270423489115668655

    :param hex_string: the string representation of a zipkin ID
    :returns: signed int representation
    """
    return struct.unpack('q', struct.pack('Q', int(hex_string, 16)))[0]

def signed_int_to_unsigned_hex(signed_int):
    """Converts a signed int value to a 64-bit hex string.

    Examples:
        1662740067609015813  => '17133d482ba4f605'
        -5270423489115668655 => 'b6dbb1c2b362bf51'

    :param signed_int: an int to convert
    :returns: unsigned hex string
    """
    hex_string = hex(struct.unpack('Q', struct.pack('q', signed_int))[0])[2:]
    if hex_string.endswith('L'):
        return hex_string[:-1]
    return hex_string


# -*- coding: utf-8 -*-
import json

from py_zipkin import thrift
from py_zipkin.encoding import protobuf
from py_zipkin.encoding._types import Encoding
from py_zipkin.encoding._types import Kind
from py_zipkin.exception import ZipkinError

def get_encoder(encoding):
    """Creates encoder object for the given encoding.

    :param encoding: desired output encoding protocol.
    :type encoding: Encoding
    :return: corresponding IEncoder object
    :rtype: IEncoder
    """
    if encoding == Encoding.V1_THRIFT:
        return _V1ThriftEncoder()
    if encoding == Encoding.V1_JSON:
        return _V1JSONEncoder()
    if encoding == Encoding.V2_JSON:
        return _V2JSONEncoder()
    if encoding == Encoding.V2_PROTO3:
        return _V2ProtobufEncoder()
    raise ZipkinError('Unknown encoding: {}'.format(encoding))

class IEncoder(object):
    """Encoder interface."""

    def fits(self, current_count, current_size, max_size, new_span):
        """Returns whether the new span will fit in the list.

        :param current_count: number of spans already in the list.
        :type current_count: int
        :param current_size: sum of the sizes of all the spans already in the list.
        :type current_size: int
        :param max_size: max supported transport payload size.
        :type max_size: int
        :param new_span: encoded span object that we want to add the the list.
        :type new_span: str or bytes
        :return: True if the new span can be added to the list, False otherwise.
        :rtype: bool
        """
        raise NotImplementedError()

    def encode_span(self, span):
        """Encodes a single span.

        :param span: Span object representing the span.
        :type span: Span
        :return: encoded span.
        :rtype: str or bytes
        """
        raise NotImplementedError()

    def encode_queue(self, queue):
        """Encodes a list of pre-encoded spans.

        :param queue: list of encoded spans.
        :type queue: list
        :return: encoded list, type depends on the encoding.
        :rtype: str or bytes
        """
        raise NotImplementedError()

class _V1ThriftEncoder(IEncoder):
    """Thrift encoder for V1 spans."""

    def fits(self, current_count, current_size, max_size, new_span):
        """Checks if the new span fits in the max payload size.

        Thrift lists have a fixed-size header and no delimiters between elements
        so it's easy to compute the list size.
        """
        return thrift.LIST_HEADER_SIZE + current_size + len(new_span) <= max_size

    def encode_remote_endpoint(self, remote_endpoint, kind, binary_annotations):
        thrift_remote_endpoint = thrift.create_endpoint(
            remote_endpoint.port,
            remote_endpoint.service_name,
            remote_endpoint.ipv4,
            remote_endpoint.ipv6,
        )
        if kind == Kind.CLIENT:
            key = thrift.zipkin_core.SERVER_ADDR
        elif kind == Kind.SERVER:
            key = thrift.zipkin_core.CLIENT_ADDR

        binary_annotations.append(thrift.create_binary_annotation(
            key=key,
            value=thrift.SERVER_ADDR_VAL,
            annotation_type=thrift.zipkin_core.AnnotationType.BOOL,
            host=thrift_remote_endpoint,
        ))

    def encode_span(self, v2_span):
        """Encodes the current span to thrift."""
        span = v2_span.build_v1_span()

        thrift_endpoint = thrift.create_endpoint(
            span.endpoint.port,
            span.endpoint.service_name,
            span.endpoint.ipv4,
            span.endpoint.ipv6,
        )

        thrift_annotations = thrift.annotation_list_builder(
            span.annotations,
            thrift_endpoint,
        )

        thrift_binary_annotations = thrift.binary_annotation_list_builder(
            span.binary_annotations,
            thrift_endpoint,
        )

        # Add sa/ca binary annotations
        if v2_span.remote_endpoint:
            self.encode_remote_endpoint(
                v2_span.remote_endpoint,
                v2_span.kind,
                thrift_binary_annotations,
            )

        thrift_span = thrift.create_span(
            span.id,
            span.parent_id,
            span.trace_id,
            span.name,
            thrift_annotations,
            thrift_binary_annotations,
            span.timestamp,
            span.duration,
        )

        encoded_span = thrift.span_to_bytes(thrift_span)
        return encoded_span

    def encode_queue(self, queue):
        """Converts the queue to a thrift list"""
        return thrift.encode_bytes_list(queue)

class _BaseJSONEncoder(IEncoder):
    """ V1 and V2 JSON encoders need many common helper functions """

    def fits(self, current_count, current_size, max_size, new_span):
        """Checks if the new span fits in the max payload size.

        Json lists only have a 2 bytes overhead from '[]' plus 1 byte from
        ',' between elements
        """
        return 2 + current_count + current_size + len(new_span) <= max_size

    def _create_json_endpoint(self, endpoint, is_v1):
        """Converts an Endpoint to a JSON endpoint dict.

        :param endpoint: endpoint object to convert.
        :type endpoint: Endpoint
        :param is_v1: whether we're serializing a v1 span. This is needed since
            in v1 some fields default to an empty string rather than being
            dropped if they're not set.
        :type is_v1: bool
        :return: dict representing a JSON endpoint.
        :rtype: dict
        """
        json_endpoint = {}

        if endpoint.service_name:
            json_endpoint['serviceName'] = endpoint.service_name
        elif is_v1:
            # serviceName is mandatory in v1
            json_endpoint['serviceName'] = ""
        if endpoint.port and endpoint.port != 0:
            json_endpoint['port'] = endpoint.port
        if endpoint.ipv4 is not None:
            json_endpoint['ipv4'] = endpoint.ipv4
        if endpoint.ipv6 is not None:
            json_endpoint['ipv6'] = endpoint.ipv6

        return json_endpoint

    def encode_queue(self, queue):
        """Concatenates the list to a JSON list"""
        return '[' + ','.join(queue) + ']'

class _V1JSONEncoder(_BaseJSONEncoder):
    """JSON encoder for V1 spans."""

    def encode_remote_endpoint(self, remote_endpoint, kind, binary_annotations):
        json_remote_endpoint = self._create_json_endpoint(remote_endpoint, True)
        if kind == Kind.CLIENT:
            key = 'sa'
        elif kind == Kind.SERVER:
            key = 'ca'

        binary_annotations.append({
            'key': key,
            'value': True,
            'endpoint': json_remote_endpoint,
        })

    def encode_span(self, v2_span):
        """Encodes a single span to JSON."""
        span = v2_span.build_v1_span()

        json_span = {
            'traceId': span.trace_id,
            'name': span.name,
            'id': span.id,
            'annotations': [],
            'binaryAnnotations': [],
        }

        if span.parent_id:
            json_span['parentId'] = span.parent_id
        if span.timestamp:
            json_span['timestamp'] = int(span.timestamp * 1000000)
        if span.duration:
            json_span['duration'] = int(span.duration * 1000000)

        v1_endpoint = self._create_json_endpoint(span.endpoint, True)

        for key, timestamp in span.annotations.items():
            json_span['annotations'].append({
                'endpoint': v1_endpoint,
                'timestamp': int(timestamp * 1000000),
                'value': key,
            })

        for key, value in span.binary_annotations.items():
            json_span['binaryAnnotations'].append({
                'key': key,
                'value': value,
                'endpoint': v1_endpoint,
            })

        # Add sa/ca binary annotations
        if v2_span.remote_endpoint:
            self.encode_remote_endpoint(
                v2_span.remote_endpoint,
                v2_span.kind,
                json_span['binaryAnnotations'],
            )

        encoded_span = json.dumps(json_span)

        return encoded_span

class _V2JSONEncoder(_BaseJSONEncoder):
    """JSON encoder for V2 spans."""

    def encode_span(self, span):
        """Encodes a single span to JSON."""

        json_span = {
            'traceId': span.trace_id,
            'id': span.span_id,
        }

        if span.name:
            json_span['name'] = span.name
        if span.parent_id:
            json_span['parentId'] = span.parent_id
        if span.timestamp:
            json_span['timestamp'] = int(span.timestamp * 1000000)
        if span.duration:
            json_span['duration'] = int(span.duration * 1000000)
        if span.shared is True:
            json_span['shared'] = True
        if span.kind and span.kind.value is not None:
            json_span['kind'] = span.kind.value
        if span.local_endpoint:
            json_span['localEndpoint'] = self._create_json_endpoint(
                span.local_endpoint,
                False,
            )
        if span.remote_endpoint:
            json_span['remoteEndpoint'] = self._create_json_endpoint(
                span.remote_endpoint,
                False,
            )
        if span.tags and len(span.tags) > 0:
            json_span['tags'] = span.tags

        if span.annotations:
            json_span['annotations'] = [
                {
                    'timestamp': int(timestamp * 1000000),
                    'value': key,
                }
                for key, timestamp in span.annotations.items()
            ]

        encoded_span = json.dumps(json_span)

        return encoded_span

class _V2ProtobufEncoder(IEncoder):
    """Protobuf encoder for V2 spans."""

    def fits(self, current_count, current_size, max_size, new_span):
        """Checks if the new span fits in the max payload size."""
        return current_size + len(new_span) <= max_size

    def encode_span(self, span):
        """Encodes a single span to protobuf."""
        if not protobuf.installed():
            raise ZipkinError(
                'protobuf encoding requires installing the protobuf\'s extra '
                'requirements. Use py-zipkin[protobuf] in your requirements.txt.'
            )

        pb_span = protobuf.create_protobuf_span(span)
        return protobuf.encode_pb_list([pb_span])

    def encode_queue(self, queue):
        """Concatenates the list to a protobuf list and encodes it to bytes"""
        return b''.join(queue)


# -*- coding: utf-8 -*-
import logging
import socket
import struct

import six
from thriftpy2.protocol.binary import read_list_begin
from thriftpy2.protocol.binary import TBinaryProtocol
from thriftpy2.thrift import TType
from thriftpy2.transport import TMemoryBuffer

from py_zipkin.encoding._helpers import Endpoint
from py_zipkin.encoding._helpers import Span
from py_zipkin.encoding._types import Encoding
from py_zipkin.encoding._types import Kind
from py_zipkin.exception import ZipkinError
from py_zipkin.thrift import zipkin_core

_HEX_DIGITS = "0123456789abcdef"
_DROP_ANNOTATIONS = {'cs', 'sr', 'ss', 'cr'}

log = logging.getLogger('py_zipkin.encoding')

def get_decoder(encoding):
    """Creates encoder object for the given encoding.

    :param encoding: desired output encoding protocol
    :type encoding: Encoding
    :return: corresponding IEncoder object
    :rtype: IEncoder
    """
    if encoding == Encoding.V1_THRIFT:
        return _V1ThriftDecoder()
    if encoding == Encoding.V1_JSON:
        raise NotImplementedError(
            '{} decoding not yet implemented'.format(encoding))
    if encoding == Encoding.V2_JSON:
        raise NotImplementedError(
            '{} decoding not yet implemented'.format(encoding))
    raise ZipkinError('Unknown encoding: {}'.format(encoding))

class IDecoder(object):
    """Decoder interface."""

    def decode_spans(self, spans):
        """Decodes an encoded list of spans.

        :param spans: encoded list of spans
        :type spans: bytes
        :return: list of spans
        :rtype: list of Span
        """
        raise NotImplementedError()

class _V1ThriftDecoder(IDecoder):

    def decode_spans(self, spans):
        """Decodes an encoded list of spans.

        :param spans: encoded list of spans
        :type spans: bytes
        :return: list of spans
        :rtype: list of Span
        """
        decoded_spans = []
        transport = TMemoryBuffer(spans)

        if six.byte2int(spans) == TType.STRUCT:
            _, size = read_list_begin(transport)
        else:
            size = 1

        for _ in range(size):
            span = zipkin_core.Span()
            span.read(TBinaryProtocol(transport))
            decoded_spans.append(self._decode_thrift_span(span))
        return decoded_spans

    def _convert_from_thrift_endpoint(self, thrift_endpoint):
        """Accepts a thrift decoded endpoint and converts it to an Endpoint.

        :param thrift_endpoint: thrift encoded endpoint
        :type thrift_endpoint: thrift endpoint
        :returns: decoded endpoint
        :rtype: Encoding
        """
        ipv4 = None
        ipv6 = None
        port = struct.unpack('H', struct.pack('h', thrift_endpoint.port))[0]

        if thrift_endpoint.ipv4 != 0:
            ipv4 = socket.inet_ntop(
                socket.AF_INET,
                struct.pack('!i', thrift_endpoint.ipv4),
            )

        if thrift_endpoint.ipv6:
            ipv6 = socket.inet_ntop(socket.AF_INET6, thrift_endpoint.ipv6)

        return Endpoint(
            service_name=thrift_endpoint.service_name,
            ipv4=ipv4,
            ipv6=ipv6,
            port=port,
        )

    def _decode_thrift_annotations(self, thrift_annotations):
        """Accepts a thrift annotation and converts it to a v1 annotation.

        :param thrift_annotations: list of thrift annotations.
        :type thrift_annotations: list of zipkin_core.Span.Annotation
        :returns: (annotations, local_endpoint, kind)
        """
        local_endpoint = None
        kind = Kind.LOCAL
        all_annotations = {}
        timestamp = None
        duration = None

        for thrift_annotation in thrift_annotations:
            all_annotations[thrift_annotation.value] = thrift_annotation.timestamp
            if thrift_annotation.host:
                local_endpoint = self._convert_from_thrift_endpoint(
                    thrift_annotation.host,
                )

        if 'cs' in all_annotations and 'sr' not in all_annotations:
            kind = Kind.CLIENT
            timestamp = all_annotations['cs']
            duration = all_annotations['cr'] - all_annotations['cs']
        elif 'cs' not in all_annotations and 'sr' in all_annotations:
            kind = Kind.SERVER
            timestamp = all_annotations['sr']
            duration = all_annotations['ss'] - all_annotations['sr']

        annotations = {
            name: self.seconds(ts) for name, ts in all_annotations.items()
            if name not in _DROP_ANNOTATIONS
        }

        return annotations, local_endpoint, kind, timestamp, duration

    def _convert_from_thrift_binary_annotations(self, thrift_binary_annotations):
        """Accepts a thrift decoded binary annotation and converts it
        to a v1 binary annotation.
        """
        tags = {}
        local_endpoint = None
        remote_endpoint = None

        for binary_annotation in thrift_binary_annotations:
            if binary_annotation.key == 'sa':
                remote_endpoint = self._convert_from_thrift_endpoint(
                    thrift_endpoint=binary_annotation.host,
                )
            else:
                key = binary_annotation.key

                annotation_type = binary_annotation.annotation_type
                value = binary_annotation.value

                if annotation_type == zipkin_core.AnnotationType.BOOL:
                    tags[key] = "true" if value == 1 else "false"
                elif annotation_type == zipkin_core.AnnotationType.STRING:
                    tags[key] = str(value)
                else:
                    log.warning('Only STRING and BOOL binary annotations are '
                                'supported right now and can be properly decoded.')

                if binary_annotation.host:
                    local_endpoint = self._convert_from_thrift_endpoint(
                        thrift_endpoint=binary_annotation.host,
                    )

        return tags, local_endpoint, remote_endpoint

    def seconds(self, us):
        if us is None:
            return None
        return round(float(us) / 1000 / 1000, 6)

    def _decode_thrift_span(self, thrift_span):
        """Decodes a thrift span.

        :param thrift_span: thrift span
        :type thrift_span: thrift Span object
        :returns: span builder representing this span
        :rtype: Span
        """
        parent_id = None
        local_endpoint = None
        annotations = {}
        tags = {}
        kind = Kind.LOCAL
        remote_endpoint = None
        timestamp = None
        duration = None

        if thrift_span.parent_id:
            parent_id = self._convert_unsigned_long_to_lower_hex(
                thrift_span.parent_id,
            )

        if thrift_span.annotations:
            annotations, local_endpoint, kind, timestamp, duration = \
                self._decode_thrift_annotations(thrift_span.annotations)

        if thrift_span.binary_annotations:
            tags, local_endpoint, remote_endpoint = \
                self._convert_from_thrift_binary_annotations(
                    thrift_span.binary_annotations,
                )

        trace_id = self._convert_trace_id_to_string(
            thrift_span.trace_id,
            thrift_span.trace_id_high,
        )

        return Span(
            trace_id=trace_id,
            name=thrift_span.name,
            parent_id=parent_id,
            span_id=self._convert_unsigned_long_to_lower_hex(thrift_span.id),
            kind=kind,
            timestamp=self.seconds(timestamp or thrift_span.timestamp),
            duration=self.seconds(duration or thrift_span.duration),
            local_endpoint=local_endpoint,
            remote_endpoint=remote_endpoint,
            shared=(kind == Kind.SERVER and thrift_span.timestamp is None),
            annotations=annotations,
            tags=tags,
        )

    def _convert_trace_id_to_string(self, trace_id, trace_id_high=None):
        """
        Converts the provided traceId hex value with optional high bits
        to a string.

        :param trace_id: the value of the trace ID
        :type trace_id: int
        :param trace_id_high: the high bits of the trace ID
        :type trace_id: int
        :returns: trace_id_high + trace_id as a string
        """
        if trace_id_high is not None:
            result = bytearray(32)
            self._write_hex_long(result, 0, trace_id_high)
            self._write_hex_long(result, 16, trace_id)
            return result.decode("utf8")

        result = bytearray(16)
        self._write_hex_long(result, 0, trace_id)
        return result.decode("utf8")

    def _convert_unsigned_long_to_lower_hex(self, value):
        """
        Converts the provided unsigned long value to a hex string.

        :param value: the value to convert
        :type value: unsigned long
        :returns: value as a hex string
        """
        result = bytearray(16)
        self._write_hex_long(result, 0, value)
        return result.decode("utf8")

    def _write_hex_long(self, data, pos, value):
        """
        Writes an unsigned long value across a byte array.

        :param data: the buffer to write the value to
        :type data: bytearray
        :param pos: the starting position
        :type pos: int
        :param value: the value to write
        :type value: unsigned long
        """
        self._write_hex_byte(data, pos + 0, (value >> 56) & 0xff)
        self._write_hex_byte(data, pos + 2, (value >> 48) & 0xff)
        self._write_hex_byte(data, pos + 4, (value >> 40) & 0xff)
        self._write_hex_byte(data, pos + 6, (value >> 32) & 0xff)
        self._write_hex_byte(data, pos + 8, (value >> 24) & 0xff)
        self._write_hex_byte(data, pos + 10, (value >> 16) & 0xff)
        self._write_hex_byte(data, pos + 12, (value >> 8) & 0xff)
        self._write_hex_byte(data, pos + 14, (value & 0xff))

    def _write_hex_byte(self, data, pos, byte):
        data[pos + 0] = ord(_HEX_DIGITS[int((byte >> 4) & 0xf)])
        data[pos + 1] = ord(_HEX_DIGITS[int(byte & 0xf)])


# encoding=utf-8
import re
import calendar
import collections

def add_currency_pre_processor(currency, overwrite=True):
    def _add_currency_pre_processor(transactions, tag, tag_dict, *args):
        if 'currency' not in tag_dict or overwrite:  # pragma: no branch
            tag_dict['currency'] = currency

        return tag_dict

    return _add_currency_pre_processor

def date_fixup_pre_processor(transactions, tag, tag_dict, *args):
    """
    Replace illegal February 29, 30 dates with the last day of February.

    German banks use a variant of the 30/360 interest rate calculation,
    where each month has always 30 days even February. Python's datetime
    module won't accept such dates.
    """
    if tag_dict['month'] == '02':
        year = int(tag_dict['year'], 10)
        _, max_month_day = calendar.monthrange(year, 2)
        if int(tag_dict['day'], 10) > max_month_day:
            tag_dict['day'] = str(max_month_day)

    return tag_dict

def date_cleanup_post_processor(transactions, tag, tag_dict, result):
    for k in ('day', 'month', 'year', 'entry_day', 'entry_month'):
        result.pop(k, None)

    return result

def mBank_set_transaction_code(transactions, tag, tag_dict, *args):
    """
    mBank Collect uses transaction code 911 to distinguish icoming mass
    payments transactions, adding transaction_code may be helpful in further
    processing
    """
    tag_dict['transaction_code'] = int(
        tag_dict[tag.slug].split(';')[0].split(' ', 1)[0])

    return tag_dict

iph_id_re = re.compile(r' ID IPH: X*(?P<iph_id>\d{0,14});')

def mBank_set_iph_id(transactions, tag, tag_dict, *args):
    """
    mBank Collect uses ID IPH to distinguish between virtual accounts,
    adding iph_id may be helpful in further processing
    """
    matches = iph_id_re.search(tag_dict[tag.slug])

    if matches:  # pragma no branch
        tag_dict['iph_id'] = matches.groupdict()['iph_id']

    return tag_dict

tnr_re = re.compile(r'TNR:[ \n](?P<tnr>\d+\.\d+)',
                    flags=re.MULTILINE | re.UNICODE)

def mBank_set_tnr(transactions, tag, tag_dict, *args):
    """
    mBank Collect states TNR in transaction details as unique id for
    transactions, that may be used to identify the same transactions in
    different statement files eg. partial mt942 and full mt940
    Information about tnr uniqueness has been obtained from mBank support,
    it lacks in mt940 mBank specification.
    """

    matches = tnr_re.search(tag_dict[tag.slug])

    if matches:  # pragma no branch
        tag_dict['tnr'] = matches.groupdict()['tnr']

    return tag_dict

# https://www.db-bankline.deutsche-bank.com/download/MT940_Deutschland_Structure2002.pdf
DETAIL_KEYS = {
    '': 'transaction_code',
    '00': 'posting_text',
    '10': 'prima_nota',
    '20': 'purpose',
    '30': 'applicant_bin',
    '31': 'applicant_iban',
    '32': 'applicant_name',
    '34': 'return_debit_notes',
    '35': 'recipient_name',
    '60': 'additional_purpose',
}

# https://www.hettwer-beratung.de/sepa-spezialwissen/sepa-technische-anforderungen/sepa-gesch%C3%A4ftsvorfallcodes-gvc-mt-940/
GVC_KEYS = {
    '': 'purpose',
    'IBAN': 'gvc_applicant_iban',
    'BIC ': 'gvc_applicant_bin',
    'EREF': 'end_to_end_reference',
    'MREF': 'additional_position_reference',
    'CRED': 'applicant_creditor_id',
    'PURP': 'purpose_code',
    'SVWZ': 'purpose',
    'MDAT': 'additional_position_date',
    'ABWA': 'deviate_applicant',
    'ABWE': 'deviate_recipient',
    'SQTP': 'FRST_ONE_OFF_RECC',
    'ORCR': 'old_SEPA_CI',
    'ORMR': 'old_SEPA_additional_position_reference',
    'DDAT': 'settlement_tag',
    'KREF': 'customer_reference',
    'DEBT': 'debitor_identifier',
    'COAM': 'compensation_amount',
    'OAMT': 'original_amount',
}

def _parse_mt940_details(detail_str):
    result = dict.fromkeys(DETAIL_KEYS.values())

    tmp = collections.OrderedDict()
    segment = ''
    segment_type = ''

    for index, char in enumerate(detail_str):
        if char != '?':
            segment += char

            continue
        tmp[segment_type] = segment if not segment_type else segment[2:]
        segment_type = detail_str[index + 1] + detail_str[index + 2]
        segment = ''

    if segment_type:  # pragma: no branch
        tmp[segment_type] = segment if not segment_type else segment[2:]

    for key, value in tmp.items():
        if key in DETAIL_KEYS:
            result[DETAIL_KEYS[key]] = value
        elif key == '33':
            key32 = DETAIL_KEYS['32']
            result[key32] = (result[key32] or '') + value
        elif key.startswith('2'):
            key20 = DETAIL_KEYS['20']
            result[key20] = (result[key20] or '') + value
        elif key in ('61', '62', '63'):
            key60 = DETAIL_KEYS['60']
            result[key60] = (result[key60] or '') + value

    return result

def _parse_mt940_gvcodes(purpose):
    result = {}

    for key, value in GVC_KEYS.items():
        result[value] = None

    tmp = {}
    segment_type = None
    text = ''

    for index, char in enumerate(purpose):
        if char == '+' and purpose[index - 4:index] in GVC_KEYS:
            if segment_type:
                tmp[segment_type] = text[:-4]
                text = ''
            else:
                text = ''
            segment_type = purpose[index - 4:index]
        else:
            text += char

    if segment_type:  # pragma: no branch
        tmp[segment_type] = text
    else:
        tmp[''] = text  # pragma: no cover

    for key, value in tmp.items():
        result[GVC_KEYS[key]] = value

    return result

def transaction_details_post_processor(transactions, tag, tag_dict, result):
    details = tag_dict['transaction_details']
    details = ''.join(detail.strip('\n\r') for detail in details.splitlines())

    # check for e.g. 103?00...
    if re.match(r'^\d{3}\?\d{2}', details):
        result.update(_parse_mt940_details(details))

        purpose = result.get('purpose')

        if purpose and purpose[:4] in GVC_KEYS:  # pragma: no branch
            result.update(_parse_mt940_gvcodes(result['purpose']))

        del result['transaction_details']

    return result


import re
import decimal
import datetime

# python 3.8+ compatibility
try:  # pragma: no cover
    from collections import abc
except ImportError:  # pragma: no cover
    import collections as abc

import mt940

from . import _compat
from . import processors

class Model(object):
    pass

class FixedOffset(datetime.tzinfo):
    '''Fixed time offset based on the Python docs
    Source: https://docs.python.org/2/library/datetime.html#tzinfo-objects

    >>> offset = FixedOffset(60)
    >>> offset.utcoffset(None).total_seconds()
    3600.0
    >>> offset.dst(None)
    datetime.timedelta(0)
    >>> offset.tzname(None)
    '60'
    '''

    def __init__(self, offset=0, name=None):
        self._name = name or str(offset)

        if not isinstance(offset, int):
            offset = int(offset)
        self._offset = datetime.timedelta(minutes=offset)

    def utcoffset(self, dt):
        return self._offset

    def dst(self, dt):
        return datetime.timedelta(0)

    def tzname(self, dt):
        return self._name

class DateTime(datetime.datetime, Model):
    '''Just a regular datetime object which supports dates given as strings

    >>> DateTime(year='2000', month='1', day='2', hour='3', minute='4',
    ...          second='5', microsecond='6')
    DateTime(2000, 1, 2, 3, 4, 5, 6)

    >>> DateTime(year='123', month='1', day='2', hour='3', minute='4',
    ...          second='5', microsecond='6')
    DateTime(2123, 1, 2, 3, 4, 5, 6)

    >>> DateTime(2000, 1, 2, 3, 4, 5, 6)
    DateTime(2000, 1, 2, 3, 4, 5, 6)

    >>> DateTime(year='123', month='1', day='2', hour='3', minute='4',
    ...          second='5', microsecond='6', tzinfo=FixedOffset('60'))
    DateTime(2123, 1, 2, 3, 4, 5, 6, tzinfo=<mt940.models.FixedOffset ...>)

    Args:
        year (str): Year (0-100), will automatically add 2000 when needed
        month (str): Month
        day (str): Day
        hour (str): Hour
        minute (str): Minute
        second (str): Second
        microsecond (str): Microsecond
        tzinfo (tzinfo): Timezone information. Overwrites `offset`
        offset (str): Timezone offset in minutes, generates a tzinfo object
                      with the given offset if no tzinfo is available.
    '''

    def __new__(cls, *args, **kwargs):
        if kwargs:
            values = dict(
                year=None,
                month=None,
                day=None,
                hour='0',
                minute='0',
                second='0',
                microsecond='0', )

            # The list makes sure this works in both Python 2 and 3
            for key, default in list(values.items()):
                # Fetch the value or the default
                value = kwargs.get(key, default)
                assert value is not None, '%s should not be None' % key
                # Convert the value to integer and force base 10 to make sure
                # it doesn't get recognized as octal
                if not isinstance(value, int):
                    value = int(value, 10)

                # Save the values again
                values[key] = value

            if values['year'] < 1000:
                values['year'] += 2000

            values['tzinfo'] = None

            if kwargs.get('tzinfo'):
                values['tzinfo'] = kwargs['tzinfo']

            if kwargs.get('offset'):
                values['tzinfo'] = FixedOffset(kwargs['offset'])

            return datetime.datetime.__new__(cls, **values)
        else:
            return datetime.datetime.__new__(cls, *args, **kwargs)

class Date(datetime.date, Model):
    '''Just a regular date object which supports dates given as strings

    >>> Date(year='2000', month='1', day='2')
    Date(2000, 1, 2)

    >>> Date(year='123', month='1', day='2')
    Date(2123, 1, 2)

    Args:
        year (str): Year (0-100), will automatically add 2000 when needed
        month (str): Month
        day (str): Day
    '''

    def __new__(cls, *args, **kwargs):
        if kwargs:
            dt = DateTime(*args, **kwargs).date()

            return datetime.date.__new__(cls, dt.year, dt.month, dt.day)
        else:
            return datetime.date.__new__(cls, *args, **kwargs)

class Amount(Model):
    '''Amount object containing currency and amount

    Args:
        amount (str): Amount using either a , or a . as decimal separator
        status (str): Either C or D for credit or debit respectively
        currency (str): A 3 letter currency (e.g. EUR)

    >>> Amount('123.45', 'C', 'EUR')
    <123.45 EUR>
    >>> Amount('123.45', 'D', 'EUR')
    <-123.45 EUR>
    '''

    def __init__(self, amount, status, currency=None, **kwargs):
        self.amount = decimal.Decimal(amount.replace(',', '.'))
        self.currency = currency

        # C = credit, D = debit

        if status == 'D':
            self.amount = -self.amount

    def __repr__(self):
        return '<%s %s>' % (
            self.amount,
            self.currency, )

class SumAmount(Amount):
    def __init__(self, *args, **kwargs):
        number = kwargs.pop('number')
        super(SumAmount, self).__init__(*args, **kwargs)
        self.number = number

    def __repr__(self):
        return '<%s %s in %s stmts)>' % (self.amount, self.currency,
                                         self.number)

class Balance(Model):
    '''Parse balance statement

    Args:
        status (str): Either C or D for credit or debit respectively
        amount (Amount): Object containing the amount and currency
        date (date): The balance date

    >>> balance = Balance('C', '0.00', Date(2010, 7, 22))
    >>> balance.status
    'C'
    >>> balance.amount.amount
    Decimal('0.00')
    >>> isinstance(balance.date, Date)
    True
    >>> balance.date.year, balance.date.month, balance.date.day
    (2010, 7, 22)

    >>> Balance()
    <None @ None>
    '''

    def __init__(self, status=None, amount=None, date=None, **kwargs):
        if amount and not isinstance(amount, Amount):
            amount = Amount(amount, status, kwargs.get('currency'))
        self.status = status
        self.amount = amount
        self.date = date

    def __repr__(self):
        return '<%s>' % self

    def __str__(self):
        return '%s @ %s' % (
            self.amount,
            self.date, )

class Transactions(abc.Sequence):
    '''
    Collection of :py:class:`Transaction` objects with global properties such
    as begin and end balance

    '''

    #: Using the processors you can pre-process data before creating objects
    #: and modify them after creating the objects
    DEFAULT_PROCESSORS = dict(
        pre_account_identification=[],
        post_account_identification=[],
        pre_available_balance=[],
        post_available_balance=[],
        pre_closing_balance=[],
        post_closing_balance=[],
        pre_intermediate_closing_balance=[],
        post_intermediate_closing_balance=[],
        pre_final_closing_balance=[],
        post_final_closing_balance=[],
        pre_forward_available_balance=[],
        post_forward_available_balance=[],
        pre_opening_balance=[],
        post_opening_balance=[],
        pre_intermediate_opening_balance=[],
        post_intermediate_opening_balance=[],
        pre_final_opening_balance=[],
        post_final_opening_balance=[],
        pre_related_reference=[],
        post_related_reference=[],
        pre_statement=[processors.date_fixup_pre_processor],
        post_statement=[processors.date_cleanup_post_processor],
        pre_statement_number=[],
        post_statement_number=[],
        pre_non_swift=[],
        post_non_swift=[],
        pre_transaction_details=[],
        post_transaction_details=[
            processors.transaction_details_post_processor
        ],
        pre_transaction_reference_number=[],
        post_transaction_reference_number=[],
        pre_floor_limit_indicator=[],
        post_floor_limit_indicator=[],
        pre_date_time_indication=[],
        post_date_time_indication=[],
        pre_sum_credit_entries=[],
        post_sum_credit_entries=[],
        pre_sum_debit_entries=[],
        post_sum_debit_entries=[])

    def __init__(self, processors=None, tags=None):
        self.processors = self.DEFAULT_PROCESSORS.copy()
        self.tags = Transactions.defaultTags().copy()

        if processors:
            self.processors.update(processors)
        if tags:
            self.tags.update(tags)

        self.transactions = []
        self.data = {}

    @property
    def currency(self):
        balance = mt940.utils.coalesce(
            self.data.get('final_opening_balance'),
            self.data.get('opening_balance'),
            self.data.get('intermediate_opening_balance'),
            self.data.get('available_balance'),
            self.data.get('forward_available_balance'),
            self.data.get('final_closing_balance'),
            self.data.get('closing_balance'),
            self.data.get('intermediate_closing_balance'),
            self.data.get('c_floor_limit'),
            self.data.get('d_floor_limit'), )

        if balance:
            if isinstance(balance, Amount):
                return balance.currency

            return balance.amount.currency

    @staticmethod
    def defaultTags():
        return mt940.tags.TAG_BY_ID

    @classmethod
    def strip(cls, lines):
        for line in lines:
            # We don't like carriage returns in case of Windows files so let's
            # just replace them with nothing
            line = line.replace('\r', '')

            # Strip trailing whitespace from lines since they cause incorrect
            # files
            line = line.rstrip()

            # Skip separators

            if line.strip() == '-':
                continue

            # Return actual lines

            if line:
                yield line

    @classmethod
    def normalize_tag_id(cls, tag_id):
        # Since non-digit tags exist, make the conversion optional
        if tag_id.isdigit():
            tag_id = int(tag_id)

        return tag_id

    def sanatize_tag_id_matches(self, matches):
        i_next = 0
        for i, match in enumerate(matches):
            # match was rejected
            if i < i_next:
                continue

            # next match would be
            i_next = i + 1

            # normalize tag id
            tag_id = self.normalize_tag_id(match.group('tag'))

            # tag should be known
            assert tag_id in self.tags, 'Unknown tag %r ' \
                'in line: %r' % (tag_id, match.group(0))

            # special treatment for long tag content with possible
            # bad line wrap which produces tag_id like line beginnings
            # seen with :86: tag
            if tag_id == mt940.tags.Tags.TRANSACTION_DETAILS.value.id:
                # search subsequent tags for unknown tag ids
                # these lines likely belong to the previous tag
                for j in range(i_next, len(matches)):
                    next_tag_id = self.normalize_tag_id(
                        matches[j].group('tag'))
                    if next_tag_id in self.tags:
                        # this one is the next valid match
                        i_next = j
                        break
                    # else reject match

            # a valid match
            yield match

    def parse(self, data):
        '''Parses mt940 data, expects a string with data

        Args:
            data (str): The MT940 data

        Returns: :py:class:`list` of :py:class:`Transaction`
        '''
        # Remove extraneous whitespace and such
        data = '\n'.join(self.strip(data.split('\n')))

        # The pattern is a bit annoying to match by regex, even with a greedy
        # match it's difficult to get both the beginning and the end so we're
        # working around it in a safer way to get everything.
        tag_re = re.compile(
            r'^:\n?(?P<full_tag>(?P<tag>[0-9]{2}|NS)(?P<sub_tag>[A-Z])?):',
            re.MULTILINE)
        matches = list(tag_re.finditer(data))

        # identify valid matches
        valid_matches = list(self.sanatize_tag_id_matches(matches))

        for i, match in enumerate(valid_matches):
            tag_id = self.normalize_tag_id(match.group('tag'))

            # get tag instance corresponding to tag id
            tag = self.tags.get(match.group('full_tag')) \
                or self.tags[tag_id]

            # Nice trick to get all the text that is part of this tag, python
            # regex matches have a `end()` and `start()` to indicate the start
            # and end index of the match.

            if valid_matches[i + 1:]:
                tag_data = \
                    data[match.end():valid_matches[i + 1].start()].strip()
            else:
                tag_data = data[match.end():].strip()

            tag_dict = tag.parse(self, tag_data)

            # Preprocess data before creating the object

            for processor in self.processors.get('pre_%s' % tag.slug, []):
                tag_dict = processor(self, tag, tag_dict)

            result = tag(self, tag_dict)

            # Postprocess the object

            for processor in self.processors.get('post_%s' % tag.slug, []):
                result = processor(self, tag, tag_dict, result)

            # Creating a new transaction for :20: and :61: tags allows the
            # tags from :20: to :61: to be captured as part of the transaction.

            if isinstance(tag, mt940.tags.Statement):
                # Transactions only get a Transaction Reference Code ID from a
                # :61: tag which is why a new transaction is created if the
                # 'id' has a value.

                if not self.transactions:
                    transaction = Transaction(self)
                    self.transactions.append(transaction)

                if transaction.data.get('id'):
                    transaction = Transaction(self, result)
                    self.transactions.append(transaction)
                else:
                    transaction.data.update(result)
            elif issubclass(tag.scope, Transaction) and self.transactions:
                # Combine multiple results together as one string, Rabobank has
                # multiple :86: tags for a single transaction

                for k, v in _compat.iteritems(result):
                    if k in transaction.data and hasattr(v, 'strip'):
                        transaction.data[k] += '\n%s' % v.strip()
                    else:
                        transaction.data[k] = v

            elif issubclass(tag.scope, Transactions):  # pragma: no branch
                self.data.update(result)

        return self.transactions

    def __getitem__(self, key):
        return self.transactions[key]

    def __len__(self):
        return len(self.transactions)

    def __repr__(self):
        return '<%s[%s]>' % (
            self.__class__.__name__,
            ']['.join('%s: %s' % (k.replace('_balance', ''), v)
                      for k, v in _compat.iteritems(self.data)
                      if k.endswith('balance')))

class Transaction(Model):
    def __init__(self, transactions, data=None):
        self.transactions = transactions
        self.data = {}
        self.update(data)

    def update(self, data):
        if data:
            self.data.update(data)

    def __repr__(self):
        return '<%s[%s] %s>' % (
            self.__class__.__name__,
            self.data.get('date'),
            self.data.get('amount'), )


# vim: fileencoding=utf-8:
'''

Format
---------------------

Sources:

.. _Swift for corporates: http://www.sepaforcorporates.com/\
    swift-for-corporates/account-statement-mt940-file-format-overview/
.. _Rabobank MT940: https://www.rabobank.nl/images/\
    formaatbeschrijving_swift_bt940s_1_0_nl_rib_29539296.pdf

 - `Swift for corporates`_
 - `Rabobank MT940`_

::

    [] = optional
    ! = fixed length
    a = Text
    x = Alphanumeric, seems more like text actually. Can include special
        characters (slashes) and whitespace as well as letters and numbers
    d = Numeric separated by decimal (usually comma)
    c = Code list value
    n = Numeric
'''

import os

import mt940

def parse(src, encoding=None):
    '''
    Parses mt940 data and returns transactions object

    :param src: file handler to read, filename to read or raw data as string
    :return: Collection of transactions
    :rtype: Transactions
    '''

    def safe_is_file(filename):
        try:
            return os.path.isfile(src)
        except ValueError:  # pragma: no cover
            return False

    if hasattr(src, 'read'):  # pragma: no branch
        data = src.read()
    elif safe_is_file(src):
        with open(src, 'rb') as fh:
            data = fh.read()
    else:  # pragma: no cover
        data = src

    if hasattr(data, 'decode'):  # pragma: no branch
        exception = None
        encodings = [encoding, 'utf-8', 'cp852', 'iso8859-15', 'latin1']

        for encoding in encodings:  # pragma: no cover
            if not encoding:
                continue

            try:
                data = data.decode(encoding)
                break
            except UnicodeDecodeError as e:
                exception = e
            except UnicodeEncodeError:
                break
        else:
            raise exception  # pragma: no cover

    transactions = mt940.models.Transactions()
    transactions.parse(data)

    return transactions


import enum

def coalesce(*args):
    '''
    Return the first non-None argument

    >>> coalesce()

    >>> coalesce(0, 1)
    0
    >>> coalesce(None, 0)
    0
    '''

    for arg in args:
        if arg is not None:
            return arg

class Strip(enum.IntEnum):
    NONE = 0
    LEFT = 1
    RIGHT = 2
    BOTH = 3

def join_lines(string, strip=Strip.BOTH):
    '''
    Join strings together and strip whitespace in between if needed
    '''
    lines = []

    for line in string.splitlines():
        if strip & Strip.RIGHT:
            line = line.rstrip()

        if strip & Strip.LEFT:
            line = line.lstrip()

        lines.append(line)

    return ''.join(lines)


# -*- coding: utf-8 -*-

"""
The MIT License (MIT)

Copyright (c) 2019 Francis Taylor

Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""

import asyncio
import os
import json
import logging
import sys
from datetime import datetime  # , timedelta
from ratelimiter import RateLimiter

import aiohttp

from . import __version__
from .errors import *

log = logging.getLogger(__name__)

async def json_or_text(response):
    """Turns response into a properly formatted json or text object"""
    text = await response.text()
    if response.headers['Content-Type'] == 'application/json; charset=utf-8':
        return json.loads(text)
    return text

class HTTPClient:
    """Represents an HTTP client sending HTTP requests to the DBL API.

    .. _aiohttp session: https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session

    Parameters
    ----------
    token :
        A DBL API Token
    **session : Optional[aiohttp session]
        The session used to request to the API
    **loop
    """

    def __init__(self, token, **kwargs):
        self.BASE = 'https://discordbots.org/api'
        self.token = token
        self.loop = asyncio.get_event_loop() if kwargs.get('loop') is None else kwargs.get('loop')
        self.session = kwargs.get('session') or aiohttp.ClientSession(loop=kwargs.get('loop'))
        self._global_over = asyncio.Event(loop=self.loop)
        self._global_over.set()

        user_agent = 'DBL-Python-Library (https://github.com/DiscordBotList/DBL-Python-Library {0}) Python/{1[0]}.{1[1]} aiohttp/{2}'
        self.user_agent = user_agent.format(__version__, sys.version_info, aiohttp.__version__)

# TODO: better implementation of ratelimits
# NOTE: current implementation doesn't maintain state over restart

    async def request(self, method, url, **kwargs):
        """Handles requests to the API"""
        rate_limiter = RateLimiter(max_calls=59, period=60, callback=limited)
        # handles ratelimits. max_calls is set to 59 because current implementation will retry in 60s after 60 calls is reached. DBL has a 1h block so obviously this doesn't work well, as it will get a 429 when 60 is reached.

        async with rate_limiter:  # this works but doesn't 'save' over restart. need a better implementation.

            if not self.token:
                raise UnauthorizedDetected('UnauthorizedDetected (status code: 401): No TOKEN provided')

            headers = {
                'User-Agent': self.user_agent,
                'Content-Type': 'application/json'
            }

            if 'json' in kwargs:
                kwargs['data'] = to_json(kwargs.pop('json'))

            kwargs['headers'] = headers
            headers['Authorization'] = self.token

            for tries in range(5):
                async with self.session.request(method, url, **kwargs) as resp:
                    log.debug('%s %s with %s has returned %s', method,
                              url, kwargs.get('data'), resp.status)

                    data = await json_or_text(resp)

                    if 300 > resp.status >= 200:
                        return data

                    if resp.status == 429:  # we are being ratelimited
                        fmt = 'We are being rate limited. Retrying in %.2f seconds (%.3f minutes).'

                        # sleep a bit
                        retry_after = json.loads(resp.headers.get('Retry-After'))
                        mins = retry_after / 60
                        log.warning(fmt, retry_after, mins)

                        # check if it's a global rate limit (True as only 1 ratelimit atm - /api/bots)
                        is_global = True  # is_global = data.get('global', False)
                        if is_global:
                            self._global_over.clear()

                        await asyncio.sleep(retry_after, loop=self.loop)
                        log.debug('Done sleeping for the rate limit. Retrying...')

                        # release the global lock now that the
                        # global rate limit has passed
                        if is_global:
                            self._global_over.set()
                            log.debug('Global rate limit is now over.')

                        continue

                    if resp.status == 400:
                        raise HTTPException(resp, data)
                    elif resp.status == 401:
                        raise Unauthorized(resp, data)
                    elif resp.status == 403:
                        raise Forbidden(resp, data)
                    elif resp.status == 404:
                        raise NotFound(resp, data)
                    else:
                        raise HTTPException(resp, data)
            # We've run out of retries, raise.
            raise HTTPException(resp, data)

    async def close(self):
        await self.session.close()

    async def recreate(self):
        self.session = aiohttp.ClientSession(loop=self.session.loop)

    async def post_guild_count(self, bot_id, guild_count, shard_count, shard_no):
        if shard_count:
            payload = {
                'server_count': guild_count,
                'shard_count': shard_count,
                'shard_no': shard_no
            }
        else:
            payload = {
                'server_count': guild_count
            }
        await self.request('POST', '{}/bots/{}/stats'.format(self.BASE, bot_id), json=payload)

    async def get_weekend_status(self):
        '''Gets the weekend status from DBL'''
        return await self.request('GET', '{}/weekend'.format(self.BASE))

    async def get_guild_count(self, bot_id):
        '''Gets the guild count of the given Bot ID'''
        return await self.request('GET', '{}/bots/{}/stats'.format(self.BASE, bot_id))

    async def get_bot_info(self, bot_id):
        '''Gets the information of the given Bot ID'''
        resp = await self.request('GET', '{}/bots/{}'.format(self.BASE, bot_id))
        resp['date'] = datetime.strptime(resp['date'], '%Y-%m-%dT%H:%M:%S.%fZ')
        for k in resp:
            if resp[k] == '':
                resp[k] = None
        return resp

    async def get_upvote_info(self, bot_id):
        '''Gets the last 1000 votes of the given Bot ID'''
        return await self.request('GET', '{}/bots/{}/votes'.format(self.BASE, bot_id))

    async def get_bots(self, limit, offset):
        '''Gets an object of bots on DBL'''
        if limit > 500:
            limit = 50
        return await self.request('GET', '{}/bots?limit={}&offset={}'.format(self.BASE, limit, offset))

    async def get_user_info(self, user_id):
        '''Gets an object of the user on DBL'''
        return await self.request('GET', '{}/users/{}'.format(self.BASE, user_id))

async def limited(until):
    """Handles the message shown when we are ratelimited"""
    duration = int(round(until - time.time()))
    mins = duration / 60
    fmt = 'We have exhausted a ratelimit quota. Retrying in %.2f seconds (%.3f minutes).'
    log.warn(fmt, duration, mins)

def to_json(obj):
    if json.__name__ == 'ujson':
        return json.dumps(obj, ensure_ascii=True)
    return json.dumps(obj, separators=(',', ':'), ensure_ascii=True)


# -*- coding: utf-8 -*-

"""
The MIT License (MIT)

Copyright (c) 2019 Francis Taylor

Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""

import asyncio
#import string
#import random
import logging

from . import __version__ as library_version
from .http import HTTPClient
#from .errors import InvalidArgument

log = logging.getLogger(__name__)

class Client:
    """Represents a client connection that connects to discordbots.org.
    This class is used to interact with the DBL API.

    .. _event loop: https://docs.python.org/3/library/asyncio-eventloops.html
    .. _aiohttp session: https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session

    Parameters
    ==========

    token :
        An API Token

    bot :
        An instance of a discord.py Bot or Client object
    **loop : Optional[event loop].
        The `event loop`_ to use for asynchronous operations.
        Defaults to ``bot.loop``.
    **session : Optional
        The aiohttp session to use for requests to the API.
    """

    def __init__(self, bot, token, **kwargs):
        self.bot = bot
        self.bot_id = None
        self.loop = kwargs.get('loop') or bot.loop
        self.http = HTTPClient(token, loop=self.loop, session=kwargs.get('session'))
        self._is_closed = False
        self.loop.create_task(self.__ainit__())

        self.get_server_count = self.get_guild_count
        self.post_server_count = self.post_guild_count

    async def __ainit__(self):
        await self.bot.wait_until_ready()
        self.bot_id = self.bot.user.id

    def guild_count(self):
        """Gets the guild count from the Client/Bot object"""
        try:
            return len(self.bot.guilds)
        except AttributeError:
            return len(self.bot.servers)

    async def get_weekend_status(self):
        """This function is a coroutine.

        Gets weekend status from discordbots.org

        Returns
        =======

        weekend status: bool
            The boolean value of weekend status.
        """
        return await self.http.get_weekend_status()

    async def post_guild_count(
            self,
            shard_count: int = None,
            shard_no: int = None
    ):
        """This function is a coroutine.

        Posts the guild count to discordbots.org

        .. _0 based indexing : https://en.wikipedia.org/wiki/Zero-based_numbering

        Parameters
        ==========

        shard_count: int[Optional]
            The total number of shards.
        shard_no: int[Optional]
            The index of the current shard. DBL uses `0 based indexing`_ for shards.
        """
        await self.http.post_guild_count(self.bot_id, self.guild_count(), shard_count, shard_no)

    async def get_guild_count(self, bot_id: int=None):
        """This function is a coroutine.

        Gets a guild count from discordbots.org

        Parameters
        ==========

        bot_id: int[Optional]
            The bot_id of the bot you want to lookup.
            Defaults to the Bot provided in Client init

        Returns
        =======

        stats: dict
            The guild count and shards of a bot.
            The date object is returned in a datetime.datetime object

        """
        if bot_id is None:
            bot_id = self.bot_id
        return await self.http.get_guild_count(bot_id)

    async def get_upvote_info(self, bot_id):
        """This function is a coroutine.

        Gets information about who upvoted a bot from discordbots.org

        .. note::

            This API endpoint is available to the owner of the bot only.

        Returns
        =======

        votes: dict
            Info about who upvoted your bot.

        """
        return await self.http.get_upvote_info(bot_id)

    async def get_bot_info(self, bot_id: int = None):
        """This function is a coroutine.

        Gets information about a bot from discordbots.org

        Parameters
        ==========

        bot_id: int[Optional]
            The bot_id of the bot you want to lookup.

        Returns
        =======

        bot_info: dict
            Information on the bot you looked up.
            https://discordbots.org/api/docs#bots
        """
        if bot_id is None:
            bot_id = self.bot_id
        return await self.http.get_bot_info(bot_id)

    async def get_bots(self, limit: int = 50, offset: int = 0):
        """This function is a coroutine.

        Gets information about listed bots on discordbots.org

        Parameters
        ==========

        limit: int[Optional]
            The number of results you wish to lookup. Defaults to 50. Max 500.
        offset: int[Optional]
            The amount of bots to skip. Defaults to 0.

        Returns
        =======

        bots: dict
            Returns info on the bots on DBL.
            https://discordbots.org/api/docs#bots
        """
        return await self.http.get_bots(limit, offset)

    async def get_user_info(self, user_id: int):
        """This function is a coroutine.

        Gets information about a user on discordbots.org

        Parameters
        ==========

        user_id: int
            The user_id of the user you wish to lookup.

        Returns
        =======

        user_data: dict
            Info about the user.
            https://discordbots.org/api/docs#users
        """
        return await self.http.get_user_info(user_id)

    async def generate_widget_large(
            self,
            bot_id: int = None,
            top: str = '2C2F33',
            mid: str = '23272A',
            user: str = 'FFFFFF',
            cert: str = 'FFFFFF',
            data: str = 'FFFFFF',
            label: str = '99AAB5',
            highlight: str = '2C2F33'
    ):
        """This function is a coroutine.

        Generates a custom large widget. Do not add `#` to the color codes (e.g. #FF00FF become FF00FF).

        Parameters
        ==========

        bot_id: int
            The bot_id of the bot you wish to make a widget for.
        top: str
            The hex color code of the top bar.
        mid: str
            The hex color code of the main section.
        user: str
            The hex color code of the username text.
        cert: str
            The hex color code of the certified text (if applicable).
        data: str
            The hex color code of the statistics (numbers only e.g. 44) of the bot.
        label: str
            The hex color code of the description (text e.g. guild count) of the statistics.
        highlight: str
            The hex color code of the data boxes.

        Returns
        =======

        URL of the widget: str
        """
        url = 'https://discordbots.org/api/widget/{0}.png?topcolor={1}&middlecolor={2}&usernamecolor={3}&certifiedcolor={4}&datacolor={5}&labelcolor={6}&highlightcolor={7}'
        if bot_id is None:
            bot_id = self.bot_id
        url = url.format(bot_id, top, mid, user, cert, data, label, highlight)
        return url

    async def get_widget_large(self, bot_id: int = None):
        """This function is a coroutine.

        Generates the default large widget.

        Parameters
        ==========

        bot_id: int
            The bot_id of the bot you wish to make a widget for.

        Returns
        =======

        URL of the widget: str
        """
        if bot_id is None:
            bot_id = self.bot_id
        url = 'https://discordbots.org/api/widget/{0}.png'.format(bot_id)
        return url

    async def generate_widget_small(
            self,
            bot_id: int = None,
            avabg: str = '2C2F33',
            lcol: str = '23272A',
            rcol: str = '2C2F33',
            ltxt: str = 'FFFFFF',
            rtxt: str = 'FFFFFF'
    ):
        """This function is a coroutine.

        Generates a custom large widget. Do not add `#` to the color codes (e.g. #FF00FF become FF00FF).

        Parameters
        ==========

        bot_id: int
            The bot_id of the bot you wish to make a widget for.
        avabg: str
            The hex color code of the background of the avatar (if the avatar has transparency).
        lcol: str
            The hex color code of the left background color.
        rcol: str
            The hex color code of the right background color.
        ltxt: str
            The hex color code of the left text.
        rtxt: str
            The hex color code of the right text.

        Returns
        =======

        URL of the widget: str
        """
        url = 'https://discordbots.org/api/widget/lib/{0}.png?avatarbg={1}&lefttextcolor={2}&righttextcolor={3}&leftcolor={4}&rightcolor={5}'
        if bot_id is None:
            bot_id = self.bot_id
        url = url.format(bot_id, avabg, ltxt, rtxt, lcol, rcol)
        return url

    async def get_widget_small(self, bot_id: int = None):
        """This function is a coroutine.

        Generates the default small widget.

        Parameters
        ==========

        bot_id: int
            The bot_id of the bot you wish to make a widget for.

        Returns
        =======

        URL of the widget: str
        """
        if bot_id is None:
            bot_id = self.bot_id
        url = 'https://discordbots.org/api/widget/lib/{0}.png'.format(bot_id)
        return url

    async def close(self):
        """This function is a coroutine.

        Closes all connections."""
        if self._is_closed:
            return
        else:
            await self.http.close()
            self._is_closed = True


# Copyright (c) 2009-2015, Dmitry Vasiliev <dima@hlabs.org>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#  * Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
#  * Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#  * Neither the name of the copyright holders nor the names of its
#    contributors may be used to endorse or promote products derived from this
#    software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

"""Erlang port protocol."""

__author__ = "Dmitry Vasiliev <dima@hlabs.org>"

import os
import errno
from struct import Struct
from threading import Lock

from erlport.erlterms import encode, decode

class Port(object):
    """Erlang port."""

    _formats = {
        1: Struct(b"B"),
        2: Struct(b">H"),
        4: Struct(b">I"),
        }

    def __init__(self, packet=4, use_stdio=True, compressed=False,
            descriptors=None, buffer_size=65536):
        if buffer_size < 1:
            raise ValueError("invalid buffer size value: %s" % (buffer_size,))
        struct = self._formats.get(packet)
        if struct is None:
            raise ValueError("invalid packet size value: %s" % (packet,))
        self.__pack = struct.pack
        self.__unpack = struct.unpack
        self.packet = packet
        self.compressed = compressed

        if descriptors is not None:
            self.in_d, self.out_d = descriptors
        elif use_stdio:
            self.in_d, self.out_d = 0, 1
        else:
            self.in_d, self.out_d = 3, 4

        self.__buffer = b""
        self.buffer_size = buffer_size
        self.__read_lock = Lock()
        self.__write_lock = Lock()

    def _read_data(self):
        try:
            buf = os.read(self.in_d, self.buffer_size)
        except OSError as why:
            if why.errno in (errno.EPIPE, errno.EINVAL):
                raise EOFError()
            raise
        if not buf:
            raise EOFError()
        return buf

    def read(self):
        """Read incoming message."""
        packet = self.packet
        with self.__read_lock:
            buffer = self.__buffer
            while len(buffer) < packet:
                buffer += self._read_data()
            length = self.__unpack(buffer[:packet])[0] + packet
            while len(buffer) < length:
                buffer += self._read_data()
            term, self.__buffer = decode(buffer[packet:])
        return term

    def write(self, message):
        """Write outgoing message."""
        data = encode(message, compressed=self.compressed)
        length = len(data)
        data = self.__pack(length) + data
        with self.__write_lock:
            while data:
                try:
                    n = os.write(self.out_d, data)
                except OSError as why:
                    if why.errno in (errno.EPIPE, errno.EINVAL):
                        raise EOFError()
                    raise
                if not n:
                    raise EOFError()
                data = data[n:]
        return length + self.packet

    def close(self):
        """Close port."""
        os.close(self.in_d)
        os.close(self.out_d)