"""DWC Network Server Emulator

    Copyright (C) 2014 polaris-
    Copyright (C) 2014 ToadKing
    Copyright (C) 2016 Sepalani

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as
    published by the Free Software Foundation, either version 3 of the
    License, or (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

 Server emulator for *.available.gs.nintendowifi.net
                 and *.master.gs.nintendowifi.net
 Query and Reporting:
 http://docs.poweredbygamespy.com/wiki/Query_and_Reporting_Overview
 http://wiki.tockdom.com/wiki/Server_NATNEG
"""

import logging
import SocketServer
import threading
import time
import Queue
import gamespy.gs_utility as gs_utils
import other.utils as utils
import traceback

from multiprocessing.managers import BaseManager
import dwc_config

logger = dwc_config.get_logger('GameSpyNatNegServer')


class GameSpyServerDatabase(BaseManager):
    pass

GameSpyServerDatabase.register("get_server_list")
GameSpyServerDatabase.register("modify_server_list")
GameSpyServerDatabase.register("find_servers")
GameSpyServerDatabase.register("find_server_by_address")
GameSpyServerDatabase.register("find_server_by_local_address")
GameSpyServerDatabase.register("add_natneg_server")
GameSpyServerDatabase.register("get_natneg_server")
GameSpyServerDatabase.register("delete_natneg_server")


def handle_natneg(nn, recv_data, addr, socket):
    """Command: Unknown."""
    logger.log(logging.DEBUG,
               "Received unknown command %02x from %s:%d...",
               ord(recv_data[7]), *addr)
    logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data))


def handle_natneg_init(nn, recv_data, addr, socket):
    """Command: 0x00 - NN_INIT.

    Send by the client to initialize the connection.

    Example:
    fd fc 1e 66 6a b2 03 00 3d f1 00 71 00 00 01 0a
    00 01 e2 00 00 6d 61 72 69 6f 6b 61 72 74 77 69
    69 00

    Description:
    fd fc 1e 66 6a b2 - NATNEG magic
    03                - NATNEG version
    00                - NATNEG record type
    3d f1 00 71       - Session id
    00                - Port type (between 0x00 and 0x03)
    00                - Client index (0x00 - Client,
                                      0x01 - Host)
    01                - Use game port
    0a 00 01 e2       - Local IP
    00 00             - Local port
    GAME_NAME 00      - Game name
    """
    logger.log(logging.DEBUG, "Received initialization from %s:%d...", *addr)

    session_id = utils.get_int(recv_data, 8)
    output = bytearray(recv_data[0:14])

    # Checked with Tetris DS, Mario Kart DS, and Metroid Prime
    # Hunters, and this seems to be the standard response to 0x00
    output += bytearray([0xff, 0xff, 0x6d, 0x16, 0xb5, 0x7d, 0xea])
    output[7] = 0x01  # Initialization response
    nn.write_queue.put((output, addr, socket))

    # Try to connect to the server
    gameid = utils.get_string(recv_data, 0x15)
    client_id = "%02x" % ord(recv_data[13])
    localaddr = utils.get_local_addr(recv_data, 15)

    nn.session_list \
        .setdefault(session_id, {}) \
        .setdefault(client_id,
                    {
                        'connected': False,
                        'addr': '',
                        'localaddr': None,
                        'serveraddr': None,
                        'gameid': None
                    })

    # In fact, it's a pointer
    client_id_session = nn.session_list[session_id][client_id]
    client_id_session['gameid'] = gameid
    client_id_session['addr'] = addr
    client_id_session['localaddr'] = localaddr

    for client in nn.session_list[session_id]:
        # Another pointer
        client_session = nn.session_list[session_id][client]
        if client_session['connected'] or client == client_id:
            continue

        # --- Send to requesting client
        # Get server info
        serveraddr = nn.get_server_addr(gameid, session_id, client)
        client_session['serveraddr'] = serveraddr
        logger.log(logging.DEBUG,
                   "Found server from local ip/port: %s from %d",
                   serveraddr, session_id)

        # Get public port
        if client_session['serveraddr'] is not None:
            publicport = int(client_session['serveraddr']['publicport'])
        else:
            publicport = \
                client_session['localaddr'][1] or \
                client_session['addr'][1]

        output = bytearray(recv_data[0:12])
        output += utils.get_bytes_from_ip_str(client_session['addr'][0])
        output += utils.get_bytes_from_short(publicport, True)

        # Unknown, always seems to be \x42\x00
        output += bytearray([0x42, 0x00])
        output[7] = 0x05  # NN_CONNECT
        nn.write_queue.put((output, client_id_session['addr'], socket))

        logger.log(logging.DEBUG,
                   "Sent connection request to %s:%d...",
                   *client_id_session['addr'])
        logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(output))

        # --- Send to other client
        # Get server info
        serveraddr = nn.get_server_addr(gameid, session_id, client_id)
        client_id_session['serveraddr'] = serveraddr
        logger.log(logging.DEBUG,
                   "Found server 2 from local ip/port: %s from %d",
                   serveraddr, session_id)

        # Get public port
        if client_id_session['serveraddr'] is not None:
            publicport = int(client_id_session['serveraddr']['publicport'])
        else:
            publicport = \
                client_id_session['localaddr'][1] or \
                client_id_session['addr'][1]

        output = bytearray(recv_data[0:12])
        output += utils.get_bytes_from_ip_str(client_id_session['addr'][0])
        output += utils.get_bytes_from_short(publicport, True)

        # Unknown, always seems to be \x42\x00
        output += bytearray([0x42, 0x00])
        output[7] = 0x05  # NN_CONNECT
        nn.write_queue.put((output, client_session['addr'], socket))

        logger.log(logging.DEBUG,
                   "Sent connection request to %s:%d...",
                   *client_session['addr'])
        logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(output))


def handle_natneg_initack(nn, recv_data, addr, socket):
    """Command: 0x01 - NN_INITACK.

    Reply by the server for record NN_INIT (0x00).

    Example:
    fd fc 1e 66 6a b2 03 01 3d f1 00 71 00 00 ff ff
    6d 16 b5 7d ea

    Description:
    fd fc 1e 66 6a b2 - NATNEG magic
    03                - NATNEG version
    01                - NATNEG record type
    3d f1 00 71       - Session id
    00                - Port type (between 0x00 and 0x03)
    00                - Client index (0x00 - Client,
                                      0x01 - Host)
    ff                - Use game port (-1)? Dummy value?
    ff 6d 16 b5       - Local IP? Dummy value?
    7d ea             - Local port? Hex speak of "Idea"? Dummy value?
    """
    logger.log(logging.WARNING,
               "Received server record type command NN_INITACK (0x01)"
               " from %s:%d...", *addr)
    logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data))


def handle_natneg_erttest(nn, recv_data, addr, socket):
    """Command: 0x02 - NN_ERTTEST.

    Reply by the server for record NN_NATIFY_REQUEST (0x0C).

    Example:
    fd fc 1e 66 6a b2 03 02 00 00 03 09 02 00 00 00
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    00 00 00 00 00 00 00 00 00

    Description:
    fd fc 1e 66 6a b2 - NATNEG magic
    03                - NATNEG version
    02                - NATNEG record type
    00 00 03 09       - Session id
    02                - Port type (between 0x00 and 0x03)
                      - 60 bytes padding?
    00                - Client index (0x00 - Client,
                                      0x01 - Host)
    00                - NATNEG result?
    00 00 00 00       - NAT type?
    00 00 00 00       - NAT mapping scheme?
    00 (x50)          - Game name?
    """
    logger.log(logging.WARNING,
               "Received server record type command NN_ERTTEST (0x02)"
               " from %s:%d...", *addr)
    logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data))


def handle_natneg_ertack(nn, recv_data, addr, socket):
    """Command: 0x03 - NN_ERTACK.

    Reply by the client for record NN_ERTTEST (0x02).
    Only the record type is changed.

    Example:
    fd fc 1e 66 6a b2 03 03 00 00 03 09 02 00 00 00
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    00 00 00 00 00 00 00 00 00

    Description:
    fd fc 1e 66 6a b2 - NATNEG magic
    03                - NATNEG version
    03                - NATNEG record type
    00 00 03 09       - Session id
    02                - Port type (between 0x00 and 0x03)
                      - 60 bytes padding?
    00                - Client index (0x00 - Client,
                                      0x01 - Host)
    00                - NATNEG result?
    00 00 00 00       - NAT type?
    00 00 00 00       - NAT mapping scheme?
    00 (x50)          - Game name?
    """
    logger.log(logging.INFO, "Received ERT acknowledgement from %s:%d", *addr)


def handle_natneg_stateupdate(nn, recv_data, addr, socket):
    """Command: 0x04 - NN_STATEUPDATE.

    TODO

    Example:
    TODO

    Description:
    TODO
    """
    logger.log(logging.WARNING,
               "Received unimplemented command NN_STATEUPDATE (0x04)"
               " from %s:%d...", *addr)
    logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(output))


def handle_natneg_connect(nn, recv_data, addr, socket):
    """Command: 0x05 - NN_CONNECT.

    Reply by the server for record NN_INIT (0x00).

    Example:
    fd fc 1e 66 6a b2 03 05 3d f1 00 71 18 ab ed 7a
    da 00 42 00

    Description:
    fd fc 1e 66 6a b2 - NATNEG magic
    03                - NATNEG version
    05                - NATNEG record type
    3d f1 00 71       - Session id
    18 ab ed 7a       - Remote IP
    da 00             - Remote port
    42                - Got remote data
    00                - Finished
    """
    logger.log(logging.WARNING,
               "Received server record type command NN_CONNECT (0x05)"
               " from %s:%d...", *addr)
    logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data))


def handle_natneg_connect_ack(nn, recv_data, addr, socket):
    """Command: 0x06 - NN_CONNECT_ACK.

    Reply by the client for record NN_CONNECT (0x05).

    Example:
    fd fc 1e 66 6a b2 03 06 3d f1 00 71 90 00 cd a0
    80 00 00 00 90

    Description:
    fd fc 1e 66 6a b2 - NATNEG magic
    03                - NATNEG version
    06                - NATNEG record type
    3d f1 00 71       - Session id
    90                - Port type (0x00, 0x80 or 0x90)
    00                - Client index (0x00 - Client,
                                      0x01 - Host)
    cd                - Use game port?
    a0 80 00 00       - Local IP?
    00 90             - Local port?
    """
    client_id = "%02x" % ord(recv_data[13])
    session_id = utils.get_int(recv_data, 8)
    logger.log(logging.DEBUG,
               "Received connected command from %s:%d...",
               *addr)

    if session_id in nn.session_list and \
       client_id in nn.session_list[session_id]:
        nn.session_list[session_id][client_id]['connected'] = True


def handle_natneg_connect_ping(nn, recv_data, addr, socket):
    """Command: 0x07 - NN_CONNECT_PING.

    Looks like NN_CONNECT but between clients.
    The server shouldn't be involved.

    Example:
    fd fc 1e 66 6a b2 03 07 ?? ?? ?? ?? ?? ?? ?? ??
    ?? ?? ?? ??

    Description:
    fd fc 1e 66 6a b2 - NATNEG magic
    03                - NATNEG version
    07                - NATNEG record type
    ?? ?? ?? ??       - Session id
    ?? ?? ?? ??       - Remote IP
    ?? ??             - Remote port
    ??                - Sequence counter (0 or 1)
    ??                - Error
    """
    logger.log(logging.WARNING,
               "Received unimplemented command NN_CONNECT_PING (0x07)"
               " from %s:%d...", *addr)
    logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data))


def handle_natneg_backup_test(nn, recv_data, addr, socket):
    """Command: 0x08 - NN_BACKUP_TEST.

    Send by the client.

    Example:
    TODO

    Description:
    Untested
    """
    logger.log(logging.DEBUG, "Received backup command from %s:%d...", *addr)
    logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data))

    # Backup response
    output = bytearray(recv_data)
    output[7] = 0x09  # NN_BACKUP_ACK
    nn.write_queue.put((output, addr, socket))


def handle_natneg_backup_ack(nn, recv_data, addr, socket):
    """Command: 0x09 - NN_BACKUP_ACK.

    Reply by the server for record NN_BACKUP_TEST (0x08).
    Only the record type is changed.

    Example:
    TODO

    Description:
    TODO
    """
    logger.log(logging.WARNING,
               "Received server record type command NN_BACKUP_ACK (0x09)"
               " from %s:%d...", *addr)
    logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(output))


def handle_natneg_address_check(nn, recv_data, addr, socket):
    """Command: 0x0A - NN_ADDRESS_CHECK.

    Send by the client during connection test.

    Example:
    fd fc 1e 66 6a b2 03 0a 00 00 00 00 01 00 00 00
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    00 00 00 00 00 00 00 00 00

    Description:
    fd fc 1e 66 6a b2 - NATNEG magic
    03                - NATNEG version
    0a                - NATNEG record type
    00 00 00 00       - Session id
    01                - Port type (between 0x00 and 0x03)
                      - 60 bytes padding?
    00                - Client index (0x00 - Client,
                                      0x01 - Host)
    00                - NATNEG result?
    00 00 00 00       - NAT type?
    00 00 00 00       - NAT mapping scheme?
    00 (x50)          - Game name?
    """
    client_id = "%02x" % ord(recv_data[13])
    logger.log(logging.DEBUG,
               "Received address check command from %s:%d...",
               *addr)
    logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data))

    output = bytearray(recv_data[0:15])
    output += utils.get_bytes_from_ip_str(addr[0])
    output += utils.get_bytes_from_short(addr[1], True)
    output += bytearray(recv_data[len(output):])

    output[7] = 0x0b  # NN_ADDRESS_REPLY
    nn.write_queue.put((output, addr, socket))

    logger.log(logging.DEBUG, "Sent address check response to %s:%d...", *addr)
    logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(output))


def handle_natneg_address_reply(nn, recv_data, addr, socket):
    """Command: 0x0B - NN_ADDRESS_REPLY.

    Reply by the server for record NN_ADDRESS_CHECK (0x0A).

    Example:
    fd fc 1e 66 6a b2 03 0b 00 00 00 03 01 00 00 25
    c9 e2 8a 91 e4

    Description:
    fd fc 1e 66 6a b2 - NATNEG magic
    03                - NATNEG version
    0b                - NATNEG record type
    00 00 00 03       - Session id
    01                - Port type (between 0x00 and 0x03)
    00                - Client index (0x00 - Client,
                                      0x01 - Host)
    00                - Use game port
    25 c9 e2 8a       - Public IP
    91 e4             - Public port
    """
    logger.log(logging.WARNING,
               "Received server record type command NN_ADDRESS_REPLY (0x0B)"
               " from %s:%d...", *addr)
    logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(output))


def handle_natneg_natify_request(nn, recv_data, addr, socket):
    """Command: 0x0C - NN_NATIFY_REQUEST.

    Send by the client during connection test.

    Example:
    fd fc 1e 66 6a b2 03 0c 00 00 03 09 01 00 00 00
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    00 00 00 00 00 00 00 00 00

    Description:
    fd fc 1e 66 6a b2 - NATNEG magic
    03                - NATNEG version
    0c                - NATNEG record type
    00 00 03 09       - Session id
    01                - Port type (between 0x00 and 0x03)
                      - 60 bytes padding?
    00                - Client index (0x00 - Client,
                                      0x01 - Host)
    00                - NATNEG result?
    00 00 00 00       - NAT type?
    00 00 00 00       - NAT mapping scheme?
    00 (x50)          - Game name?
    """
    port_type = "%02x" % ord(recv_data[12])
    logger.log(logging.DEBUG, "Received natify command from %s:%d...", *addr)

    output = bytearray(recv_data)
    output[7] = 0x02  # ERT Test
    nn.write_queue.put((output, addr, socket))

    logger.log(logging.DEBUG, "Sent natify response to %s:%d...", *addr)
    logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(output))


def handle_natneg_report(nn, recv_data, addr, socket):
    """Command: 0x0D - NN_REPORT.

    Send by the client.

    Example:
    fd fc 1e 66 6a b2 03 0d 3d f1 00 71 00 00 01 00
    00 00 06 00 00 00 00 6d 61 72 69 6f 6b 61 72 74
    77 69 69 00 00 00 00 00 00 00 00 00 00 00 00 00
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    00 00 00 00 00 00 00 00 00

    Description:
    fd fc 1e 66 6a b2 - NATNEG magic
    03                - NATNEG version
    0d                - NATNEG record type
    3d f1 00 71       - Session id
    00                - Port type (0x00, 0x80 or 0x90)
    00                - Client index (0x00 - Client,
                                      0x01 - Host)
    01                - NATNEG result (0x00 - Error,
                                       0x01 - Success)
    00 00 00 06       - NAT type (0x00 - No NAT,
                                  0x01 - Firewall only,
                                  0x02 - Full cone,
                                  0x03 - Restricted cone,
                                  0x04 - Port restricted cone,
                                  0x05 - Symmetric,
                                  0x06 - Unknown)
    00 00 00 00       - NAT mapping scheme (0x00 - Unknown,
                                            0x01 - Private same as public,
                                            0x02 - Consistent port,
                                            0x03 - Incremental,
                                            0x04 - Mixed)
    GAME_NAME 00      - Game name (GAME_NAME is 49 bytes length)
    """
    logger.log(logging.DEBUG, "Received report command from %s:%d...", *addr)
    logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data))

    # Report response
    output = bytearray(recv_data[:21])
    output[7] = 0x0e  # Report response
    output[14] = 0  # Clear byte to match real server's response
    nn.write_queue.put((output, addr, socket))


def handle_natneg_report_ack(nn, recv_data, addr, socket):
    """Command: 0x0E - NN_REPORT_ACK.

    Reply by the server for record NN_REPORT (0x0D).

    Example:
    fd fc 1e 66 6a b2 03 0e 3d f1 00 71 00 00 00 00
    00 00 06 00 00

    Description:
    fd fc 1e 66 6a b2 - NATNEG magic
    03                - NATNEG version
    0e                - NATNEG record type
    3d f1 00 71       - Session id
    00                - Port type (0x00, 0x80 or 0x90)
    00                - Client index (0x00 - Client,
                                      0x01 - Host)
    00                - NATNEG result (0x00 - Error,
                                       0x01 - Success)
    00 00 00 06       - NAT type (0x00 - No NAT,
                                  0x01 - Firewall only,
                                  0x02 - Full cone,
                                  0x03 - Restricted cone,
                                  0x04 - Port restricted cone,
                                  0x05 - Symmetric,
                                  0x06 - Unknown)
    """
    logger.log(logging.WARNING,
               "Received server record type command NN_REPORT_ACK (0x0E)"
               " from %s:%d...", *addr)
    logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data))


def handle_natneg_preinit(nn, recv_data, addr, socket):
    """Command: 0x0F - NN_PREINIT.

    Natneg v4 command thanks to Pipian.
    Only seems to be used in very few DS games, namely,
    Pokemon Black/White/Black 2/White 2.

    Example:
    fd fc 1e 66 6a b2 04 0f b5 e0 95 2a 00 24 38 b2
    b3 5e

    Description:
    fd fc 1e 66 6a b2 - NATNEG magic
    04                - NATNEG version
    0f                - NATNEG record type
    b5 e0 95 2a       - Session id
    00                - Client index (0x00 - Client,
                                      0x01 - Host)
    24                - State (0x00 - Waiting for client,
                               0x01 - Waiting for matchup,
                               0x02 - Ready)
    38 b2 b3 5e       - Other client's session id
    """
    logger.log(logging.DEBUG, "Received pre-init command from %s:%d...", *addr)
    logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data))

    session = utils.get_int(recv_data[-4:], 0)

    # Report response
    output = bytearray(recv_data[:-4]) + bytearray([0, 0, 0, 0])
    output[7] = 0x10  # Pre-init response

    if not session:
        # What's the correct behavior when session == 0?
        output[13] = 2
    elif session in nn.natneg_preinit_session:
        # Should this be sent to both clients or just the one that
        # connected most recently?
        # I can't tell from a one sided packet capture of Pokemon.
        # For the time being, send to both clients just in case.
        output[13] = 2
        nn.write_queue.put((output, nn.natneg_preinit_session[session],
                            socket))

        output[12] = (1, 0)[output[12]]  # Swap the index
        del nn.natneg_preinit_session[session]
    else:
        output[13] = 0
        nn.natneg_preinit_session[session] = addr

    nn.write_queue.put((output, addr, socket))


def handle_natneg_preinit_ack(nn, recv_data, addr, socket):
    """Command: 0x10 - NN_PREINIT_ACK.

    Reply by the server for record NN_PREINIT (0x0F).

    Example:
    fd fc 1e 66 6a b2 04 10 b5 e0 95 2a 00 00 00 00
    00 00

    After receiving other client's PREINIT:
    fd fc 1e 66 6a b2 04 10 b5 e0 95 2a 01 02 00 00
    00 00

    Description:
    fd fc 1e 66 6a b2 - NATNEG magic
    04                - NATNEG version
    10                - NATNEG record type
    b5 e0 95 2a       - Session id
    00                - Client index (0x00 - Client,
                                      0x01 - Host)
    00                - State (0x00 - Waiting for client,
                               0x01 - Waiting for matchup,
                               0x02 - Ready)
    00 00 00 00       - Other client's session id (or empty)
    """
    logger.log(logging.WARNING,
               "Received server record type command NN_PREINIT_ACK (0x10)"
               " from %s:%d...", *addr)
    logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data))


class GameSpyNatNegUDPServerHandler(SocketServer.BaseRequestHandler):
    """GameSpy NAT Negotiation handler."""

    nn_magics = bytearray([0xfd, 0xfc, 0x1e, 0x66, 0x6a, 0xb2])
    nn_commands = {
        '\x00': handle_natneg_init,
        '\x01': handle_natneg_initack,
        '\x02': handle_natneg_erttest,
        '\x03': handle_natneg_ertack,
        '\x04': handle_natneg_stateupdate,
        '\x05': handle_natneg_connect,
        '\x06': handle_natneg_connect_ack,
        '\x07': handle_natneg_connect_ping,
        '\x08': handle_natneg_backup_test,
        '\x09': handle_natneg_backup_ack,
        '\x0A': handle_natneg_address_check,
        '\x0B': handle_natneg_address_reply,
        '\x0C': handle_natneg_natify_request,
        '\x0D': handle_natneg_report,
        '\x0E': handle_natneg_report_ack,
        '\x0F': handle_natneg_preinit,
        '\x10': handle_natneg_preinit_ack
    }

    def handle(self):
        """Handle NAT Negotiation request."""
        recv_data, socket = self.request
        addr = self.client_address

        logger.log(logging.DEBUG, "Connection from %s:%d...", *addr)
        logger.log(logging.DEBUG, "%s", utils.pretty_print_hex(recv_data))

        # Make sure it's a legal packet
        if not recv_data.startswith(self.nn_magics):
            logger.log(logging.ERROR, "Aborted due to illegal packet!")
            return

        # Handle commands
        try:
            command = self.nn_commands.get(recv_data[7], handle_natneg)
            command(self.server, recv_data, addr, socket)
        except:
            logger.log(logging.ERROR, "Failed to handle command!")
            logger.log(logging.ERROR, "%s", traceback.format_exc())


class GameSpyNatNegUDPServer(SocketServer.UDPServer):
    """GameSpy NAT Negotiation server."""

    def __init__(self,
                 server_address=dwc_config.get_ip_port('GameSpyNatNegServer'),
                 RequestHandlerClass=GameSpyNatNegUDPServerHandler,
                 bind_and_activate=True):
        SocketServer.UDPServer.__init__(self,
                                        server_address,
                                        RequestHandlerClass,
                                        bind_and_activate)
        self.session_list = {}
        self.natneg_preinit_session = {}
        self.secret_key_list = gs_utils.generate_secret_keys("gslist.cfg")

        self.server_manager = GameSpyServerDatabase(
            address=dwc_config.get_ip_port('GameSpyManager'),
            authkey=""
        )
        self.server_manager.connect()

        self.write_queue = Queue.Queue()
        threading.Thread(target=self.write_queue_worker).start()

    def write_queue_send(self, data, address, socket):
        time.sleep(0.05)
        socket.sendto(data, address)

    def write_queue_worker(self):
        while True:
            data, address, socket = self.write_queue.get()
            threading.Thread(target=self.write_queue_send,
                             args=(data, address, socket)).start()
            self.write_queue.task_done()

    def get_server_info(self, gameid, session_id, client_id):
        """Get server by public IP."""
        server = None
        ip_str = self.session_list[session_id][client_id]['addr'][0]
        servers = self.server_manager.get_natneg_server(session_id) \
                                     ._getvalue()
        
        if servers is None:
            return None
            
        for console in [False, True]:
            if server is not None:
                break
            ip = str(utils.get_ip_from_str(ip_str, console))
            server = next((s for s in servers if s['publicip'] == ip), None)

        return server

    def get_server_info_alt(self, gameid, session_id, client_id):
        """Get server by local address."""
        server = None
        ip_str = self.session_list[session_id][client_id]['addr'][0]

        for console in [False, True]:
            if server is not None:
                break
            ip = str(utils.get_ip_from_str(ip_str, console))
            server = self.server_manager.find_server_by_local_address(
                ip,
                self.session_list[session_id][client_id]['localaddr'],
                self.session_list[session_id][client_id]['gameid']
            )._getvalue()

        return server

    def get_server_addr(self, gameid, session_id, client_id):
        """Get server address."""
        return \
            self.get_server_info(gameid, session_id, client_id) or \
            self.get_server_info_alt(gameid, session_id, client_id)


class GameSpyNatNegServer(object):
    def start(self):
        server = GameSpyNatNegUDPServer()
        logger.log(logging.INFO, "Server is now listening on %s:%d...",
                   *server.server_address)
        server.serve_forever()


if __name__ == "__main__":
    natneg_server = GameSpyNatNegServer()
    natneg_server.start()