/*
Copyright (C) 2005 David Green <green@couchpotato.net>

This file is part of I3J.

I3J 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.

I3J 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.

You should have received a copy of the GNU General Public License
along with I3J; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

package com.aelfengard.i3;

import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import com.aelfengard.i3.packet.ChanListReplyPacket;
import com.aelfengard.i3.packet.ChannelEPacket;
import com.aelfengard.i3.packet.ChannelListenPacket;
import com.aelfengard.i3.packet.ChannelMPacket;
import com.aelfengard.i3.packet.ChannelTPacket;
import com.aelfengard.i3.packet.EmoteToPacket;
import com.aelfengard.i3.packet.ErrorPacket;
import com.aelfengard.i3.packet.I3Packet;
import com.aelfengard.i3.packet.MudListPacket;
import com.aelfengard.i3.packet.StartupReq3Packet;
import com.aelfengard.i3.packet.TellPacket;
import com.aelfengard.i3.packet.WhoReplyPacket;
import com.aelfengard.i3.packet.WhoReqPacket;

public class I3Client {

    private static final String DEFAULT_ROUTER_NAME = "*gjs";
    private static final int DEFAULT_PASSWORD = -1;

    private final Map<String,MudInfo> mudMap = new TreeMap<String,MudInfo>();
    private final Map<String,ChanInfo> channelMap = new TreeMap<String,ChanInfo>();
    private final Map<String,Set<I3ChannelListener>> channelListeners = new HashMap<String,Set<I3ChannelListener>>();
    private final Set<I3EventListener> listeners = new HashSet<I3EventListener>();
    
    private long lastReconnect = 0;
    
    private String host = I3Connection.DEFAULT_HOST;
    private int port = I3Connection.DEFAULT_PORT;
    private String routerName = DEFAULT_ROUTER_NAME;
    private int password = DEFAULT_PASSWORD;
    
    private String mudName;
    private String adminEmail;
    private int playerPort = -1;
    private String mudLib = "";
    private String baseMudLib = "";
    private String driver = "";
    private String mudType = "";
    private String openStatus = "";
    
    private I3Connection conn;
    
    public void autoconnect() {
        synchronized(I3Client.this) {
            if (conn != null) {
                return; // already connected
            }
        }
        long now = System.currentTimeMillis();
        long nextReconnect = lastReconnect + 300000; // 5 minutes between reconnects
        if (nextReconnect < now) {
            nextReconnect = now;
        }
        lastReconnect = nextReconnect;
        final long delay = nextReconnect - now;
        new Thread() {
            public void run() {
                try {
                    if (delay > 0) {
                        Thread.sleep(delay);
                    }
                }
                catch (InterruptedException ex) {
                    ex.printStackTrace();
                    return;
                }
                try {
                    connect();
                }
                catch (IOException ex) {
                    autoconnect();
                }
            }
        }.start();
    }
    
    public void connect() throws IOException {
        synchronized(this) {
            if (conn != null) {
                return; // already connected
            }
        }
        if (mudName == null) {
            throw new IllegalStateException("mudName is null");
        }
        if (adminEmail == null) {
            throw new IllegalStateException("adminEmail is null");
        }
        
        if (System.getProperty("i3.debug") != null) {
            System.err.println("I3 Connecting...");
        }
        I3Connection lconn = new I3Connection(host, port);
        if (System.getProperty("i3.debug") != null) {
            System.err.println("I3 Connected.");
        }
        
        synchronized(this) {
            conn = lconn;
            conn.addPacketListener(new MyPacketListener());

            StartupReq3Packet packet = new StartupReq3Packet();
            packet.setTargetMudName(new LPCMixed(routerName));
            packet.setPassword(new LPCMixed(password));
            packet.setOriginatorMudName(new LPCMixed(mudName));
            MudInfo info = packet.getMudInfo();
            info.setAdminEmail(new LPCMixed(adminEmail));
            info.setPlayerPort(new LPCMixed(playerPort));
            info.setMudLib(new LPCMixed(mudLib));
            info.setBaseMudLib(new LPCMixed(baseMudLib));
            info.setDriver(new LPCMixed(driver));
            info.setMudType(new LPCMixed(mudType));
            info.setOpenStatus(new LPCMixed(openStatus));
            Map<LPCMixed,LPCMixed> services = new LinkedHashMap<LPCMixed,LPCMixed>();
            services.put(new LPCMixed(MudInfo.SERVICE_TYPE_TELL), new LPCMixed(1));
            services.put(new LPCMixed(MudInfo.SERVICE_TYPE_EMOTE_TO), new LPCMixed(1));
            services.put(new LPCMixed(MudInfo.SERVICE_TYPE_CHANNEL), new LPCMixed(1));
            services.put(new LPCMixed(MudInfo.SERVICE_TYPE_WHO), new LPCMixed(1));
            info.setServices(new LPCMixed(services));
            //info.setService(MudInfo.SERVICE_TYPE_LOCATE, 1);
            //info.setService(MudInfo.SERVICE_TYPE_FINGER, 1);
            conn.send(packet);
        }
        for (String channel : channelListeners.keySet()) {
            modifyChannelDirect(channel, true);
        }
        
    }

    public String getAdminEmail() {
        return adminEmail;
    }
    

    public void setAdminEmail(String adminEmail) {
        this.adminEmail = adminEmail;
    }
    

    public String getBaseMudLib() {
        return baseMudLib;
    }
    

    public void setBaseMudLib(String baseMudLib) {
        this.baseMudLib = baseMudLib;
    }
    

    public String getDriver() {
        return driver;
    }
    

    public void setDriver(String driver) {
        this.driver = driver;
    }
    

    public String getMudLib() {
        return mudLib;
    }
    

    public void setMudLib(String mudLib) {
        this.mudLib = mudLib;
    }
    

    public String getMudType() {
        return mudType;
    }
    

    public void setMudType(String mudType) {
        this.mudType = mudType;
    }
    

    public String getOpenStatus() {
        return openStatus;
    }
    

    public void setOpenStatus(String openStatus) {
        this.openStatus = openStatus;
    }
    

    public int getPlayerPort() {
        return playerPort;
    }
    

    public void setPlayerPort(int playerPort) {
        this.playerPort = playerPort;
    }
    

    public String getHost() {
        return host;
    }
    

    public void setHost(String host) {
        this.host = host;
    }
    

    public String getMudName() {
        return mudName;
    }
    

    public void setMudName(String mudName) {
        this.mudName = mudName;
    }
    

    public int getPassword() {
        return password;
    }
    

    public void setPassword(int password) {
        this.password = password;
    }
    

    public int getPort() {
        return port;
    }
    

    public void setPort(int port) {
        this.port = port;
    }
    

    public String getRouterName() {
        return routerName;
    }
    

    public void setRouterName(String routerName) {
        this.routerName = routerName;
    }
    
    public synchronized void addChannelListener(String channel, I3ChannelListener who) {
        Set<I3ChannelListener> set = channelListeners.get(channel);
        if (set == null) {
            set = new HashSet<I3ChannelListener>();
            channelListeners.put(channel, set);
            modifyChannelDirect(channel, true);
        }
        set.add(who);
    }
    
    public synchronized void removeChannelListener(String channel, I3ChannelListener who) {
        Set<I3ChannelListener> set = channelListeners.get(channel);
        if (set != null) {
            set.remove(who);
            if (set.isEmpty()) {
                channelListeners.remove(channel);
                modifyChannelDirect(channel, false); // noone listening anymore
            }
        }
    }
    
    public synchronized void addEventListener(I3EventListener l) {
        listeners.add(l);
    }
    
    public synchronized void removeEventListener(I3EventListener l) {
        listeners.remove(l);
    }
    
    public void modifyChannelDirect(String channel, boolean isOn) {
        ChannelListenPacket packet = new ChannelListenPacket();
        packet.setOriginatorMudName(new LPCMixed(mudName));
        packet.setTargetMudName(new LPCMixed(routerName));
        packet.setChannelName(new LPCMixed(channel));
        packet.setIsOn(new LPCMixed(isOn ? 1 : 0));
        synchronized(this) {
            if (conn != null) {
                conn.send(packet);
                if (System.getProperty("i3.debug") != null) {
                    System.err.println("I3Client: Channel " + channel + " is now " + (isOn ? "ON" : "OFF"));
                }
            }
        }
    }
        
    private synchronized void handleMudList(MudListPacket packet) {
        Map<LPCMixed,LPCMixed> mudList = packet.getInfoMapping().asMap();
        if (mudList == null) {
            return;
        }
        for (Map.Entry<LPCMixed,LPCMixed> entry : mudList.entrySet()) {
            String name = entry.getKey().asString();
            List<LPCMixed> list = entry.getValue().asList();
            if (list == null) {
                mudMap.remove(name);
            }
            else {
                mudMap.put(name, new MudInfo(list));
            }
        }
    }
    
    private synchronized void handleChanList(ChanListReplyPacket packet) {
        Map<LPCMixed,LPCMixed> channelList = packet.getChannelList().asMap(true);
        for (Map.Entry<LPCMixed,LPCMixed> entry : channelList.entrySet()) {
            String name = entry.getKey().asString();
            List<LPCMixed> list = entry.getValue().asList();
            if (list == null) {
                channelMap.remove(name);
            }
            else {
                channelMap.put(name, new ChanInfo(list));
            }
        }
    }
    
    private void handleChannelM(ChannelMPacket packet) {
        String channel = packet.getChannelName().asString();
        Set<I3ChannelListener> set = getChannelListeners(channel);
        if (set != null) {
            for (I3ChannelListener l : set) {
                l.i3Message(packet);
            }
        }
    }
    
    private synchronized Set<I3ChannelListener> getChannelListeners(String channel) {
        Set<I3ChannelListener> set = channelListeners.get(channel);
        if (set == null) {
            System.err.println("WARNING: Found unused channel: " + channel);
            ChannelListenPacket listenPacket = new ChannelListenPacket();
            listenPacket.setOriginatorMudName(new LPCMixed(mudName));
            listenPacket.setChannelName(new LPCMixed(channel));
            listenPacket.setTargetMudName(new LPCMixed(routerName));
            listenPacket.setIsOn(new LPCMixed(false));
            if (conn != null) {
                conn.send(listenPacket);
            }
            return null;
        }
        else {
            return new HashSet<I3ChannelListener>(set);
        }
    }
    
    private void handleTell(final TellPacket packet) {
        Set<I3EventListener> set;
        synchronized(this) {
            set = new HashSet<I3EventListener>(listeners);
        }
        for (I3EventListener l : set) {
            l.tell(packet, new ErrorCallback() {

                public void returnError(String reason) {
                    tellFailed(packet, reason);
                }
                
            });
        }
    }

    private void handleEmoteTo(final EmoteToPacket packet) {
        Set<I3EventListener> set;
        synchronized(this) {
            set = new HashSet<I3EventListener>(listeners);
        }
        for (I3EventListener l : set) {
            l.emoteTo(packet, new ErrorCallback() {

                public void returnError(String reason) {
                    emoteToFailed(packet, reason);
                }
                
            });
        }
    }

    private void handleChannelE(ChannelEPacket packet) {
        String channel = packet.getChannelName().asString();
        Set<I3ChannelListener> set = getChannelListeners(channel);
        if (set != null) {
            for (I3ChannelListener l : set) {
                l.i3Message(packet);
            }
        }
    }
    
    private void handleChannelT(ChannelTPacket packet) {
        String channel = packet.getChannelName().asString();
        Set<I3ChannelListener> set = getChannelListeners(channel);
        if (set != null) {
            for (I3ChannelListener l : set) {
                l.i3Message(packet);
            }
        }
    }
    
    private void handleWhoReply(WhoReplyPacket packet) {
        Set<I3EventListener> set;
        synchronized(this) {
            set = new HashSet<I3EventListener>(listeners);
        }
        List<LPCMixed> whoData = packet.getWhoData().asList();
        if (whoData != null) {
            for (I3EventListener l : set) {
                l.whoReply(packet.getTargetUsername(), packet.getOriginatorMudName(), whoData);
            }
        }
    }
    
    private void handleWhoReq(final WhoReqPacket packet) {
        new Thread() {
            public void run() {
                Set<I3EventListener> set;
                synchronized(this) {
                    set = new HashSet<I3EventListener>(listeners);
                }
                for (I3EventListener l : set) {
                    List<LPCMixed> list = l.whoRequest();
                    if (list == null) {
                        continue;
                    }
                    WhoReplyPacket reply = new WhoReplyPacket();
                    reply.setOriginatorMudName(new LPCMixed(mudName));
                    reply.setTargetMudName(packet.getOriginatorMudName());
                    reply.setTargetUsername(packet.getOriginatorUsername());
                    reply.setWhoData(new LPCMixed(list));
                    synchronized(this) {
                        if (conn != null) {
                            conn.send(reply);
                        }
                    }
                    break; // don't look for any more replies
                }
            }
        }.start();
    }

    private void handleError(ErrorPacket packet) {
        String errcode = packet.getErrorCode().asString();
        if (errcode.equals(ErrorPacket.ROUTER_ERROR_CODE_OPERATION_NOT_ALLOWED)) {
            I3Packet tmp = packet.getErrorPacket();
            if (tmp != null) {
                // figure out *what* wasn't allowed...
                switch (tmp.getType()) {
                    case CHANNEL_LISTEN: {
                        ChannelListenPacket listenPacket = (ChannelListenPacket) tmp;
                        // unregister each player for this channel
                        // and let them know what happened
                        String channel = listenPacket.getChannelName().asString();
                        Set<I3ChannelListener> set;
                        synchronized(this) {
                            set = channelListeners.get(channel);
                            if (set != null) {
                                set = new HashSet<I3ChannelListener>(set);
                            }
                        }
                        if (set != null) {
                            Iterator<I3ChannelListener> iter = set.iterator(); 
                            while (iter.hasNext()) {
                                I3ChannelListener l = iter.next();
                                l.channelRemoved(channel, packet.getErrorMessage());
                                iter.remove();
                            }
                            channelListeners.remove(channel);
                        }
                        return; // handled
                    }
                }
            }
        }
        else if (errcode.equals(ErrorPacket.ROUTER_ERROR_CODE_UNKNOWN_DESTINATION_MUD)) {
            I3Packet tmp = packet.getErrorPacket();
            if (tmp != null) {
                // figure out *what* wasn't allowed...
                switch (tmp.getType()) {
                    case TELL: {
                        TellPacket tellPacket = (TellPacket) tmp;
                        // failed tell attempt
                        Set<I3EventListener> set;
                        synchronized(this) {
                            set = new HashSet<I3EventListener>(listeners);
                        }
                        for (I3EventListener l : set) {
                            l.tellFailed(packet.getTargetUsername(), tellPacket.getTargetMudName(), tellPacket.getTargetUsername(), packet.getErrorMessage());
                        }
                        return; // handled
                    }
                    case WHO_REQ: {
                        WhoReqPacket whoPacket = (WhoReqPacket) tmp;
                        // failed who attempt
                        Set<I3EventListener> set;
                        synchronized(this) {
                            set = new HashSet<I3EventListener>(listeners);
                        }
                        for (I3EventListener l : set) {
                            l.whoFailed(packet.getTargetUsername(), whoPacket.getTargetMudName(), packet.getErrorMessage());
                        }
                        return; // handled
                    }
                }
            }
        }
        else if (errcode.equals(ErrorPacket.MUD_ERROR_CODE_UNKNOWN_TARGET_USER)) {
            I3Packet tmp = packet.getErrorPacket();
            if (tmp != null) {
                switch (tmp.getType()) {
                    case TELL: {
                        TellPacket tellPacket = (TellPacket) tmp;
                        // failed tell attempt
                        Set<I3EventListener> set;
                        synchronized(this) {
                            set = new HashSet<I3EventListener>(listeners);
                        }
                        for (I3EventListener l : set) {
                            l.tellFailed(packet.getTargetUsername(), tellPacket.getTargetMudName(), tellPacket.getTargetUsername(), packet.getErrorMessage());
                        }
                        return; // handled
                    }
                }
            }
        }
        Set<I3EventListener> set;
        synchronized(this) {
            set = new HashSet<I3EventListener>(listeners);
        }
        for (I3EventListener l : set) {
            l.i3Error(packet);
        }
    }

    public synchronized boolean isConnected() {
        return conn != null;
    }
    
    public synchronized Map<String,MudInfo> getMudList() {
        return new LinkedHashMap<String,MudInfo>(mudMap);
    }
    
    public synchronized Map<String,ChanInfo> getChannelList() {
        return new LinkedHashMap<String,ChanInfo>(channelMap);
    }

    private class MyPacketListener implements I3PacketListener {

        public void packetReceived(I3Connection c, I3Packet packet) {
            switch(packet.getType()) {
                // TODO: Handle startup reply properly, reconnect to another router if asked
                case MUD_LIST: 
                        handleMudList((MudListPacket) packet); break;
                case CHANLIST_REPLY:
                        handleChanList((ChanListReplyPacket) packet); break;
                case CHANNEL_M:
                        handleChannelM((ChannelMPacket) packet); break;
                case CHANNEL_E:
                        handleChannelE((ChannelEPacket) packet); break;
                case CHANNEL_T:
                        handleChannelT((ChannelTPacket) packet); break;
                case EMOTE_TO:
                        handleEmoteTo((EmoteToPacket) packet); break;
                case TELL:
                        handleTell((TellPacket) packet); break;
                case WHO_REQ:
                        handleWhoReq((WhoReqPacket) packet); break;
                case WHO_REPLY:
                        handleWhoReply((WhoReplyPacket) packet); break;
                case ERROR:
                        handleError((ErrorPacket) packet); break;
                case STARTUP_REPLY:
                        // nothing to do yet.
                        break;
                default:
                    System.err.println("Warning: I3Client doesn't know how to handle this packet: " + packet);
            }
        }

        public void unrecognizedPacket(I3Connection c, String packet) {
            System.err.println("Warning: Unrecognized packet: " + packet);
        }

        public void connectionClosed(I3Connection c) {
            synchronized(I3Client.this) {
                conn = null;
            }
            autoconnect();
        }
        
    }
    
    public synchronized void send(TellPacket packet) throws I3NotConnectedException {
        if (conn == null) {
            throw new I3NotConnectedException();
        }
        packet.setOriginatorMudName(new LPCMixed(mudName));
        conn.send(packet);
    }
    
    public synchronized void send(ChannelMPacket packet) throws I3NotConnectedException {
        if (conn == null) {
            throw new I3NotConnectedException();
        }
        packet.setOriginatorMudName(new LPCMixed(mudName));
        conn.send(packet);
    }

    public synchronized void send(ChannelEPacket packet) throws I3NotConnectedException {
        if (conn == null) {
            throw new I3NotConnectedException();
        }
        packet.setOriginatorMudName(new LPCMixed(mudName));
        conn.send(packet);
    }

    public synchronized void send(ChannelTPacket packet) throws I3NotConnectedException {
        if (conn == null) {
            throw new I3NotConnectedException();
        }
        packet.setOriginatorMudName(new LPCMixed(mudName));
        conn.send(packet);
    }

    public synchronized boolean isValidChannel(String channel) {
        return channelMap.containsKey(channel);
    }

    public synchronized void sendWho(LPCMixed username, LPCMixed targetmud) throws I3NotConnectedException {
        if (conn == null) {
            throw new I3NotConnectedException();
        }
        WhoReqPacket packet = new WhoReqPacket();
        packet.setOriginatorMudName(new LPCMixed(mudName));
        packet.setOriginatorUsername(username);
        packet.setTargetMudName(targetmud);
        conn.send(packet);
    }

    public synchronized MudInfo getMudInfo(String mudname) {
        return mudMap.get(mudname);
    }

    private synchronized void tellFailed(TellPacket packet, String msg) {
        if (conn == null) {
            return;
        }
        ErrorPacket errorPacket = new ErrorPacket();
        errorPacket.setErrorCode(new LPCMixed(ErrorPacket.MUD_ERROR_CODE_UNKNOWN_TARGET_USER));
        errorPacket.setErrorMessage(new LPCMixed(msg));
        errorPacket.setErrorPacket(packet);
        errorPacket.setOriginatorMudName(new LPCMixed(mudName));
        errorPacket.setTargetMudName(packet.getOriginatorMudName());
        errorPacket.setTargetUsername(packet.getOriginatorUsername());
        conn.send(errorPacket);
    }

    private synchronized void emoteToFailed(EmoteToPacket packet, String msg) {
        if (conn == null) {
            return;
        }
        ErrorPacket errorPacket = new ErrorPacket();
        errorPacket.setErrorCode(new LPCMixed(ErrorPacket.MUD_ERROR_CODE_UNKNOWN_TARGET_USER));
        errorPacket.setErrorMessage(new LPCMixed(msg));
        errorPacket.setErrorPacket(packet);
        errorPacket.setOriginatorMudName(new LPCMixed(mudName));
        errorPacket.setTargetMudName(packet.getOriginatorMudName());
        errorPacket.setTargetUsername(packet.getOriginatorUsername());
        conn.send(errorPacket);
    }

}