#!/usr/bin/env python3
# #!/bin/env PYTHONUNBUFFERED="yes" python
"""
Publish JSON fifo and routes to one or more e-links (FIDs)

Usage:
    felix-fifo2elink [options] <fifo> <hostname_or_ip_or_iface> <port> <elink> (<did> <cid>)...

Options:
    -b, --netio-pagesize=SIZE       NetIO page size in kilobytes [default: 64]
    -B, --netio-pages=N             Number of NetIO pages [default: 256]
    -g, --bus-groupname GROUP_NAME  Group name to use for the bus [default: FELIX]
    -b, --bus-dir DIRECTORY         Directory to use for the bus [default: ./bus]
    -v, --verbose                   Verbose output
    -w, --netio-watermark=SIZE      NetIO watermark in kilobytes [default: 56]
    --error-out=fifo                (NA) Write error information to the error FIFO

Arguments:
    <fifo>                      FIFO (JSON) to read from
    <hostname_or_ip_or_iface>   Hostname or IP or Interface to publish from
    <port>                      Port number to use
    <elink>                     Elink to use for FID
    <did>                       DetectorID to use for FID
    <cid>                       ConnectorID to use for FID
"""
import errno
import functools
import json
import netifaces
import os
import socket
import sys

print = functools.partial(print, flush=True)

# add the ../python path to find felix python libs
sys.path.append(os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'python'))

from docopt import docopt  # noqa: E402

from inotify_simple import INotify, flags, masks  # noqa: E402

from netio_py import ffi, lib  # noqa: E402

from libfelix_bus_py import FelixBus, FelixBusInfo  # noqa: E402

from felix.felix_fifo_util import FelixFifoUtil  # noqa: E402

from felix_fid import FID
from felixtag import FELIX_TAG


@ffi.def_extern()
def on_init(data):
    """On Init."""
    if verbose:
        print("on_init")


@ffi.def_extern()
def on_publish_subscribe(netio_socket, fid, addr, addrlen):
    """On Subscribe."""
    if verbose:
        print("on_subscribe")


@ffi.def_extern()
def on_publish_connection_established(socket):
    """On Connection Established."""
    if verbose:
        print("connection to subscriber established")


@ffi.def_extern()
def on_publish_connection_closed(socket):
    """On Connection Closed."""
    if verbose:
        print("connection to subscriber closed")


@ffi.def_extern()
def on_publish_buffer_available(netio_socket):
    """On Buffer Available."""
    if verbose:
        print("on_buffer_available")


@ffi.def_extern()
def on_file(fd, data):
    """On File changes."""
    if verbose:
        print("on_file")
    for event in inotify.read():
        for flag in flags.from_mask(event.mask):
            if verbose:
                print(event, flag)
            if flag == flags.MODIFY:
                while True:
                    try:
                        buffer = os.read(pipe, 4096)
                    except OSError as err:
                        if err.errno == errno.EAGAIN or err.errno == errno.EWOULDBLOCK:
                            buffer = []
                        else:
                            raise  # something else has happened -- better reraise

                    if not buffer:
                        return

                    # FIXME can be optimized using jsonlines (for ndjson)
                    ndjson_bytes = []
                    for ndjson in buffer.decode("utf-8").split('\n'):
                        if ndjson == '':
                            continue
                        # FIXME handle errors here
                        msg = json.loads(ndjson)
                        # FIXME handle missin
                        fid = msg.get('fid')
                        if fid not in fids:
                            if verbose:
                                print("fid", hex(fid), fid, "not in list")
                            continue
                        if not fid:
                            if verbose:
                                print("missing fid in message", ndjson)
                            continue
                        if verbose:
                            print("Published", len(ndjson), "bytes to fid", hex(fid))
                        ndjson_bytes = (ndjson + '\n').encode('utf-8')
                        lib.netio_buffered_publish(netio_socket, fid, ffi.from_buffer(ndjson_bytes), len(ndjson_bytes), 0, ffi.NULL)
                        lib.netio_buffered_publish_flush(netio_socket, fid, ffi.NULL)

                    bytes_read = len(buffer)
            else:
                # output error
                print("Unhandled flag " + str(flag))

def get_first_ip(interface):
    """TBD"""
    address = None
    try:
        address = netifaces.ifaddresses(interface)
    except ValueError:
        pass
    if address and address[netifaces.AF_INET] and address[netifaces.AF_INET][0] and address[netifaces.AF_INET][0]['addr']:
        return address[netifaces.AF_INET][0]['addr']
    return None


if __name__ == "__main__":
    """fifo2elink."""
    args = docopt(__doc__, version=sys.argv[0] + " " + FELIX_TAG)

    bus_dir = args["--bus-dir"]
    bus_groupname = args["--bus-groupname"]
    verbose = args['--verbose']

    pagesize = int(args['--netio-pagesize'], 0)
    num_pages = int(args['--netio-pages'], 0)
    watermark = int(args['--netio-watermark'], 0)
    # FIXME: not yet used
    error = FelixFifoUtil(args['--error-out']) if args['--error-out'] else None

    fifo = args['<fifo>']
    hostname = args['<hostname_or_ip_or_iface>']
    ip = get_first_ip(hostname)
    if not ip:
        ip = socket.gethostbyname(hostname)
    port = int(args['<port>'], 0)
    elink = int(args['<elink>'], 0)

    fids = []
    for i in range(len(args['<did>'])):
        did = int(args['<did>'][i], 0)
        cid = int(args['<cid>'][i], 0)
        fid = FID.get_fid(did, cid, elink, stream_id=0, is_to_flx=False, is_virtual=True)
        fids.append(fid)

    bus = FelixBus()
    bus.set_path(bus_dir)
    bus.set_groupname(bus_groupname)

    info = FelixBusInfo()
    info.ip = ip
    info.port = port
    info.unbuffered = False
    info.pubsub = True
    info.netio_pages = num_pages
    info.netio_pagesize = pagesize

    for fid in fids:
        if verbose:
            print("Publishing on bus elink", elink, "on", hex(fid), fid)
        bus.publish(fid, os.path.basename(fifo), info)

    # FIXME: check if its a fifo
    pipe = os.open(fifo, os.O_RDONLY | os.O_NONBLOCK)
    if verbose:
        print("Opened fifo", fifo)

    if verbose:
        print("netio_context")
    context = ffi.new("struct netio_context *")

    if verbose:
        print("netio_init")
    lib.netio_init(context)

    if verbose:
        print("netio_eventloop and cb")
    evloop = ffi.addressof(context.evloop)
    evloop.cb_init = lib.on_init

    print("inotify setup, watching", fifo)
    inotify = INotify()
    watch_flags = flags.DELETE | flags.MODIFY | flags.DELETE_SELF
    # watch_flags = masks.ALL_EVENTS  # careful: you will see your own events if you read the file, OPEN, ACCESS, CLOSE_NOWRITE
    wd = inotify.add_watch(fifo, watch_flags)

    if verbose:
        print("netio_register_read and cb")
    event_context = ffi.new("struct netio_event_context *")
    event_context.fd = inotify.fileno()
    lib.netio_register_read_fd(evloop, event_context)
    event_context.cb = lib.on_file

    if verbose:
        print("netio_socket setup")
    attr = ffi.new("struct netio_buffered_socket_attr *")
    attr.num_pages = num_pages
    attr.pagesize = pagesize * 1024
    attr.watermark = watermark * 1024

    netio_socket = ffi.new("struct netio_publish_socket *")
    lib.netio_publish_socket_init(netio_socket, context, hostname.encode(encoding="UTF-8"), port, attr)
    netio_socket.cb_subscribe = lib.on_publish_subscribe
    netio_socket.cb_connection_established = lib.on_publish_connection_established
    netio_socket.cb_connection_closed = lib.on_publish_connection_closed
    netio_socket.cb_buffer_available = lib.on_publish_buffer_available

    if verbose:
        print("EventLoop")
    lib.netio_run(evloop)

    if verbose:
        print("Closing")
    os.close(pipe)

    print("Never Ends")
