/*
 * Copyright (C) by Jocelyn Turcotte <jturcotte@woboq.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 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 General Public License
 * for more details.
 */

#include "socketapisocket_mac.h"
#import <Cocoa/Cocoa.h>

@protocol ChannelProtocol <NSObject>
- (void)sendMessage:(NSData*)msg;
@end

@protocol RemoteEndProtocol <NSObject, ChannelProtocol>
- (void)registerTransmitter:(id)tx;
@end

@interface LocalEnd : NSObject <ChannelProtocol>
@property SocketApiSocketPrivate *wrapper;
- (instancetype)initWithWrapper:(SocketApiSocketPrivate *)wrapper;
@end

@interface Server : NSObject
@property SocketApiServerPrivate *wrapper;
- (instancetype)initWithWrapper:(SocketApiServerPrivate *)wrapper;
- (void)registerClient:(NSDistantObject <RemoteEndProtocol> *)remoteEnd;
@end

class SocketApiSocketPrivate
{
public:
    SocketApiSocket *q_ptr;

    SocketApiSocketPrivate(NSDistantObject <ChannelProtocol> *remoteEnd);
    ~SocketApiSocketPrivate();

    NSDistantObject <ChannelProtocol> *remoteEnd;
    LocalEnd *localEnd;
    QByteArray inBuffer;
};

class SocketApiServerPrivate
{
public:
    SocketApiServer *q_ptr;

    SocketApiServerPrivate();
    ~SocketApiServerPrivate();

    QList<SocketApiSocket*> pendingConnections;
    NSConnection *connection;
    Server *server;
};


@implementation LocalEnd
- (instancetype)initWithWrapper:(SocketApiSocketPrivate *)wrapper
{
    self = [super init];
    self->_wrapper = wrapper;
    return self;
}

- (void)sendMessage:(NSData*)msg
{
    if (_wrapper) {
        _wrapper->inBuffer += QByteArray::fromRawNSData(msg);
        emit _wrapper->q_ptr->readyRead();
    }
}

- (void)connectionDidDie:(NSNotification*)notification
{
#pragma unused(notification)
    if (_wrapper)
        emit _wrapper->q_ptr->disconnected();
}
@end

@implementation Server
- (instancetype)initWithWrapper:(SocketApiServerPrivate *)wrapper
{
    self = [super init];
    self->_wrapper = wrapper;
    return self;
}

- (void)registerClient:(NSDistantObject <RemoteEndProtocol> *)remoteEnd
{
    // This saves a few mach messages that would otherwise be needed to query the interface
    [remoteEnd setProtocolForProxy:@protocol(RemoteEndProtocol)];

    SocketApiServer *server = _wrapper->q_ptr;
    SocketApiSocketPrivate *socketPrivate = new SocketApiSocketPrivate(remoteEnd);
    SocketApiSocket *socket = new SocketApiSocket(server, socketPrivate);
    _wrapper->pendingConnections.append(socket);
    emit server->newConnection();

    [remoteEnd registerTransmitter:socketPrivate->localEnd];
}
@end


SocketApiSocket::SocketApiSocket(QObject *parent, SocketApiSocketPrivate *p)
    : QIODevice(parent)
    , d_ptr(p)
{
    Q_D(SocketApiSocket);
    d->q_ptr = this;
    open(ReadWrite);
}

SocketApiSocket::~SocketApiSocket()
{
}

qint64 SocketApiSocket::readData(char *data, qint64 maxlen)
{
    Q_D(SocketApiSocket);
    qint64 len = std::min(maxlen, static_cast<qint64>(d->inBuffer.size()));
    memcpy(data, d->inBuffer.constData(), len);
    d->inBuffer.remove(0, len);
    return len;
}

qint64 SocketApiSocket::writeData(const char *data, qint64 len)
{
    @try {
        Q_D(SocketApiSocket);
        // FIXME: The NSConnection will make this block unless the function is marked as "oneway"
        // in the protocol. This isn't async and reduces our performances but this currectly avoids
        // a Mach queue deadlock during requests bursts of the legacy OwnCloudFinder extension.
        // Since FinderSync already runs in a separate process, blocking isn't too critical.
        [d->remoteEnd sendMessage:[NSData dataWithBytesNoCopy:const_cast<char *>(data) length:len freeWhenDone:NO]];
        return len;
    } @catch(NSException* e) {
        // connectionDidDie can be notified too late, also interpret any sending exception as a disconnection.
        emit disconnected();
        return -1;
    }
}

qint64 SocketApiSocket::bytesAvailable() const
{
    Q_D(const SocketApiSocket);
    return d->inBuffer.size() + QIODevice::bytesAvailable();
}

bool SocketApiSocket::canReadLine() const
{
    Q_D(const SocketApiSocket);
    return d->inBuffer.indexOf('\n', int(pos())) != -1 || QIODevice::canReadLine();
}

SocketApiSocketPrivate::SocketApiSocketPrivate(NSDistantObject <ChannelProtocol> *remoteEnd)
    : remoteEnd(remoteEnd)
    , localEnd([[LocalEnd alloc] initWithWrapper:this])
{
    [remoteEnd retain];
    // (Ab)use our objective-c object just to catch the notification
    [[NSNotificationCenter defaultCenter] addObserver:localEnd
        selector:@selector(connectionDidDie:)
        name:NSConnectionDidDieNotification
        object:[remoteEnd connectionForProxy]];
}

SocketApiSocketPrivate::~SocketApiSocketPrivate()
{
    [remoteEnd release];
    // The DO vended localEnd might still be referenced by the connection
    localEnd.wrapper = nil;
    [localEnd release];
}

SocketApiServer::SocketApiServer()
    : d_ptr(new SocketApiServerPrivate)
{
    Q_D(SocketApiServer);
    d->q_ptr = this;
}

SocketApiServer::~SocketApiServer()
{
}

void SocketApiServer::close()
{
    // Assume we'll be destroyed right after
}

bool SocketApiServer::listen(const QString &name)
{
    Q_D(SocketApiServer);
    // Set the name of the root object
    return [d->connection registerName:name.toNSString()];
}

SocketApiSocket *SocketApiServer::nextPendingConnection()
{
    Q_D(SocketApiServer);
    return d->pendingConnections.takeFirst();
}

SocketApiServerPrivate::SocketApiServerPrivate()
{
    // Create the connection and server object to vend over Disributed Objects
    connection = [[NSConnection alloc] init];
    server = [[Server alloc] initWithWrapper:this];
    [connection setRootObject:server];
}

SocketApiServerPrivate::~SocketApiServerPrivate()
{
    [connection release];
    server.wrapper = nil;
    [server release];
}
