#!/usr/bin/env python3
"""
coresendmsg: utility for generating CORE messages
"""

import optparse
import os
import socket
import sys

from core.api.tlv import coreapi
from core.api.tlv.enumerations import CORE_API_PORT, MessageTypes, SessionTlvs
from core.emulator.enumerations import MessageFlags


def print_available_tlvs(t, tlv_class):
    """
    Print a TLV list.
    """
    print(f"TLVs available for {t} message:")
    for tlv in sorted([tlv for tlv in tlv_class.tlv_type_map], key=lambda x: x.name):
        print(tlv.name.lower())


def print_examples(name):
    """
    Print example usage of this script.
    """
    examples = [
        ("node number=3 x_position=125 y_position=525",
         "move node number 3 to x,y=(125,525)"),
        ("node number=4 icon=/usr/local/share/core/icons/normal/router_red.gif",
         "change node number 4\"s icon to red"),
        ("node flags=add number=5 type=0 name=\"n5\" x_position=500 y_position=500",
         "add a new router node n5"),
        ("link n1_number=2 n2_number=3 delay=15000",
         "set a 15ms delay on the link between n2 and n3"),
        ("link n1_number=2 n2_number=3 gui_attributes=\"color=blue\"",
         "change the color of the link between n2 and n3"),
        ("link flags=add n1_number=4 n2_number=5 interface1_ip4=\"10.0.3.2\" "
         "interface1_ip4_mask=24 interface2_ip4=\"10.0.3.1\" interface2_ip4_mask=24",
         "link node n5 with n4 using the given interface addresses"),
        ("execute flags=string,text node=1 number=1000 command=\"uname -a\" -l",
         "run a command on node 1 and wait for the result"),
        ("execute node=2 number=1001 command=\"killall ospfd\"",
         "run a command on node 2 and ignore the result"),
        ("file flags=add node=1 name=\"/var/log/test.log\" data=\"hello world.\"",
         "write a test.log file on node 1 with the given contents"),
        ("file flags=add node=2 name=\"test.log\" source_name=\"./test.log\"",
         "move a test.log file from host to node 2"),
    ]
    print(f"Example {name} invocations:")
    for cmd, descr in examples:
        print(f"  {name} {cmd}\n\t\t{descr}")


def receive_message(sock):
    """
    Retrieve a message from a socket and return the CoreMessage object or
    None upon disconnect. Socket data beyond the first message is dropped.
    """
    try:
        # large receive buffer used for UDP sockets, instead of just receiving
        # the 4-byte header
        data = sock.recv(4096)
        msghdr = data[:coreapi.CoreMessage.header_len]
    except KeyboardInterrupt:
        print("CTRL+C pressed")
        sys.exit(1)

    if len(msghdr) == 0:
        return None

    msgdata = None
    msgtype, msgflags, msglen = coreapi.CoreMessage.unpack_header(msghdr)

    if msglen:
        msgdata = data[coreapi.CoreMessage.header_len:]
    try:
        msgcls = coreapi.CLASS_MAP[msgtype]
    except KeyError:
        msg = coreapi.CoreMessage(msgflags, msghdr, msgdata)
        msg.message_type = msgtype
        print(f"unimplemented CORE message type: {msg.type_str()}")
        return msg
    if len(data) > msglen + coreapi.CoreMessage.header_len:
        data_size = len(data) - (msglen + coreapi.CoreMessage.header_len)
        print(f"received a message of type {msgtype}, dropping {data_size} bytes of extra data")
    return msgcls(msgflags, msghdr, msgdata)


def connect_to_session(sock, requested):
    """
    Use Session Messages to retrieve the current list of sessions and
    connect to the first one.
    """
    # request the session list
    tlvdata = coreapi.CoreSessionTlv.pack(SessionTlvs.NUMBER.value, "")
    flags = MessageFlags.STRING.value
    smsg = coreapi.CoreSessionMessage.pack(flags, tlvdata)
    sock.sendall(smsg)

    print("waiting for session list...")
    smsgreply = receive_message(sock)
    if smsgreply is None:
        print("disconnected")
        return False

    sessstr = smsgreply.get_tlv(SessionTlvs.NUMBER.value)
    if sessstr is None:
        print("missing session numbers")
        return False

    # join the first session (that is not our own connection)
    tmp, localport = sock.getsockname()
    sessions = sessstr.split("|")
    sessions.remove(str(localport))
    if len(sessions) == 0:
        print("no sessions to join")
        return False

    if not requested:
        session = sessions[0]
    elif requested in sessions:
        session = requested
    else:
        print("requested session not found!")
        return False

    print(f"joining session: {session}")
    tlvdata = coreapi.CoreSessionTlv.pack(SessionTlvs.NUMBER.value, session)
    flags = MessageFlags.ADD.value
    smsg = coreapi.CoreSessionMessage.pack(flags, tlvdata)
    sock.sendall(smsg)
    return True


def receive_response(sock, opt):
    """
    Receive and print a CORE message from the given socket.
    """
    print("waiting for response...")
    msg = receive_message(sock)
    if msg is None:
        print(f"disconnected from {opt.address}:{opt.port}")
        sys.exit(0)
    print(f"received message: {msg}")


def main():
    """
    Parse command-line arguments to build and send a CORE message.
    """
    types = [message_type.name.lower() for message_type in MessageTypes]
    flags = [flag.name.lower() for flag in MessageFlags]
    types_usage = " ".join(types)
    flags_usage = " ".join(flags)
    usagestr = (
        "usage: %prog [-h|-H] [options] [message-type] [flags=flags] "
        "[message-TLVs]\n\n"
        f"Supported message types:\n  {types_usage}\n"
        f"Supported message flags (flags=f1,f2,...):\n  {flags_usage}"
    )
    parser = optparse.OptionParser(usage=usagestr)
    default_address = "localhost"
    default_session = None
    default_tcp = False
    parser.set_defaults(
        port=CORE_API_PORT,
        address=default_address,
        session=default_session,
        listen=False,
        examples=False,
        tlvs=False,
        tcp=default_tcp
    )
    parser.add_option("-H", dest="examples", action="store_true",
                      help="show example usage help message and exit")
    parser.add_option("-p", "--port", dest="port", type=int,
                      help=f"TCP port to connect to, default: {CORE_API_PORT}")
    parser.add_option("-a", "--address", dest="address", type=str,
                      help=f"Address to connect to, default: {default_address}")
    parser.add_option("-s", "--session", dest="session", type=str,
                      help=f"Session to join, default: {default_session}")
    parser.add_option("-l", "--listen", dest="listen", action="store_true",
                      help="Listen for a response message and print it.")
    parser.add_option("-t", "--list-tlvs", dest="tlvs", action="store_true",
                      help="List TLVs for the specified message type.")
    parser.add_option("--tcp", dest="tcp", action="store_true",
                      help=f"Use TCP instead of UDP and connect to a session default: {default_tcp}")

    def usage(msg=None, err=0):
        print()
        if msg:
            print(f"{msg}\n")
        parser.print_help()
        sys.exit(err)

    # parse command line opt
    opt, args = parser.parse_args()
    if opt.examples:
        print_examples(os.path.basename(sys.argv[0]))
        sys.exit(0)
    if len(args) == 0:
        usage("Please specify a message type to send.")

    # given a message type t, determine the message and TLV classes
    t = args.pop(0)
    t = t.lower()
    if t not in types:
        usage(f"Unknown message type requested: {t}")
    message_type = MessageTypes[t.upper()]
    msg_cls = coreapi.CLASS_MAP[message_type.value]
    tlv_cls = msg_cls.tlv_class

    # list TLV types for this message type
    if opt.tlvs:
        print_available_tlvs(t, tlv_cls)
        sys.exit(0)

    # build a message consisting of TLVs from "type=value" arguments
    flagstr = ""
    tlvdata = b""
    for a in args:
        typevalue = a.split("=")
        if len(typevalue) < 2:
            usage(f"Use \"type=value\" syntax instead of \"{a}\".")
        tlv_typestr = typevalue[0].lower()
        tlv_valstr = "=".join(typevalue[1:])
        if tlv_typestr == "flags":
            flagstr = tlv_valstr
            continue
        try:
            tlv_type = tlv_cls.tlv_type_map[tlv_typestr.upper()]
            tlvdata += tlv_cls.pack_string(tlv_type.value, tlv_valstr)
        except KeyError:
            usage(f"Unknown TLV: \"{tlv_typestr}\"")

    flags = 0
    for f in flagstr.split(","):
        if f == "":
            continue
        try:
            flag_enum = MessageFlags[f.upper()]
            n = flag_enum.value
            flags |= n
        except KeyError:
            usage(f"Invalid flag \"{f}\".")

    msg = msg_cls.pack(flags, tlvdata)

    if opt.tcp:
        protocol = socket.SOCK_STREAM
    else:
        protocol = socket.SOCK_DGRAM

    sock = socket.socket(socket.AF_INET, protocol)
    sock.setblocking(True)

    try:
        sock.connect((opt.address, opt.port))
    except Exception as e:
        print(f"Error connecting to {opt.address}:{opt.port}:\n\t{e}")
        sys.exit(1)

    if opt.tcp and not connect_to_session(sock, opt.session):
        print("warning: continuing without joining a session!")

    sock.sendall(msg)
    if opt.listen:
        receive_response(sock, opt)
    if opt.tcp:
        sock.shutdown(socket.SHUT_RDWR)
    sock.close()
    sys.exit(0)


if __name__ == "__main__":
    main()