/*
 * UCCWrapper.java
 *
 */
package cz.cuni.amis.pogamut.ut2004.server;

import cz.cuni.amis.pogamut.base.utils.Pogamut;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CountDownLatch;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import cz.cuni.amis.pogamut.ut2004.server.exceptions.UCCStartException;

/**
 * Wrapper of running instance of UCC server. Implements pooling of instances.
 * Usage scenario is:
 * <code>
 * UCCWrapper ucc = UCCWrapper.create();
 * ...
 * ucc.release();
 * </code>
 * The location of UCC executabe will be determined by an environment variable
 * pogamut.unreal.home (e.g. c:\Games\UT2004). The property cam be set via <i>java ... 
 * -Dpogamut.unreal.home=c:\Games\UT2004</i>
 * 
 * @author Ik
 */
public class UCCWrapper {

    /** Loger containing all output from running instance of UCC. */
    protected Logger uccLog = Logger.getAnonymousLogger();
    /** Constant for environment variable with path to the Unreal home dir. */
    public static final String PROP_UNREAL_HOME = "pogamut.unreal.home";
    /** Constant for environment variable with ucc executable. On win32 probably ucc.exe, on unix probably ucc-bin */
    public static final String PROP_UCC_EXEC = "pogamut.unreal.serverexec";
    /**
     * Pool of unused ucc servers, it takes some time to start new ucc server so
     * it is convenientto keep unused instances in this pool.
     */
    protected static Collection<UCCWrapper> uccPool =
            Collections.synchronizedList(new ArrayList<UCCWrapper>());
    /** List of unreleased UCC instances. */
    protected static Collection<UCCWrapper> uccUnreleased =
            Collections.synchronizedList(new ArrayList<UCCWrapper>());
    /** Timer executing tasks for cleaning pool of UCC servers. */
    protected Timer poolCleanerTimer = new Timer("UCC pool cleaner");
    /** Task that will remove this timer from the pool. */
    protected TimerTask terminationTask = null;
    /** Counter of files with ports. Used to generate unique file name. */
    protected static int fileCounter = 0;
    Process uccProcess = null;
    protected int gbPort = -1;
    protected int controlPort = -1;
    protected UT2004Server utServer = null;
    /** First port assigned to a ucc instance. */
    protected static final int basePort = 39782;
    protected static Integer nextUccWrapperUID = 0;
    /** ID of the wrapper object. Useful for debuging. */
    protected int uccWrapperUID = 0;
    protected String mapName = "DM-Trainingday";
    /** One of BotAPI.* modes */
    protected String gameType = "BotDeathMatch";
    protected String unrealHome = null;

    //protected String mapToLoad
    /**
     * @return Log with output of UCC. If you want to listen also for messages 
     * from the startup sequence then use UCCWrapper.create(Logger parent). Set
     * Parent logger of this log and register listeners before creating this
     * instance of UCCWrapper.  
     */
    public Logger getLogger() {
        return uccLog;
    }

    public UT2004Server getUTServer() {
        return utServer;
    }

    protected String getUnrealHome() {
        if (unrealHome == null) {
            return Pogamut.getPlatform().getProperty(PROP_UNREAL_HOME);
        } else {
            return unrealHome;
        }
    }

    public UCCWrapper(Logger log, String startMap) throws UCCStartException {
        if (log != null) {
            uccLog.setParent(log);
        }
        mapName = startMap;
        initUCCWrapper();
    }

    public UCCWrapper(String unrealHome, Logger log, String startMap) throws UCCStartException {
        if (log != null) {
            uccLog.setParent(log);
        }
        mapName = startMap;
        initUCCWrapper();
    }

    /**
     * Reads content of the stream and discards it.
     */
    protected class StreamSink extends Thread {

        protected InputStream os = null;

        public StreamSink(InputStream os) {
            setName("UCC Stream handler");
            this.os = os;
        }

        protected void handleInput(String str) {
            uccLog.info("ID" + uccWrapperUID + " " + str);
        }

        @Override
        public void run() {
            BufferedReader stdInput = new BufferedReader(new InputStreamReader(os));

            String s = null;
            try {
                while ((s = stdInput.readLine()) != null) {
                    handleInput(s);
                }
                os.close();
            } catch (IOException ex) {
                // the process has been closed so reading the line has failed, 
                // don't worry about it
                //ex.printStackTrace();
            }
        }
    }

    /**
     * Scanns the output of UCC for some specific srings (Ports bounded. START MATCH). 
     */
    public class ScannerSink extends StreamSink {

        public long startingTimeout = 2 * 60 * 1000;
        /** Exception that ended the startig. Should be checked after the latch is raised. */
        public UCCStartException exception = null;

        public ScannerSink(InputStream is) {
            super(is);
            timer.schedule(task = new TimerTask() {

                public void run() {
                    exception = new UCCStartException("Starting timed out. Ports weren't bound in the required time (" + startingTimeout + " ms).", this);
                    timer.cancel();
                    portsBindedLatch.countDown();
                }
            }, startingTimeout);
        }
        public CountDownLatch portsBindedLatch = new CountDownLatch(1);
        public int controlPort = -1;
        public int botsPort = -1;
        /**
         * Thread that kills ucc process after specified time if the ports arent 
         * read from the console. This prevents freezing the ScannerSink when ucc
         * fails to start.
         */
        Timer timer = new Timer("UCC start timeout");
        TimerTask task = null;
        private final Pattern portPattern = Pattern.compile("ControlServerPort:(\\d*) BotServerPort:(\\d*)");
        private final Pattern commandletNotFoundPattern = Pattern.compile("Commandlet server not found");
        private final Pattern mapNotFoundPattern = Pattern.compile("No maplist entries found matching the current command line.*");
        private final Pattern matchStartedPattern = Pattern.compile("START MATCH");

        @Override
        protected void handleInput(String str) {
            super.handleInput(str);
            if (portsBindedLatch.getCount() != 0) {
                // ports still havent been found, try to scan the line

                Matcher matcher = portPattern.matcher(str);
                if (matcher.find()) {
                    controlPort = Integer.parseInt(matcher.group(1));
                    botsPort = Integer.parseInt(matcher.group(2));
                //raiseLatch();
                }

                matcher = commandletNotFoundPattern.matcher(str);
                if (matcher.find()) {
                    exception = new UCCStartException("UCC failed to start due to: Commandlet server not found.", this);
                    raiseLatch();
                }

                matcher = mapNotFoundPattern.matcher(str);
                if (matcher.find()) {
                    exception = new UCCStartException("UCC failed to start due to: Map not found.", this);
                    raiseLatch();
                }

                matcher = matchStartedPattern.matcher(str);
                if (matcher.find()) {
                    // The match has started, raise the latch
                    raiseLatch();
                }
            }

        }

        protected void raiseLatch() {
            timer.cancel();
            task.cancel();
            portsBindedLatch.countDown();
        }
    }
    public static long stamp = System.currentTimeMillis();
    /**
     * Time in miliseconds for which the unused instance will remain in the pool 
     * of servers.
     */
    protected long killAfter = 120 * 1000;

    /**
     *  Returns instance of UCC server that is not in use.
     * @param maxTimeInPool Time in miliseconds for which the unused instance
     * will remain in the pool of servers.
     */
    public static UCCWrapper create(long maxTimeInPool) throws UCCStartException {
        return create(maxTimeInPool, null);
    }

    /**
     * Returns instance of UCC server that is not in use.
     * @param maxTimeInPool Time in miliseconds for which the unused instance will remain in the pool of servers.
     * @param parent Parent logger for logging UCC output. Can be null.
     */
    public static UCCWrapper create(long maxTimeInPool, Logger parent) throws UCCStartException {
        UCCWrapper ucc = create(parent);
        ucc.killAfter = maxTimeInPool;
        return ucc;
    }

    /**
     * Returns instance of UCC server that is not in use.
     */
    public static UCCWrapper create() throws UCCStartException {
        return create(null);
    }

    /**
     * Returns instance of UCC server that is not in use.
     * @param parent Parent logger for logging UCC output. Can be null.
     */
    public static UCCWrapper create(Logger parent) throws UCCStartException {
        UCCWrapper ucc = null;
        System.out.println("UCCWRAP " + stamp);
        // get unused ucc
        // make sure that the UCC won't be destroyed by the terminationTask
        synchronized (poolRetrievalLock) {
            if (uccPool.isEmpty()) {
                ucc = new UCCWrapper(parent);
            } else {
                // use an instance from the pool
                ucc = uccPool.iterator().next();
                //if (ucc.terminationTask != null) {
                ucc.terminationTask.cancel();
                //}
                uccPool.remove(ucc);
                // recreate the cleaner timer if the previous was destroyed
                if (ucc.poolCleanerTimer == null) {
                    ucc.poolCleanerTimer = new Timer("UCC pool cleaner");
                }
            }
            uccUnreleased.add(ucc);
            ucc.released = false;
        }
        return ucc;
    }
    /** Lock used when retrieving existing node from the pool. */
    private static Object poolRetrievalLock = new Object();

    protected void initUCCWrapper() throws UCCStartException {
        try {
            // start new ucc instance
            String id = System.currentTimeMillis() + "a" + fileCounter++;
            String fileWithPorts = "GBports" + id;
            String uccHomePath = getUnrealHome();
            String systemDirPath = uccHomePath + File.separator + "System" + File.separator;
            
            //String logDirPath = uccHomePath + File.separator + "UserLogs" + File.separator;
            String uccFile = "ucc.exe";

            // determine OS type, if it isn't win then add option to ucc 
            String options = "";
            if (!System.getProperty("os.name").contains("Windows")) {
                options = " -nohomedir";
                uccFile = "ucc";
            }

            String execStr = systemDirPath + uccFile;

            //int port = basePort + uccWrapperUID * 4;

            ProcessBuilder procBuilder = new ProcessBuilder(execStr, "server",
                    mapName + "?game=GameBotsNew." + gameType + "?PortsLog=" + fileWithPorts + "?bRandomPorts=true?Mutator=UnrealGame.MutGameSpeed?GameSpeed=1" + options);
            procBuilder.directory(new File(systemDirPath));
            /*ProcessBuilder procBuilder = new ProcessBuilder(execStr, "server",
            "DM-TrainingDay?game=BotAPI.BotDeathMatch");
             */
            uccProcess = procBuilder.start();
            ScannerSink scanner = new ScannerSink(uccProcess.getInputStream());
            scanner.start();
            new StreamSink(uccProcess.getErrorStream()).start();

            scanner.portsBindedLatch.await();
            if (scanner.exception != null) {
                // ucc failed to start 
                uccProcess.destroy();
                throw scanner.exception;
            }

            // TODO how to create UTSErver instance?
            //utServer = new UT2004Server();

            controlPort = scanner.controlPort;
            gbPort = scanner.botsPort;
        // TODO how to connect the server
        //utServer.setGamebotsControlConnectionURI(URI.create("ut://localhost:" + scanner.controlPort));
        //utServer.setGamebotsBotURI(URI.create("ut://localhost:" + scanner.botsPort));
        } catch (InterruptedException ex) {
            throw new UCCStartException("Interrupted.", ex);
        } catch (IOException ex) {
            throw new UCCStartException("IO Exception.", ex);
        }
    }

    /**
     * Creates new instance with default parrent logger
     * @param parent Parent logger for sending UT messages. Can be null.
     * @throws cz.cuni.pogamut.exceptions.UCCStartException
     */
    protected UCCWrapper(Logger parent) throws UCCStartException {
        if (parent != null) {
            uccLog.setParent(parent);
        }
        synchronized (nextUccWrapperUID) {
            uccWrapperUID = nextUccWrapperUID++;
        }
        initUCCWrapper();
    }

    public Process getProcess() {
        return uccProcess;
    }

    public void kill() {
        uccProcess.destroy();
    }

    /** Removes the UCC from the pool and terminates it. Must be called only from block synchronized on poolRetrievalLock. */
    protected void close() {
        if (uccPool.remove(this)) {
            // TODO utServer.terminate();
            uccProcess.destroy();
            poolCleanerTimer.cancel();
            poolCleanerTimer = null;
        } else {
            // instance wasn't in the pool, someone received it from the create
            // method, cancel the close operation
            }
    }
    /** Was this instance already released? */
    protected boolean released = false;

    /**
     * This informs UCC that you dont want to use it in the future.
     * UCC is send to the pool where it will reside for some time and if no one
     * will ask for it it will be terminated.
     */
    public synchronized void release() {
        if (!released) {
            synchronized (poolRetrievalLock) {
                uccUnreleased.remove(this);
                uccPool.add(this);

                if (killAfter > 0) {
                    // if unused for some time, remove the UCC from the pool and terminate
                    poolCleanerTimer.schedule(terminationTask = new TimerTask() {

                        public void run() {
                            synchronized (poolRetrievalLock) {
                                close();
                            }
                        }
                    }, killAfter);
                } else {
                    close();
                }
                released = true;
            }
        }
    }

    /**
     * @return Port for GameBots connection.
     */
    public int getGbPort() {
        return gbPort;
    }

    /**
     * @return Port for control connection.
     */
    public int getControlPort() {
        return controlPort;
    }


    {
        // hook that will release all UCC instances when the program finishes
        Runtime.getRuntime().addShutdownHook(new Thread("UCC wrapper finalizer") {

            public void run() {
                synchronized (poolRetrievalLock) {
                    while (!uccPool.isEmpty()) {
                        uccPool.iterator().next().close();
                    }

                    while (!uccUnreleased.isEmpty()) {
                        uccUnreleased.iterator().next().close();
                    }
                }
            }
        });
    }
}
