/***********************************************************************

GECCO, a platform for command and control gaming

Copyright (C) 2001 Henrik Bäärnhielm, Andreas Enblom, Niklas
Hallenfur, Karl Hasselström, Henrik Hägerström, Oskar Linde, Jing Fu
Zi and Jon Åslund

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.

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

***********************************************************************/

package gecco.server.startup;

import java.awt.Color;
import java.io.*;
import java.net.*;
import java.util.*;

import gecco.communication.server.ClientAmbassador;
import gecco.communication.server.RoleDefinition;
import gecco.server.actionprocessor.ActionProcessor;
import gecco.server.actsofgod.*;
import gecco.server.automaton.*;
import gecco.server.core.*;
import gecco.server.qmanager.QManager;
import gecco.server.unitmanager.UnitManager;
import gecco.server.visibility.VisibilityManager;
import gecco.util.*;

/**
 *  This class is the startup routine for the server, that parses all
 *  configuration files, and sets everything up.
 *
 * @author     Henrik Bäärnhielm, Karl Hasselström, Henrik Hägerström
 * @created    May 3, 2001
 */
public class StartServer {

    /**
     *  Description of the Field
     */
    private final String globalConfFileName = "global.conf";
    /**
     *  Description of the Field
     */
    private final String rolesConfFileName = "roles.conf";
    /**
     *  Description of the Field
     */
    private final String unitTypesConfFileName = "unittypes.conf";
    /**
     *  Description of the Field
     */
    private final String unitsConfFileName = "units.conf";
    /**
     *  Description of the Field
     */
    private final String actsofgodConfFileName = "actsofgod.conf";

    /**
     *  Description of the Field
     */
    private final String logFileName = "/var/tmp/server.log";

    /**
     *  Description of the Field
     */
    private BufferedReader bufReader;
    /**
     *  Description of the Field
     */
    private int currentLine;

    /**
     *  Description of the Field
     */
    private Class automatonClass;

    /**
     *  Description of the Field
     */
    private int connectionPort;

    /**
     *  Description of the Field
     */
    private String game;

    /**
     *  Description of the Field
     */
    private UnitManager unitManager;
    /**
     *  Description of the Field
     */
    private QManager qManager;
    /**
     *  Description of the Field
     */
    private ActsOfGod actsOfGod;
    /**
     *  Description of the Field
     */
    private VisibilityManager visibilityManager;
    /**
     *  Description of the Field
     */
    private ActionProcessor actionProcessor;
    /**
     *  Description of the Field
     */
    private LogManager logManager;
    /**
     *  Description of the Field
     */
    private Set roles;
    /**
     *  Description of the Field
     */
    private Set godseyeRoles;

    /**
     *  Description of the Field
     */
    private AutomatonMap map;
    /**
     *  Description of the Field
     */
    private int mapX, mapY;
    /**
     *  Description of the Field
     */
    private HashMap colorToState;



    /**
     *  Constructor for the StartServer object
     *
     * @param  props                      Description of Parameter
     * @exception  FileNotFoundException  Description of Exception
     * @exception  IOException            Description of Exception
     * @exception  SyntaxErrorException   Description of Exception
     * @exception  ParseErrorException    Description of Exception
     */
    public StartServer(CommandLineProperties props)
             throws FileNotFoundException, IOException, SyntaxErrorException,
            ParseErrorException {

        try {
            logManager = new LogManager(logFileName, true);
            ReferenceHolder.setLogManager(logManager);
        } catch (IOException e) {
            ReferenceHolder.out.println("Could not instantiate the log manager. Exiting.");
            System.exit(-1);
        }

	// Make a class loader to read from classes.jar
        game = props.getProperty("-game");
        if (game == null) {
            ReferenceHolder.out.print("No game specified with command-line switch -game.\n" +
				      "Please enter the main directory of a game: ");
            game = ReferenceHolder.in.readLine().trim();
        }
	try {
	    File gameClasses = new File(game, "classes/classes.jar");
	    URL [] gameClassesURL = { gameClasses.toURL() };
	    ReferenceHolder.setGameClassLoader(new URLClassLoader(gameClassesURL));
	} catch (MalformedURLException e) {
	    ReferenceHolder.out.println("Could not find game class files. Exiting.\n");
	    System.exit(-1);
	} catch (SecurityException e) {
	    ReferenceHolder.out.println("SecurityException prevents loading of game class files.\n");
	    ReferenceHolder.out.println(e);
	    System.exit(-1);
	}

        // Global conf. file
        ReferenceHolder.out.print("Parsing " + globalConfFileName + " . . . ");
        parseGlobal(getConfFile(globalConfFileName));
        ReferenceHolder.out.println("\tdone.");

        ReferenceHolder.setAutomatonMap(map);

        // Roles conf. file
        ReferenceHolder.out.print("Parsing " + rolesConfFileName + " . . . ");
        parseRoles(getConfFile(rolesConfFileName));
        ReferenceHolder.out.println("\tdone.");

        String[] rls = new String[roles.size()];
        Iterator it = roles.iterator();
        int i = 0;
        while (it.hasNext()) {
            rls[i++] = (String) it.next();
        }

        unitManager = new UnitManager(rls, godseyeRoles, map);
        ReferenceHolder.setUnitManager(unitManager);

        qManager = new QManager(map);
        ReferenceHolder.setQManager(qManager);

        actsOfGod = new ActsOfGod(qManager);

        // Unit types conf. file
        ReferenceHolder.out.print("Parsing " + unitTypesConfFileName + " . . . ");
        parseUnitTypes(getConfFile(unitTypesConfFileName));
        ReferenceHolder.out.println("\tdone.");

        // Units conf. file
        ReferenceHolder.out.print("Parsing " + unitsConfFileName + " . . . ");
        parseUnits(getConfFile(unitsConfFileName));
        ReferenceHolder.out.println("\tdone.");

        // Acts of God conf. file
        ReferenceHolder.out.print("Parsing " + actsofgodConfFileName + " . . . ");
        parseActsOfGod(getConfFile(actsofgodConfFileName));
        ReferenceHolder.out.println("\tdone.");

        // Get port from command line. If not defined there, take it
        // from global.conf.
        connectionPort = props.getIntProperty("port", connectionPort);
        ReferenceHolder.out.println("Initializing communication server at port " + connectionPort + ".");
        Server si = new ServerImpl();
        ReferenceHolder.setComServer(si);

        Vector roleDefs = new Vector();
        for (int j = 0; j < rls.length; j++) {
            roleDefs.add(new RoleDefinition(rls[j], unitManager.isRoleCommander(rls[j])));
        }

        Client ci = new ClientAmbassador(roleDefs, si, connectionPort);
        ReferenceHolder.setComClient(ci);

        ReferenceHolder.out.println("Initializing visibility manager");
        visibilityManager = new VisibilityManager(map, ci, unitManager);
        ReferenceHolder.setVisibilityManager(visibilityManager);

        ReferenceHolder.out.println("Initializing action processor");
        actionProcessor = new ActionProcessor(unitManager, visibilityManager, ci);
        ReferenceHolder.setActionProcessor(actionProcessor);

        Thread actionProcessorThread = new Thread(actionProcessor);
        actionProcessorThread.start();

        ReferenceHolder.out.println("Initializing qmanager");
        Thread qManagerThread = new Thread(qManager);
        qManagerThread.start();

        ReferenceHolder.out.println("Initializing Acts of God scheduler");
        actsOfGod.start();

        /*
         * ReferenceHolder.out.println("Starting server test environment.");
         * server.test.ServerTest sTest = new server.test.ServerTest(si);
         */
        ReferenceHolder.out.println("Done!\nServer is ready and waiting for incoming connections...");
    }


    private Reader getConfFile(String name) throws FileNotFoundException {
	File file = new File(game, "conf/" + name);
	return new BufferedReader(new FileReader(file));
    }

    private byte [] getImage(String name) throws FileNotFoundException, IOException {
	File file = new File(game, "images/" + name);
	InputStream inStream = new BufferedInputStream(new FileInputStream(file));
	byte[] byteArr = new byte[inStream.available()];
	inStream.read(byteArr);
	inStream.close();
	return byteArr;
    }


    /**
     *  Parses the global configuration file.
     *
     * @param  theStream                 The file stream.
     * @exception  IOException           Description of Exception
     * @exception  SyntaxErrorException  Description of Exception
     * @exception  ParseErrorException   Description of Exception
     */
    private void parseGlobal(Reader in)
             throws IOException, SyntaxErrorException, ParseErrorException {

        // Read the file into a data structure
        PropertyTree pt = new PropertyTree();
        pt.parseStream(in);

        // Command line defaults
        PropertyTree defaults = pt.getSingleSubtree("defaults");
        connectionPort = defaults.getSingleInt("port");

        // The map
        PropertyTree mapSettings = pt.getSingleSubtree("map");

        // Size
        mapX = mapSettings.getSingleInt("width");
        mapY = mapSettings.getSingleInt("height");

        // Automaton class
        String className = mapSettings.getSingleString("class");
        try {
            Class cl = Class.forName(className, true, ReferenceHolder.getGameClassLoader());
            Object o = cl.newInstance();
            if (!(o instanceof Automaton)) {
                throw new ParseErrorException(className +
                        " is not an Automaton subclass!");
            }
            automatonClass = cl;
        } catch (ClassNotFoundException e) {
            throw new IOException(e.getMessage());
        } catch (IllegalAccessException e) {
            throw new IOException(e.getMessage());
        } catch (InstantiationException e) {
            throw new IOException(e.getMessage());
        }

        // State definitions
        colorToState = new HashMap();
        Iterator iter = mapSettings.getSubtreeList("state").iterator();
        while (iter.hasNext()) {
            PropertyTree state = (PropertyTree) iter.next();
            Integer number = new Integer(state.getSingleInt("number"));
            PropertyTree color = state.getSingleSubtree("color");
            int red = color.getSingleInt("red");
            int green = color.getSingleInt("green");
            int blue = color.getSingleInt("blue");
            colorToState.put(new Color(red, green, blue), number);
        }

        // Initial state
        String image = mapSettings.getSingleString("initial_state");
        byte[] state = getImage(image);

        // Create the map
        map = new AutomatonMap(automatonClass, mapX, mapY, state, colorToState);
    }


    /**
     *  Parses the Roles configuration file.
     *
     * @param  theStream                 The file stream.
     * @exception  IOException           Description of Exception
     * @exception  SyntaxErrorException  Description of Exception
     * @exception  ParseErrorException   Description of Exception
     */
    private void parseRoles(Reader in)
             throws IOException, SyntaxErrorException, ParseErrorException {

        // Read the file into a data structure
        PropertyTree pt = new PropertyTree();
        pt.parseStream(in);

        // Extract information from the data structure
        HashSet roleSet = new HashSet();
        HashSet godseyeSet = new HashSet();
        Iterator iter = pt.getStringList("role").iterator();
        while (iter.hasNext()) {
            String role = (String) iter.next();
            roleSet.add(role);
        }
        iter = pt.getStringList("gods_eye").iterator();
        while (iter.hasNext()) {
            String gods_eye = (String) iter.next();
            roleSet.add(gods_eye);
            godseyeSet.add(gods_eye);
        }
        roles = roleSet;
        godseyeRoles = godseyeSet;
    }


    /**
     *  Parses the Unit Types configuration file.
     *
     * @param  theStream                 The file stream.
     * @exception  IOException           Description of Exception
     * @exception  SyntaxErrorException  Description of Exception
     * @exception  ParseErrorException   Description of Exception
     */
    private void parseUnitTypes(Reader in)
             throws IOException, SyntaxErrorException, ParseErrorException {

        // Read the file into a data structure
        PropertyTree pt = new PropertyTree();
        pt.parseStream(in);

        // Extract information from the data structure
        Iterator iter = pt.getSubtreeList("unit_type").iterator();
        while (iter.hasNext()) {
            // Get the next unit type
            PropertyTree unit_type = (PropertyTree) iter.next();

            // Create the unit type
            UnitType currentUnitType = new
                    UnitType(unit_type.getSingleString("name"));

            // Some simple properties
            currentUnitType.setUnitClass(unit_type.getSingleString("class"));
            currentUnitType.setRange(unit_type.getSingleInt("range"));
            currentUnitType.setEventHandler(unit_type.getSingleString("event_handler"));

            // Load the icon
            String image = unit_type.getSingleString("image");
            currentUnitType.setImageData(getImage(image));

            // Parse the actions
            Iterator iter2 = unit_type.getSubtreeList("action").iterator();
            while (iter2.hasNext()) {
                PropertyTree action = (PropertyTree) iter2.next();
                String name = action.getSingleString("name");
                String className = action.getSingleString("class");
                String argumentType = action.getSingleString("argument");
                String description = action.getSingleString("description");
                currentUnitType.addAction(name, description, argumentType, className);
            }

            // Parse the user-defined properties
            iter2 = unit_type.getSubtreeList("property").iterator();
            while (iter2.hasNext()) {
                PropertyTree property = (PropertyTree) iter2.next();
                String name = property.getSingleString("name");
                String type = property.getSingleString("type");

                // Make sure the property is not already defined as another type
                String oldType = currentUnitType.getPropertyType(name);
                if (oldType != null && !oldType.equals(type)) {
                    throw new ParseErrorException("Property " + name + " cannot be redefined to type " + type);
                }

                if (type.equals("STRING")) {
                    currentUnitType.setProperty(name, property.getSingleString("value"));
                } else if (type.equals("INT")) {
                    currentUnitType.setProperty(name, property.getSingleInt("value"));
                } else if (type.equals("DOUBLE")) {
                    currentUnitType.setProperty(name, property.getSingleDouble("value"));
                } else {
                    throw new ParseErrorException("Type must be STRING, INT or DOUBLE.");
                }
            }

            // Add the unit type
            unitManager.addUnitType(currentUnitType);
        }
    }


    /**
     *  Parses the Units configuration file.
     *
     * @param  theStream                 The file stream.
     * @exception  IOException           Description of Exception
     * @exception  SyntaxErrorException  Description of Exception
     * @exception  ParseErrorException   Description of Exception
     */
    private void parseUnits(Reader in)
             throws IOException, SyntaxErrorException, ParseErrorException {

        // Read the file into a data structure
        PropertyTree pt = new PropertyTree();
        pt.parseStream(in);

        // Extract information from the data structure
        Iterator iter = pt.getSubtreeList("unit").iterator();
        while (iter.hasNext()) {
            // Get the next unit
            PropertyTree unit = (PropertyTree) iter.next();

            // Create the unit
            String type = unit.getSingleString("type");
            Unit currentUnit = unitManager.createUnit(type);
            currentUnit.setUnitType(type);

            // Read the name
            currentUnit.setName(unit.getSingleString("name"));

            // An optional commander
            List command = unit.getStringList("command");
            if (command.size() > 1) {
                throw new ParseErrorException("More than one commander specified!");
            } else if (command.size() == 1) {
                currentUnit.setCommander((String) command.get(0));
            }

            // Zero or more observers
            List observe = unit.getStringList("observe");
            Iterator iter2 = observe.iterator();
            while (iter2.hasNext()) {
                currentUnit.addObserver((String) iter2.next());
            }

            // Parse the user-defined properties
            iter2 = unit.getSubtreeList("property").iterator();
            while (iter2.hasNext()) {
                PropertyTree property = (PropertyTree) iter2.next();
                String name = property.getSingleString("name");
                String propertyType = property.getSingleString("type");

                // Make sure the property is not already defined as another type
                String oldType = currentUnit.getPropertyType(name);
                if (oldType != null && !oldType.equals(propertyType)) {
                    throw new ParseErrorException("Property " + name + " cannot be redefined to type " + propertyType);
                }

                if (propertyType.equals("STRING")) {
                    currentUnit.setProperty(name, property.getSingleString("value"));
                } else if (propertyType.equals("INT")) {
                    currentUnit.setProperty(name, property.getSingleInt("value"));
                } else if (propertyType.equals("DOUBLE")) {
                    currentUnit.setProperty(name, property.getSingleDouble("value"));
                } else {
                    throw new ParseErrorException("Type must be STRING, INT or DOUBLE.");
                }
            }

            // Read the initial position
            double x = unit.getSingleDouble("init_x");
            double y = unit.getSingleDouble("init_y");
            currentUnit.setPosition(x, y);
            unitManager.initializeUnitPosition(currentUnit);
        }
    }


    /**
     *  Parses the Acts of God configuration file.
     *
     * @param  theStream                 The file stream.
     * @exception  IOException           Description of Exception
     * @exception  SyntaxErrorException  Description of Exception
     * @exception  ParseErrorException   Description of Exception
     */
    private void parseActsOfGod(Reader in)
             throws IOException, SyntaxErrorException, ParseErrorException {

        // Read the file into a data structure
        PropertyTree pt = new PropertyTree();
        pt.parseStream(in);

        // Extract information from the data structure
        Iterator iter = pt.getSubtreeList("event").iterator();
        while (iter.hasNext()) {
            // Get the next event
            PropertyTree event = (PropertyTree) iter.next();

            // Parse simple properties
            String type = event.getSingleString("type");
            int factor = event.getSingleInt("factor");
            double time = event.getSingleDouble("time");
            double x = event.getSingleDouble("position_x");
            double y = event.getSingleDouble("position_y");

            // Add the act
            actsOfGod.addAct(type, factor, x, y, time);
        }
    }


    /**
     *  The main program for the StartServer class
     *
     * @param  args  The command line arguments
     */
    public static void main(String[] args) {

	// Set up standard input/output
	ReferenceHolder.in = new BufferedReader(new InputStreamReader(System.in));
	ReferenceHolder.out = new AutoFlushWriter(new OutputStreamWriter(System.out));
	ReferenceHolder.err = ReferenceHolder.out;

	// Use the Pure Java AWT (PJA) awt replacement. This replaces
	// all (or most :-) of awt's native calls with pure Java
	// equivalents, so that we can use awt for off-screen drawing
	// and such without having an X-server running.
	System.setProperty ("awt.toolkit", "com.eteks.awt.PJAToolkit");
	//System.setProperty ("java.awt.graphicsenv", "com.eteks.java2d.PJAGraphicsEnvironment");

	// Start up the server
        try {
            new StartServer(new CommandLineProperties(args));
        } catch (FileNotFoundException e) {
            abort(e.getMessage(), 1);
        } catch (IOException e) {
            abort(e.getMessage(), 2);
        } catch (ParseErrorException e) {
            abort(e.getMessage(), 3);
        } catch (SyntaxErrorException e) {
            abort(e.getMessage(), 4);
        }
    }


    /**
     *  Description of the Method
     *
     * @param  message     Description of Parameter
     * @param  exitStatus  Description of Parameter
     */
    private static void abort(String message, int exitStatus) {
        ReferenceHolder.out.println();
        ReferenceHolder.out.println(message);
        ReferenceHolder.out.println();
        ReferenceHolder.out.println("Server aborted.");
        ReferenceHolder.out.println();
        System.exit(exitStatus);
    }
}
