#ifdef HAVE_CONFIG_H
    #include "config.h"
#endif

#include "clientConnection.h"
#include "stubServer.h"
#include "objectSummary.h"
#include "testUtils.h"
#include "agent.h"

#include <Eris/LogStream.h>
#include <Eris/Exceptions.h>
#include <Atlas/Objects/Encoder.h>
#include <Atlas/Objects/RootOperation.h>
#include <Atlas/Objects/Operation.h>
#include <Atlas/Codecs/Bach.h>

using Atlas::Objects::Root;
using Atlas::Objects::smart_dynamic_cast;
using namespace Atlas::Objects::Operation;
using namespace Eris;
using Atlas::Objects::Entity::RootEntity;

typedef Atlas::Objects::Entity::Account AtlasAccount;

ClientConnection::ClientConnection(StubServer* ss, int socket) :
    m_stream(socket),
    m_server(ss),
    m_codec(NULL),
    m_encoder(NULL)
{
    m_acceptor = new Atlas::Net::StreamAccept("Eris Stub Server", m_stream);
    m_acceptor->poll(false);
}

ClientConnection::~ClientConnection()
{
    for (AgentMap::iterator A=m_agents.begin(); A != m_agents.end(); ++A) {
        delete A->second;
    }

    delete m_encoder;
    delete m_acceptor;
    delete m_codec;
}

void ClientConnection::poll()
{
    if (m_stream.eof()) {
        fail();
        return;
    }
    
    if (m_acceptor)
    {
        negotiate();
    }
    else
    {
        m_codec->poll();
        
        while (!m_objDeque.empty())
        {
            RootOperation op = smart_dynamic_cast<RootOperation>(m_objDeque.front());
            if (!op.isValid())
                throw InvalidOperation("ClientConnection recived something that isn't an op");
            
            dispatch(op);
            m_objDeque.pop_front();
        }
    }
}

bool ClientConnection::isDisconnected()
{
    return !m_stream.is_open();
}

Agent* ClientConnection::findAgentForEntity(const std::string& eid) const
{
    AgentMap::const_iterator it = m_agents.find(eid);
    if (it != m_agents.end()) return it->second;
    return NULL;
}

void ClientConnection::shutdown()
{
    m_stream.shutdown();
}

#pragma mark -
// basic Atlas connection / stream stuff

void ClientConnection::fail()
{
    m_stream.close();
}

void ClientConnection::negotiate()
{
    m_acceptor->poll(); 
    
    switch (m_acceptor->getState()) {
    case Atlas::Net::StreamAccept::IN_PROGRESS:
        break;

    case Atlas::Net::StreamAccept::FAILED:
        error() << "ClientConnection got Atlas negotiation failure";
        fail();
        break;

    case Atlas::Net::StreamAccept::SUCCEEDED:
        m_codec = m_acceptor->getCodec(*this);
        m_encoder = new Atlas::Objects::ObjectsEncoder(*m_codec);
        m_codec->streamBegin();
                
        delete m_acceptor;
        m_acceptor = NULL;
        break;

    default:
        throw InvalidOperation("unknown state from Atlas StreamAccept in ClientConnection::negotiate");
    }             
}

void ClientConnection::objectArrived(const Root& obj)
{
/*
    std::stringstream debugStream;
    Atlas::Codecs::Bach debugCodec(debugStream, NULL);
    Atlas::Objects::ObjectsEncoder debugEncoder(debugCodec);
    debugEncoder.streamObjectsMessage(obj);
    debugStream << std::flush;

    std::cout << "recieved:" << debugStream.str() << std::endl;
  */
    m_objDeque.push_back(obj);
}

#pragma mark -
// dispatch / decode logic

void ClientConnection::dispatch(const RootOperation& op)
{    
    const std::vector<Root>& args = op->getArgs();
    
    if (op->getFrom().empty())
    {        
        if (m_account.empty())
        {
            // not logged in yet
            if (op->getClassNo() == LOGIN_NO) {
                processLogin(smart_dynamic_cast<Login>(op));
                return;
            }
       
            if (op->getClassNo() == CREATE_NO) {
                assert(!args.empty());
                processAccountCreate(smart_dynamic_cast<Create>(op));
                return;
            }
        }
        
        if (op->getClassNo() == GET_NO) {
            processAnonymousGet(smart_dynamic_cast<Get>(op));
            return;
        }
        
        if (op->getClassNo() == LOGOUT_NO) {
            return;
        }
        
        throw TestFailure("got anonymous op I couldn't dispatch");
    }
    
    if (op->getFrom() == m_account) {
        dispatchOOG(op);
        return;
    }
    
    if (m_agents.count(op->getFrom())) {
        m_agents[op->getFrom()]->processOp(op);
        return;
    }


    if (entityIsCharacter(op->getFrom())) {
        // IG activation
        activateCharacter(op->getFrom(), op);
        return;
    }
    
        
    debug() << "totally failed to handle operation " << objectSummary(op);
}

void ClientConnection::dispatchOOG(const RootOperation& op)
{
    switch (op->getClassNo()) {
    case LOOK_NO:
        processOOGLook(smart_dynamic_cast<Look>(op));
        return;
    
    case MOVE_NO:
        return;
        
    case TALK_NO: {
        Talk tk = smart_dynamic_cast<Talk>(op);
        processTalk(tk);
        return;
    }
   
    case LOGOUT_NO: {
        Logout logout = smart_dynamic_cast<Logout>(op);
        
        Info logoutInfo;
        logoutInfo->setArgs1(logout);
        logoutInfo->setTo(m_account);
        logoutInfo->setRefno(logout->getSerialno());
        send(logoutInfo);
        return;
    }
    
    case CREATE_NO: {
        const StringList& arg0Parents(op->getArgs().front()->getParents());
        if (arg0Parents.front() == "__fail__") {
            sendError("bad type for char creation", op);
            return;
        }
        
        if (arg0Parents.front() == "settler") {
            createCharacter(op);
            return;
        }
    }
    
    default:
        error() << "clientConnection failed to handle OOG op";
    } // of classNo switch
}

void ClientConnection::processLogin(const Login& login)
{
    const std::vector<Root>& args = login->getArgs();
    if (!args.front()->hasAttr("password") || !args.front()->hasAttr("username"))
        throw InvalidOperation("got bad LOGIN op");
    
    std::string username = args.front()->getAttr("username").asString();
    if (username == "_timeout_") {
        // deliberately send nothing
        return;
    }
    
    AccountMap::const_iterator A = m_server->findAccountByUsername(username);
    if (A  == m_server->m_accounts.end())
    {
        sendError("unknown account: " + username, login);
        return;
    }
    
    if (A->second->getPassword() != args.front()->getAttr("password").asString())
    {
        sendError("bad password", login);
        return;
    }
    
    // update the really important member variable
    m_account = A->second->getId();
    
    Info loginInfo;
    loginInfo->setArgs1(A->second);
    loginInfo->setTo(m_account);
    loginInfo->setRefno(login->getSerialno());
    send(loginInfo);
    
    m_server->joinRoom(m_account, "_lobby");
    m_server->resetWorld();
}

void ClientConnection::processAccountCreate(const Create& cr)
{
    const std::vector<Root>& args = cr->getArgs();
    if (args.empty()) {
        sendError("missing account in create", cr);
        return;
    }
    
    // check for duplicate username
    AtlasAccount acc = smart_dynamic_cast<AtlasAccount>(args.front());
    if (!acc.isValid()) {
        sendError("malformed account in create", cr);
        return;
    }
    
    AccountMap::const_iterator A = m_server->findAccountByUsername(acc->getUsername());
    if (A  != m_server->m_accounts.end()) {
        sendError("duplicate account: " + acc->getUsername(), cr);
        return;
    }
    
    m_account = std::string("_") + acc->getUsername() + "_123";
    acc->setId(m_account);
    m_server->m_accounts[m_account] = acc;
    
    Info createInfo;
    createInfo->setArgs1(acc);
    createInfo->setTo(m_account);
    createInfo->setRefno(cr->getSerialno());
    send(createInfo);
    
    m_server->joinRoom(m_account, "_lobby");
    m_server->resetWorld();
}

void ClientConnection::processOOGLook(const Look& lk)
{
    const std::vector<Root>& args = lk->getArgs();
    std::string lookTarget;
                
    if (args.empty()) {
        lookTarget = "_lobby";
    } else {
        lookTarget = args.front()->getId();
    }
    
    RootEntity thing;
    if (m_server->m_accounts.count(lookTarget))
    {
        thing = m_server->m_accounts[lookTarget];
        if (lookTarget != lk->getFrom())
        {
            // prune
            thing->removeAttr("characters");
            thing->removeAttr("password");
        }
    }
    else if (m_server->m_world.count(lookTarget))
    {
        // ensure it's owned by the account, i.e in characters
        if (!entityIsCharacter(lookTarget))
        {
            sendError("not allowed to look at that entity", lk);
            return;
        }
        
        thing = m_server->m_world[lookTarget];
    }
    else if (m_server->m_rooms.count(lookTarget))
    {
        // should check room view permissions?
        thing = m_server->m_rooms[lookTarget];
    } else {
        // didn't find any entity with the id
        sendError("processed OOG look for unknown entity " + lookTarget, lk);
        return;
    }
    
    Sight st;
    st->setArgs1(thing);
    st->setFrom(lookTarget);
    st->setTo(lk->getFrom());
    st->setRefno(lk->getSerialno());
    send(st);
}

void ClientConnection::processTalk(const Atlas::Objects::Operation::Talk& tk)
{
    if (m_server->m_rooms.count(tk->getTo()))
        return m_server->talkInRoom(tk, tk->getTo());
    
    if (m_server->m_accounts.count(tk->getTo())) {
        ClientConnection* cc = m_server->getConnectionForAccount(tk->getTo());
        if (!cc) {
            sendError("oog chat: account is offline", tk);
            return;
        }
        
        Sound snd;
        snd->setFrom(m_account); 
        snd->setArgs1(tk);
        snd->setTo(cc->m_account);
        cc->send(snd);
        return;
    }
    
    if (tk->getTo().empty()) {
        // lobby chat
        return m_server->talkInRoom(tk, "_lobby");
    }
    
    sendError("bad TO for OOG TALK op: " + tk->getTo(), tk);
}

void ClientConnection::processAnonymousGet(const Get& get)
{
    const std::vector<Root>& args = get->getArgs();
    if (args.empty())
    {
        Info serverInfo;
        RootEntity svObj;
        
        Atlas::Message::ListType prs;
        prs.push_back("server");
        svObj->setParentsAsList(prs);
        
        svObj->setName("Bob's StubServer");
        svObj->setAttr("server", "stubserver");
        svObj->setAttr("ruleset", "stub-world");
        svObj->setAttr("uptime", 666.0);
        svObj->setAttr("clients", 42);
        
        serverInfo->setArgs1(svObj);
        send(serverInfo);
    } else {
        std::string typeName = args.front()->getId();

        if (m_server->m_types.count(typeName))
        {
            Info typeInfo;
            typeInfo->setArgs1(m_server->m_types[typeName]);
            typeInfo->setRefno(get->getSerialno());
            send(typeInfo);
        } else
            sendError("unknown type " + typeName, get);
    }
}

void ClientConnection::activateCharacter(const std::string& charId, const RootOperation& op)
{
    // special magic testing IDs
    if (charId == "_fail_") {
        sendError("deliberate", op);
        return;
    }

    assert(entityIsCharacter(charId));
    //debug() << "activation, inbound op's serial is " << op->getSerialno();
    
    if (m_agents.count(charId)) {
        sendError("duplicate character action", op);
        return;
    }
    
    Agent* ag = new Agent(this, charId);
    m_agents[charId] = ag;
    
    Info info;
    info->setArgs1(m_server->m_world[charId]);
    info->setFrom(charId);
    info->setTo(m_account); // I *think* this is right
    info->setRefno(op->getSerialno());
    
    send(info);
    ag->processOp(op); // process as normal
}

void ClientConnection::createCharacter(const RootOperation& op)
{
    static unsigned int charCounter = 0;
    char charId[64];
    ::snprintf(charId, 64, "_customChar_%d", ++charCounter);
    
    RootEntity ent = smart_dynamic_cast<RootEntity>(op->getArgs().front());
    
    ent->setId(charId);
    ent->setLoc("_world");
    m_server->m_world[charId] = ent;

    StringList children(m_server->m_world["_world"]->getContains());
    children.push_back(charId);
    m_server->m_world["_world"]->setContains(children);

    Agent* ag = new Agent(this, charId);
    ag->setEntityVisible(charId, true);
    
    m_agents[charId] = ag;
    
    Info info;
    info->setArgs1(m_server->m_world[charId]);
    info->setFrom(charId);
    info->setTo(m_account); // I *think* this is right
    info->setRefno(op->getSerialno());
    
    send(info);
}

#pragma mark -

void ClientConnection::sendError(const std::string& msg, const RootOperation& op)
{
    Error errOp;
    
    errOp->setRefno(op->getSerialno());
    errOp->setTo(op->getFrom());
    
    Root arg0;
    arg0->setAttr("message", msg);
    
    std::vector<Root>& args = errOp->modifyArgs();
    args.push_back(arg0);
    args.push_back(op);
    
    send(errOp);
}

void ClientConnection::send(const Root& obj)
{
    m_encoder->streamObjectsMessage(obj);
    m_stream << std::flush;
}

bool ClientConnection::entityIsCharacter(const std::string& id)
{
    assert(!m_account.empty());
    AtlasAccount p = m_server->m_accounts[m_account];
    StringSet characters(p->getCharacters().begin(),  p->getCharacters().end());
    
    return characters.count(id);
}

std::ostream& operator<<(std::ostream& io, const objectSummary& summary)
{
    const StringList& parents = summary.m_obj->getParents();
    if (parents.size() == 0)
    {
        io << "un-typed object";
    } else {
        io << parents.front();
    }
    
    return io;
}


syntax highlighted by Code2HTML, v. 0.9.1