class MultiAddr:

    # Validates input string and constructs internal representation.
    def __init__(self, addr):
        self.protocol_map = dict()

        # Empty multiaddrs are valid.
        if not addr:
            self.protocol_map = dict()
            return

        if not addr[0] == "/":
            raise MultiAddrValueError("Invalid input multiaddr.")

        addr = addr[1:]
        protocol_map = dict()
        split_addr = addr.split("/")

        if not split_addr or len(split_addr) % 2 != 0:
            raise MultiAddrValueError("Invalid input multiaddr.")

        is_protocol = True
        curr_protocol = ""

        for addr_part in split_addr:
            if is_protocol:
                curr_protocol = addr_part
            else:
                protocol_map[curr_protocol] = addr_part
            is_protocol = not is_protocol

        # Basic validation of protocols
        # TODO(rzajac): Add more validation as necessary.
        if 'ip4' in self.protocol_map and 'ip6' in self.protocol_map:
            raise MultiAddrValueError("Multiaddr should not specify two IP layers.")

        if 'tcp' in self.protocol_map and 'udp' in self.protocol_map:
            raise MultiAddrValueError("Multiaddr should not specify two transport layers.")

        self.protocol_map = protocol_map

    def get_protocols(self):
        """
        :return: List of protocols contained in this multiaddr.
        """
        return list(self.protocol_map.keys())

    def get_protocol_value(self, protocol):
        """
        Getter for protocol values in this multiaddr.
        :param protocol: the protocol whose value to retrieve
        :return: value of input protocol
        """
        if protocol not in self.protocol_map:
            return None

        return self.protocol_map[protocol]

    def add_protocol(self, protocol, value):
        """
        Setter for protocol values in this multiaddr.
        :param protocol: the protocol whose value to set or add
        :param value: the value for the input protocol
        :return: True if successful
        """
        self.protocol_map[protocol] = value
        return True

    def remove_protocol(self, protocol):
        """
        Remove protocol and its value from this multiaddr.
        :param protocol: the protocol to remove
        :return: True if remove succeeded, False if protocol was not contained in this multiaddr
        """
        del self.protocol_map[protocol]

    def get_multiaddr_string(self):
        """
        :return: the string representation of this multiaddr.
        """
        addr = ""

        for protocol in self.protocol_map:
            addr += "/" + protocol + "/" + self.get_protocol_value(protocol)

        return addr

    def to_options(self):
        """
        Gives back a dictionary with access to transport information from this multiaddr.
        Example: MultiAddr('/ip4/127.0.0.1/tcp/4001').to_options()
        = { family: 'ipv4', host: '127.0.0.1', transport: 'tcp', port: '4001' }
        :return: {{family: String, host: String, transport: String, port: String}}
        with None if field does not exist
        """
        options = dict()

        if 'ip4' in self.protocol_map:
            options['family'] = 'ipv4'
            options['host'] = self.protocol_map['ip4']
        elif 'ip6' in self.protocol_map:
            options['family'] = 'ipv6'
            options['host'] = self.protocol_map['ip6']
        else:
            options['family'] = None
            options['host'] = None

        if 'tcp' in self.protocol_map:
            options['transport'] = 'tcp'
            options['port'] = self.protocol_map['tcp']
        elif 'udp' in self.protocol_map:
            options['transport'] = 'udp'
            options['port'] = self.protocol_map['udp']
        else:
            options['transport'] = None
            options['port'] = None

        return options


class MultiAddrValueError(ValueError):
    """Raised when the input string to the MultiAddr constructor was invalid."""
    pass