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

This file is part of Aelfengard.

Aelfengard is proprietary software. You may not redistribute it without
prior written permission from the copyright holder.
*/

package server;

import java.io.IOException;
import java.io.InputStream;
import java.net.BindException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.TreeMap;

import javax.crypto.Cipher;

import server.login.LoginHandler;

import com.aelfengard.i3.ErrorCallback;
import com.aelfengard.i3.I3Client;
import com.aelfengard.i3.I3EventListener;
import com.aelfengard.i3.LPCMixed;
import com.aelfengard.i3.MudInfo;
import com.aelfengard.i3.packet.EmoteToPacket;
import com.aelfengard.i3.packet.ErrorPacket;
import com.aelfengard.i3.packet.TellPacket;
import common.ui.GameClient;


public class GameServer {

    private static final SecurityManager SECURITY_MANAGER = System.getSecurityManager();
    
    private static final Runnable[][] EMPTY_RUNNABLE_ARRAY = {};

    private static TreeMap<Long,Runnable> SCHEDULED_TASKS = new TreeMap<Long,Runnable>();

    private static List<Object> EXEC_QUEUE = new ArrayList<Object>();

    private static Selector selector;
    
    private static String gameMode;
    private static String myHostname;
    private static int myPort;
    private static String rcSubdir;
    
    public static final I3Client i3client = new I3Client();
    
    public static final Random RANDOM = new Random();

    public static void main(String... args) {
        if (args.length == 1 && "-mkdevdb".equals(args[0])) {
            PersistenceManager.MAKING_DEV_DB = true;
            PersistenceManager.doLoad();
            PersistenceManager.doInlineSave("aelfengard-dev.db");
            System.exit(0);
        }
        else if (args.length < 3 || args.length > 4) {
            usage();
        }
        System.setIn(new InputStream() {
            @Override
            public int read() throws IOException {
                return -1; // EOF
            }
        });
        gameMode = System.getProperty("aelfengard.gameMode");
        myHostname = args[0];
        myPort = Integer.parseInt(args[1]);
        rcSubdir = args[2];
        boolean launchClient = false;
        if (args.length > 3) {
            if (args[3].equals("-gui")) {
                launchClient = true;
            }
            else {
                usage();
            }
        }
        try {
            System.setOut(new TimeStampPrintStream(System.out));
            System.setErr(new TimeStampPrintStream(System.err));
            System.out.println("Aelfengard server starting up...");
            run(launchClient);
        } catch (Throwable t) {
            t.printStackTrace();
        } finally {
            System.exit(-1);
        }
    }

    private static void run(boolean launchClient) throws Exception {
        selector = Selector.open();
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.configureBlocking(false);
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        try {
            serverChannel.socket().bind(new InetSocketAddress(myPort));
        }
        catch (BindException ex) {
            if (launchClient) {
                GameClient.setExtraModeInfo("AUX");
                GameClient.main("localhost", String.valueOf(myPort));
            }
            throw ex;
        }
        
        if (launchClient) {
            GameClient.setExtraModeInfo("MAIN");
            new Thread() {
                @Override
                public void run() {
                    GameClient.main("localhost", String.valueOf(myPort));
                }
            }.start();
        }
        
        PersistenceManager.doLoad();
        
        createBasicObjects();
        
        schedule(30000, new Runnable() {
            public void run() {
                PersistenceManager.doThreadedSave(null);
                schedule(30000, this); // repeat
            }
        });

        if (gameMode == null) {
            i3client.setMudName("Aelfengard");
            i3client.setOpenStatus(MudInfo.OPEN_STATUS_MUDLIB_DEVELOPMENT + " - Go to http://couchpotato.net/gamerc/aelfengard.jnlp to connect");
            i3client.setPlayerPort(23);
        }
        else {
            i3client.setMudName("Aelfengard-" + gameMode);
            i3client.setOpenStatus(MudInfo.OPEN_STATUS_RESTRICTED_ACCESS);
        }
        i3client.setAdminEmail("green@couchpotato.net");
        i3client.setMudLib("Custom");
        i3client.setBaseMudLib("Custom");
        i3client.setDriver("Custom");
        i3client.setMudType("Graphics & Text");
        
        i3client.addEventListener(new MyI3EventListener());
        
        if (System.getProperty("aelfengard.i3") != null) {        
            i3client.autoconnect();
        }
        
        while (true) {
            long timeout = 0; // 0 causes select() to block indefinitely
            while (true) {
                if (SCHEDULED_TASKS.isEmpty()) {
                    break;
                }
                Long firstKey = SCHEDULED_TASKS.firstKey();
                long time = firstKey;
                long now = System.currentTimeMillis();
                if (time > now) {
                    timeout = time - now;
                    break;
                }
                Runnable r = SCHEDULED_TASKS.remove(firstKey);
                r.run();
            }
            int count = selector.selectNow();
            if (count == 0) {
                Object[][] todo = EMPTY_RUNNABLE_ARRAY;
                synchronized (GameServer.class) {
                    if (!EXEC_QUEUE.isEmpty()) {
                        todo = new Object[EXEC_QUEUE.size()][];
                        EXEC_QUEUE.toArray(todo);
                        EXEC_QUEUE = new ArrayList<Object>();
                    }
                }
                if (todo != EMPTY_RUNNABLE_ARRAY) {
                    for (int i = 0; i < todo.length; i++) {
                        Object ret = null;
                        Exception ex = null;
                        if (todo[i][0] instanceof XRunnable) {
                            // TODO: Any "OK" exceptions would be caught and returned.
                            //try {
                                ret = ((XRunnable) todo[i][0]).run();
                            //} catch (Exception rex) {
                            //    ex = rex;
                            //}
                        } else {
                            ((Runnable) todo[i][0]).run();
                        }
                        synchronized (todo[i]) {
                            todo[i][0] = null; // mark as done
                            todo[i][1] = ret;
                            todo[i][2] = ex;
                            todo[i].notifyAll();
                        }
                    }
                    continue;
                }
                // no clients to service, and no runnables to 
                // run. So let's sleep until the next timeout
                // value.
                count = selector.select(timeout);
                if (count == 0) {
                    // wakeup() called, or timed out
                    continue;
                }
            }
            for (final SelectionKey key : selector.selectedKeys()) {
                if (key.isWritable()) {
                    Writable writable = (Writable) key.attachment();
                    writable.doWrite();
                } else if (key.isReadable()) {
                    Readable readable = (Readable) key.attachment();
                    readable.doRead();
                } else if (key.isAcceptable()) {
                    SocketChannel clientChannel = serverChannel.accept();
                    InetAddress addr = ((InetSocketAddress) clientChannel
                            .socket().getRemoteSocketAddress()).getAddress();
                    String ip = addr.getHostAddress();
                    System.out.println("Client connected from " + ip);
                    new LoginHandler(clientChannel).start();
                }
            }
            selector.selectedKeys().clear();
        }
    }

    public static void invokeLater(Runnable tmp) {
        Object[] r = new Object[] { tmp, null, null };
        synchronized (GameServer.class) {
            EXEC_QUEUE.add(r);
        }
        selector.wakeup();
    }

    public static Object invokeAndWait(XRunnable tmp) throws Exception {
        Object[] r = new Object[] { tmp, null, null };
        synchronized (GameServer.class) {
            EXEC_QUEUE.add(r);
        }
        selector.wakeup();
        synchronized (r) {
            while (r[0] != null) {
                r.wait();
            }
            if (r[2] != null) {
                throw (Exception) r[2];
            }
            return r[1];
        }
    }

    // Must run this only from within the main thread
    public static void schedule(long offset, Runnable r) {
        long time = System.currentTimeMillis() + offset;
        Long otime;
        while (true) {
            // find an available run time
            otime = new Long(time);
            if (SCHEDULED_TASKS.get(otime) == null) {
                break; // found one
            }
            time++;
        }
        SCHEDULED_TASKS.put(otime, r);
    }

    public static void loggedIn(SocketChannel channel, Player player, Cipher encipher, Cipher decipher) {
        try {
            channel.configureBlocking(false);
            SelectionKey key = channel.register(selector, 0);
            player.initClientState(key, encipher, decipher);
            
        } catch (IOException ex) {
            throw new FatalError(ex);
        }
    }

    private static void createBasicObjects() {
        Area mainArea = Area.findByName("main");
        if (mainArea == null) {
            mainArea = new Area("main");
            Room defaultRoom = mainArea.getDefaultRoom();
            defaultRoom.setName("the trash mound");
            defaultRoom.setDescription( 
                    "The acrid stench of the air around the enourmous pile " +
                    "of garbage is enough to pull all the insect life away " +
                    "from other parts of town.  A small swarm of flies " +
                    "gathers around each of the many sources of rotted food.");
            
        }
    }

    public static void sendCoderMessage(String msg) {
        msg = "@10F@10D[@10FCODERS@10D] @10F" + msg;
        for (Player player : Player.getOnlinePlayers()) {
            if (player.getAdminFlags().contains(AdminFlag.CODER)) {
                player.sendText(true, msg);
            }
        }
    }
    
    public static int sendAdminsMessage(Player player, String premsg, String postmsg) {
        if (premsg == null) {
            premsg = "";
        }
        if (postmsg == null) {
            postmsg = "";
        }
        int count = 0;
        Collection<Player> admins = new HashSet<Player>(Player.getOnlineAdmins());
        for (Player target : admins) {
            String nick;
            if (player == null) {
                nick = "";
            }
            else if (player == target) { // TODO: Is this test still needed?
                nick = "You";
            }
            else {
                nick = player.getName(target, false);
                nick = Utils.capitalize(nick);
            }
            String text = "@10F@10D[@10Fadmins@10D] @10F" + premsg + nick + postmsg;
            target.sendText(true, text);
            if (!target.isAFK()) {
                count++;
            }
        }
        return count;
    }
    
    public static String getGameMode() {
        return gameMode;
    }
    
    private static void usage() {
        System.err.println("Usage: java -jar aelfengard.jar <my_hostname> <listen_port> <rc_subdir> [-gui]");
        System.err.println("   or: java -jar aelfengard.jar -mkdevdb");
        System.exit(-1);
    }
    
    public static String getMyHostname() {
        return myHostname;
    }
    
    public static String getRcSubdir() {
        return rcSubdir;
    }
    
    private static class MyI3EventListener implements I3EventListener {

        public void whoReply(final LPCMixed targetUsername, final LPCMixed originatorMudName, final List<LPCMixed> whoInfo) {
            invokeLater(new Runnable() {
                public void run() {
                    Player player = Player.findByUsername(targetUsername.asString());
                    if (player != null) {
                        player.showWhoInfo(originatorMudName, whoInfo);
                    }
                }
            });
        }

        public void whoFailed(final LPCMixed targetUsername, final LPCMixed targetMudName, final LPCMixed errorMessage) {
            invokeLater(new Runnable() {
                public void run() {
                    Player player = Player.findByUsername(targetUsername.asString());
                    if (player != null) {
                        player.sendText(true, "I3 WHO listing of \"@10F" + targetMudName.asString() + "@ZZZ\" failed: " + Utils.prettyMessage(errorMessage.asString()));
                    }
                }
            });
        }

        public List<LPCMixed> whoRequest() {
            // TODO: Change this to respond later rather than RETURNing the answer
            final List<LPCMixed> list = new LinkedList<LPCMixed>();
            try {
                invokeAndWait(new XRunnable() {

                    public Object run() throws Exception {
                        int admins = 0;
                        int count = Player.getOnlinePlayers().size();
                        for (Player p : Player.getOnlineAdmins()) {
                            count--;
                            if (p.isAFK()) {
                                continue; // don't list afk players
                            }
                            List<LPCMixed> entry = new LinkedList<LPCMixed>();
                            entry.add(new LPCMixed(Utils.capitalize(p.getUsername())));
                            entry.add(new LPCMixed(-1));
                            entry.add(new LPCMixed(""));
                            list.add(new LPCMixed(entry));
                            admins++;
                        }
                        return null;
                    }
                    
                });
            }
            catch (Exception ex) {
                // can't see how this could happen
                ex.printStackTrace();
                System.exit(-1);
            }
            return list;
        }

        public void tell(final TellPacket packet, final ErrorCallback callback) {
            invokeLater(new Runnable() {
                public void run() {
                    Player player = Player.findByUsername(packet.getTargetUsername().asString());
                    if (player == null || !player.hasI3()) {
                        callback.returnError("Unknown player");
                    }
                    else if (!player.isOnline()) {
                        callback.returnError("Player is offline");
                    }
                    else {
                        player.i3Tell(packet.getOrigVisName(), packet.getOriginatorMudName(), packet.getMessage());
                    }
                }
            });
        }

        public void tellFailed(final LPCMixed username, final LPCMixed targetMudName, final LPCMixed targetUsername, final LPCMixed errorMessage) {
            invokeLater(new Runnable() {
                public void run() {
                    Player player = Player.findByUsername(username.asString());
                    if (player != null && player.hasI3()) {
                        player.sendText(true, "I3 TELL to @10F" + targetUsername + "@@10F" + Utils.quoteIfSpace(targetMudName.asString()) + "@ZZZ failed: " + Utils.prettyMessage(errorMessage.asString()));
                    }
                }
            });
        }

        public void i3Error(final ErrorPacket packet) {
            invokeLater(new Runnable() {
                public void run() {
                    String username = packet.getTargetUsername().asString();
                    if (username == null) {
                        System.err.println("Got unhandled, untargetted error packet: " + packet);
                        return;
                    }
                    Player player = Player.findByUsername(username);
                    if (player != null && player.hasI3()) {
                        player.sendText(true, "I3 ERROR: [" + packet.getErrorCode() + "] " + packet.getErrorMessage());
                    }
                }
            });
        }

        public void emoteTo(final EmoteToPacket packet, final ErrorCallback callback) {
            invokeLater(new Runnable() {
                public void run() {
                    Player player = Player.findByUsername(packet.getTargetUsername().asString());
                    if (player == null || !player.hasI3()) {
                        callback.returnError("Unknown player");
                    }
                    else if (!player.isOnline()) {
                        callback.returnError("Player is offline");
                    }
                    else {
                        player.i3EmoteTo(packet.getOrigVisName(), packet.getOriginatorMudName(), packet.getMessage());
                    }
                }
            });
        }
        
    }

    public static void internalOnly() {
        if (SECURITY_MANAGER != null) {
            SECURITY_MANAGER.checkPermission(GamePermission.SINGLETON);
        }
    }

    
}