package edu.caltech.nanodb.server;


import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;

import edu.caltech.nanodb.commands.ExitCommand;
import edu.caltech.nanodb.functions.FunctionDirectory;
import edu.caltech.nanodb.queryeval.Planner;
import edu.caltech.nanodb.server.properties.BooleanFlagValidator;
import edu.caltech.nanodb.server.properties.IntegerValueValidator;
import edu.caltech.nanodb.server.properties.PlannerClassValidator;
import edu.caltech.nanodb.server.properties.ServerProperties;
import edu.caltech.nanodb.server.properties.StringEnumValidator;
import edu.caltech.nanodb.server.properties.StringValueValidator;
import edu.caltech.nanodb.sqlparse.ParseUtil;
import edu.caltech.nanodb.storage.DBFile;

import edu.caltech.nanodb.commands.Command;
import edu.caltech.nanodb.commands.SelectCommand;
import edu.caltech.nanodb.server.properties.PropertyRegistry;
import edu.caltech.nanodb.storage.StorageManager;


/**
 * <p>
 * This class provides the entry-point operations for managing the database
 * server, and executing commands against it.  While it is certainly possible
 * to implement these operations separately, these implementations are
 * strongly recommended since they include all necessary supporting steps,
 * such as firing before- and after-command events, acquiring and releasing
 * locks, and other critical resource-management tasks.
 * </p>
 * <p>
 * This class also includes operations to parse strings into command objects,
 * so that code can construct SQL statements as strings and then issue them
 * against the database.  Sometimes it is easier to create a SQL string than
 * it is to construct the corresponding command objects.
 * </p>
 */
public class NanoDBServer implements ServerProperties {
    /** A logging object for reporting anything interesting that happens. */
    private static Logger logger = LogManager.getLogger(NanoDBServer.class);


    /** The property registry for this database server. */
    private PropertyRegistry propertyRegistry;


    /** The function directory for this database server. */
    private FunctionDirectory functionDirectory;


    /** The event dispatcher for this database server. */
    private EventDispatcher eventDispatcher;


    /** The storage manager for this database server. */
    private StorageManager storageManager;


    /**
     * A read-write lock to force DDL operations to occur serially, and
     * without any overlap from DML operations.
     */
    private ReentrantReadWriteLock schemaLock;


    /**
     * This static method encapsulates all of the operations necessary for
     * cleanly starting the NanoDB server.  Database server properties are
     * initialized from the JVM system properties, and/or defaults are used.
     */
    public void startup() {
        startup(null);
    }


    /**
     * This static method encapsulates all of the operations necessary for
     * cleanly starting the NanoDB server.  Database server properties are
     * initialized from the JVM system properties, and/or defaults are used,
     * but these values may optionally be overridden by the initial properties
     * specified as an argument.
     *
     * @param initialProperties an optional set of database configuration
     *        properties which will override system properties and default
     *        values, or {@code null} if no initial properties should be
     *        provided.
     */
    public void startup(Properties initialProperties) {
        // Start up the database by doing the appropriate startup processing.

        schemaLock = new ReentrantReadWriteLock();

        // Start with objects that a lot of database components need.

        // Everything needs configuration.
        propertyRegistry = new PropertyRegistry();

        // Apply system properties first (populated from e.g. command line),
        // then override with any arguments to this function.
        propertyRegistry.setProperties(System.getProperties());
        if (initialProperties != null)
            propertyRegistry.setProperties(initialProperties);

        propertyRegistry.setupCompleted();

        // Components for query evaluation, index updating, etc.
        functionDirectory = new FunctionDirectory();
        eventDispatcher = new EventDispatcher();

        // The storage manager is a big one!

        logger.info("Initializing storage manager.");
        storageManager = new StorageManager();
        storageManager.initialize(this);
    }


    /**
     * Returns the event-dispatcher for this database server.
     *
     * @return the event-dispatcher for this database server.
     */
    public EventDispatcher getEventDispatcher() {
        return eventDispatcher;
    }


    /**
     * Returns the property registry for this database server.
     *
     * @return the property registry for this database server.
     */
    public PropertyRegistry getPropertyRegistry() {
        return propertyRegistry;
    }


    /**
     * Returns the function directory for this database server.
     *
     * @return the function directory for this database server.
     */
    public FunctionDirectory getFunctionDirectory() {
        return functionDirectory;
    }


    /**
     * Returns the storage manager for this database server.
     *
     * @return the storage manager for this database server.
     */
    public StorageManager getStorageManager() {
        return storageManager;
    }


    public Command parseCommand(String command) {
        return ParseUtil.parseCommand(command, functionDirectory);
    }


    /**
     * Returns a query-planner object of the type specified in the current
     * server properties.  If the planner cannot be instantiated for some
     * reason, a {@code RuntimeException} will be thrown.
     *
     * @return a query-planner object of the type specified in the current
     *         server properties.
     *
     * @throws RuntimeException if the specified planner class cannot be
     *         instantiated for some reason.
     */
    public Planner getQueryPlanner() {
        String className =
            propertyRegistry.getStringProperty(PROP_PLANNER_CLASS);

        try {
            // Load and instantiate the specified planner class.
            Class<?> c = Class.forName(className);
            Planner p = (Planner) c.getDeclaredConstructor().newInstance();
            p.setStorageManager(storageManager);
            return p;
        }
        catch (Exception e) {
            throw new RuntimeException(
                "Couldn't instantiate Planner class " + className, e);
        }
    }


    /**
     * Parse and execute a single command, returning a {@link CommandResult}
     * object describing the results.  The tuples produced by the command may
     * optionally be included in the results as well.
     *
     * @param command the SQL operation to perform
     * @param includeTuples if {@code true}, the results will include all
     *        tuples produced by the command
     *
     * @return an object describing the outcome of the command execution
     */
    public CommandResult doCommand(String command, boolean includeTuples) {

        try {
            Command commandObject = parseCommand(command);
            return doCommand(commandObject, includeTuples);
        }
        catch (Exception e) {
            // If a parsing error or some other exception occurs, we won't
            // have a CommandResult to return.  So, make sure to return one
            // here.
            CommandResult result = new CommandResult();
            result.recordFailure(e);
            return result;
        }
    }


    /**
     * Parse and execute one or more commands, returning a list of
     * {@link CommandResult} object describing the results.  The tuples
     * produced by the commands may optionally be included in the results as
     * well.
     *
     * @param commands one or more SQL operations to perform
     * @param includeTuples if {@code true}, the results will include all
     *        tuples produced by the commands
     *
     * @return a list of objects describing the outcome of the command
     *         execution
     */
    public List<CommandResult> doCommands(String commands,
        boolean includeTuples) {

        ArrayList<CommandResult> results = new ArrayList<>();

        // Parse the string into however many commands there are.  If there is
        // a parsing error, no commands will run.
        List<Command> parsedCommands =
            ParseUtil.parseCommands(commands, functionDirectory);

        // Try to run each command in order.  Stop if a command fails.
        for (Command cmd : parsedCommands) {
            CommandResult result = doCommand(cmd, includeTuples);
            results.add(result);
            if (result.failed())
                break;
        }

        return results;
    }


    /**
     * Executes a single database command, generating a {@code CommandResult}
     * holding details about the operation, and optionally any tuples
     * generated by the operation.  This method also takes care of other
     * command-related details, such as firing "before-command" and
     * "after-command" events, and acquiring and releasing appropriate locks.
     *
     * @param command the command to execute
     * @param includeTuples a value of {@code true} causes the command's
     *        tuples to be stored into the command-result; {@code false}
     *        causes any tuples to be discarded.
     *
     * @return a command-result describing the results of the operation
     */
    public CommandResult doCommand(Command command, boolean includeTuples) {

        // DDL operations must be performed serially on the database; all
        // other operations are allowed to overlap with each other.
        Lock lock;
        if (command.getCommandType() == Command.Type.DDL)
            lock = schemaLock.writeLock();
        else
            lock = schemaLock.readLock();

        // The try-block is mainly here to ensure that the lock is released
        // at the end of command execution.
        lock.lock();  // TODO:  lockInterruptibly()?
        try {
            CommandResult result = new CommandResult();

            if (includeTuples && command instanceof SelectCommand)
                result.collectSelectResults((SelectCommand) command);

            result.startExecution();
            try {
                if (command instanceof ExitCommand) {
                    result.setExit();
                }
                else {
                    // Execute the command, but fire before- and after-command
                    // handlers when we execute it.
                    eventDispatcher.fireBeforeCommandExecuted(command);
                    command.execute(this);
                    eventDispatcher.fireAfterCommandExecuted(command);
                }
            }
            catch (Exception e) {
                logger.error("Command threw an exception!", e);
                result.recordFailure(e);

                if (!(e instanceof NanoDBException)) {
                    System.out.println("UNEXPECTED EXCEPTION:");
                    e.printStackTrace(System.out);
                }
            }
            result.endExecution();

            // Post-command cleanup:
            storageManager.getBufferManager().unpinAllSessionPages();

            if (propertyRegistry.getBooleanProperty(PROP_FLUSH_AFTER_CMD)) {
                try {
                    storageManager.flushAllData();
                }
                catch (Exception e) {
                    logger.error(
                        "Post-command flush of all data threw an exception!",
                        e);
                }
            }

            return result;
        }
        finally {
            lock.unlock();
        }
    }


    /**
     * This method encapsulates all of the operations necessary for cleanly
     * shutting down the NanoDB server.
     *
     * @return {@code true} if the database server was shutdown cleanly, or
     *         {@code false} if an error occurred during shutdown.
     */
    public boolean shutdown() {
        boolean success = true;

        try {
            storageManager.shutdown();
        }
        catch (Exception e) {
            logger.error("Couldn't cleanly shut down the Storage Manager!", e);
            success = false;
        }

        return success;
    }
}