// $Id: ModuleContext.java,v 1.43 2004/09/22 17:03:57 m31 Exp $
//
// This file is part of the program suite "Principia Mathematica II"
// which is a working prototype for the main project:
// "Hilbert II" - http://www.qedeq.org
//
// Copyright 2001-2004  Michael Meyling <mime@qedeq.org>.
//
// "Principia Mathematica II" 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.

package com.meyling.principia.module;


import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import com.meyling.principia.argument.Argument;
import com.meyling.principia.argument.ArgumentException;
import com.meyling.principia.config.Settings;
import com.meyling.principia.io.Output;
import com.meyling.principia.io.ParsingException;
import com.meyling.principia.io.TextInput;
import com.meyling.principia.io.Utility;
import com.meyling.principia.log.Logger;
import com.meyling.principia.log.Trace;
import com.meyling.principia.logic.paragraph.Proposition;


/**
 * This class provides static access methods for loading modules.
 *
 * @version $Revision: 1.43 $
 * @author  Michael Meyling
 */
public final class ModuleContext {

    private final String monitor = new String();
    private final String syncToken = new String();
    private int processCounter = 0;

    /** version of this kernel */
    private final static Version kernelVersion;

    /** maximum version of supported rules */
    private final static Version ruleVersion;


    /** version dependend directory of this kernel */
    private final static String versionDirectory;

    /** thread for watching loading process. */
    private final PeekTextInput peek;

    /** collection of modules. */
    private final Modules modules;


    /**
     * Make attribut initialization.
     */
    static {
        // Initialize version information.
        try {
            kernelVersion = new Version("0.00.53");
            ruleVersion = new Version("1.02.00");
            versionDirectory = kernelVersion.getText().toString().replace('.', '_');
        } catch (ArgumentException e) {
            throw new IllegalArgumentException(e.toString());
        }
    }


    private static ModuleEventListener listener = null;

    private static ModuleContext instance = null;


    /**
     * Get instance of module context. Method {@link #setup} must have been called before.
     *
     * @return  singleton, which represents the module context
     * @throws  IllegalStateException   {@link #setup} wasn't called before this method call
     */
    public final static ModuleContext getInstance() throws IllegalStateException {
        if (instance == null) {
            throw new IllegalStateException("ModuleContext not initialized, call ModuleContext.setup before!");
        }
        return instance;
    }


    /**
     * Set listener for this context. Must be called at first. Couldn't be called again.
     *
     * @param   listener    this object listens to module events
     * @throws  IllegalStateException   {@link #setup} was called once before
     *           or {@link Config#setup} was not configured properly
     * @throws  IOException directory entries were no valid file names
     */
    public final static void setup(final ModuleEventListener listener)
            throws IOException {
        final String METHOD = "setup";
        Trace.traceBegin(ModuleContext.class, METHOD);
        try {
            if (instance != null) {
                throw new IllegalStateException(ModuleContext.class.getName()
                    + " is already initialized!");
            }
            synchronized (ModuleContext.class) {
                if (instance != null) {
                    throw new IllegalStateException(ModuleContext.class.getName()
                        + " is already initialized!");
                }
                instance = new ModuleContext(listener);
            }
        } finally {
            Trace.traceEnd(ModuleContext.class, METHOD);
        }
    }


    public final void start() {
        if (Settings.getInstance().isAutoReloadLastSessionChecked()) {
            final Thread thread = new Thread() {
                public void run() {
                    final String METHOD = "run"; 
                    try {
                        Trace.traceBegin(this, METHOD);
                        Logger.getInstance().logMessage(
                            "Trying to load previously successfully checked modules.");
                        if (instance.loadPreviouslyCheckedModules()) {
                            Logger.getInstance().logMessage(
                                "Loading of previously successfully checked modules successfully done.");
                        } else {
                            Logger.getInstance().logMessage(
                                "Loading of all previously successfully checked modules failed.");
                        }
                    } catch (Exception e) {
                        Trace.trace(this, METHOD, e);
                    } finally {
                        Trace.traceBegin(this, METHOD);
                    }
                }
            };
            thread.setDaemon(true);
            thread.start();
        }
    }


    /**
     * Set dummy listener for this context. Must be called at first. Shouldn't be called again.
     *
     * @throws  IllegalStateException   {@link #setup} was called once before
     *           or {@link Config#setup} was not configured properly
     * @throws  IOException directory entries were no valid file names
     */
    public final static void setup() throws IllegalStateException, IOException {

        setup(new ModuleEventListener() {

            public void addModule(ModuleProperties prop) {
                Logger.getInstance().logMessageState("Module added. "  + prop.getStateDescription(),
                    prop.getAddress());
            }

            public void stateChanged(ModuleProperties prop) {
                Logger.getInstance().logMessageState("Module state changed. " 
                    + prop.getStateDescription(), prop.getAddress());

            }

            public void removeModule(ModuleProperties prop) {
                Logger.getInstance().logMessageState("Module removed. "
                    + prop.getStateDescription(), prop.getAddress());
            }
        });

    }


    /**
     * Remove all modules from memory.
     */
    public final void removeAllModules() {
        do {
            synchronized (syncToken) {
                if (processCounter == 0) {
                    modules.removeAllModules();
                    return;
                }
            }
            synchronized (this.monitor) {
                try {
                    this.monitor.wait(10000);
                } catch (InterruptedException e) {
                }
            }
        } while (true);

    }


    /**
     * Remove a module and its dependents from memory.
     */
    public final void removeModuleAndDependents(final ModuleProperties prop) {
        do {
            synchronized (syncToken) {
                if (processCounter == 0) {
                    modules.removeModuleAndDependents(prop);
                    return;
                }
            }
            synchronized (this.monitor) {
                try {
                    this.monitor.wait(10000);
                } catch (InterruptedException e) {
                }
            }
        } while (true);

    }

    public final void clearLocalBuffer()
            throws IOException {
        removeAllModules();
        final File bufferDir = new File(ModuleContext.getBufferDirectory());
        if (bufferDir.exists() && !Utility.deleteDir(bufferDir)) {
            throw new IOException("buffer could not be deleted");
        }
    }


    /**
     * Constructor.
     *
     * @param   listener    listener to module events.
     */
    private ModuleContext(final ModuleEventListener listener) {
        peek = new PeekTextInput();
        peek.setDaemon(true);
        peek.start();
        Logger.getInstance().logMessage("this is pmii, version " + getKernelVersion());
        Logger.getInstance().logMessage("  see \"http://www.qedeq.org\" for more information");
        Logger.getInstance().logMessage("  supports rules till version " + getRuleVersion());
        ModuleContext.listener = listener;
        ModuleContext.instance = this;
        this.modules = new Modules();
    }


    /**
     * Get maximum version of supported rules.
     *
     * @return  rule version
     */
    public final static String getRuleVersion() {
        return ModuleContext.ruleVersion.getText();
    }


    /**
     * Get version of this kernel.
     *
     * @return  kernel version
     */
    public final static String getKernelVersion() {
        return ModuleContext.kernelVersion.getText();
    }

    /**
     * Get relative version directory of this kernel.
     *
     * @return  version subdirectory
     */
    public final static String getVersionDirectory() {
        return ModuleContext.versionDirectory;
    }


// TODO not neccessary any longer because peek is now a daemon thread
/*
    public final static void exit() {
        peek.stopPeek();
    }
*/


    /**
     * Remove double and unused lines of all propositions of a module and
     * save modified back in local generated directory. The module in module
     * cache is not modified.
     *
     * @param   address  address of module.
     * @throws  IOException module file couldn't be read or written
     * @throws  ParsingException TODO
     */
     public final boolean compress(final String address)
            throws IOException, ParsingException {
        final String METHOD = "compress";
        this.processInc();
        try {
            final ModuleAddress moduleAddress = new ModuleAddress(address);
            final Module module;
            try {
                module = (Module) loadModule(moduleAddress).copy();
            } catch (IllegalArgumentException e) {
                Logger.getInstance().logFailureState("Compression of module failed!", address,
                    "unknown reason");
                Trace.trace(this, METHOD, e);
                throw new IOException(e.getMessage());
            }
            try {
                module.setModuleAddress(moduleAddress);
                final Proposition[] propositions = module.getPropositions();
                for (int i = 0; i < propositions.length; i++) {
                    propositions[i].compress();
                }
                module.check();
                final StringBuffer buffer = new StringBuffer();
                final Output output = new Output(buffer);
                ModuleWriter.writeModule(module, output);
                final File f = new File(moduleAddress.getGeneratedName()).getParentFile();
                f.mkdirs();
                Utility.saveFile(moduleAddress.getGeneratedName(), buffer);
    //            // replace old module in memory buffer
    //            modules.remove(moduleAddress);
    //            modules.put(moduleAddress, module);
            } catch (IOException e) {
                Trace.trace(this, METHOD, e);
                Logger.getInstance().logFailureState("Compression of module failed!", address,
                    "IO access problem");
                throw new IllegalArgumentException(e.getMessage());
            } catch (IllegalArgumentException e) {
                Trace.trace(this, METHOD, e);
                Logger.getInstance().logFailureState("Compression of module failed!", address,
                    "unknown reason");
                throw e;
            } catch (ArgumentException e) { // could only occur if
                                            // module wasn't checked
                Trace.trace(this, METHOD, e);
                Logger.getInstance().logFailureState("Compression of module failed!", address,
                    "module check failed!");
                throw new IllegalArgumentException(e.toString());
            } catch (ParsingException e) {
                Trace.trace(this, METHOD, e);
                Logger.getInstance().logFailureState("Compression of module failed!", address,
                    "module parsing problem");
                throw e;
            }
            return false;
        } finally {
            this.processDec();
        }
     }


    /**
     * Reduce module file to module file that uses only basic rules.
     * (Basic rules have the rule version &quot;1.00.00&quot;.)
     * The resulting file will be saved at the same directory
     * in the <strong>file buffer</code> where the original resides.
     *
     * @param   address  address of module
     * @throws  IOException if writing the resulting file is not possible
     *                  (e.g. because there is already a module file of
     *                  that name)
     * @throws  ParsingException    if there were a parsing problem for
     *          the referenced (or one indirectly connected) module
     * @throws  IllegalArgumentException if something unexpected happens
     *                  (e.g. a programming error occurs)
     */
     public final void reduceToBasicRules(final String address)
            throws IOException, ParsingException {
        final String METHOD = "reduceToBasicRules";
        this.processInc();
        try {
            final ModuleAddress moduleAddress = new ModuleAddress(address);
            final Version version;
            final ModuleAddress newAddress;
            try {
                version =  new Version("1.00.00");
                newAddress = new ModuleAddress(moduleAddress,
                    moduleAddress.getName(),
                    moduleAddress.getModuleVersion(), version);
    //            final File file = new File(newAddress.getLocalName());
    //            if (file.exists()) {
    //                throw new IOException(
    //                    ModuleConstants.FILE_EXISTS_ALREADY + "\n  "
    //                    + newAddress.getLocalName());
    //            }
            } catch (ArgumentException e) {
                Trace.trace(this, METHOD, e);
                Logger.getInstance().logFailureState("Reduce to basic rules of module failed!", 
                    address, "module couldn't be created");
                throw new IllegalArgumentException(e.getMessage());
            } catch (IOException e) {
                Trace.trace(this, METHOD, e);
                Logger.getInstance().logFailureState("Reduce to basic rules of module failed!",
                    address, "IO access problems");
                throw e;
            }
            try {
                final Module module = (Module) loadModule(moduleAddress).copy();
                module.setModuleAddress(moduleAddress);
                module.check();
                final Proposition[] propositions = module.getPropositions();
                for (int i = 0; i < propositions.length; i++) {
                    propositions[i].reduceRuleVersion(module, version);
                }
                module.setRuleVersion(version);
                module.setModuleAddress(newAddress);
                if (!newAddress.equals(module.getModuleAddress())) {
                    Trace.trace(this, METHOD, "programming error");
                    Trace.traceStack(this, METHOD);
                    final String description = "addresses not equal: "
                        + newAddress + " != " + module.getModuleAddress();
                    Logger.getInstance().logFailureState("Reduce to basic rules of module failed!",
                        address, description);
                    throw new IllegalArgumentException(description);
                }
                module.check();
                final StringBuffer buffer = new StringBuffer();
                final Output output = new Output(buffer);
                ModuleWriter.writeModule(module, output);
                final File f = new File(moduleAddress.getGeneratedName()).getParentFile();
                f.mkdirs();
                Utility.saveFile(module.getModuleAddress().getGeneratedName(),
                    buffer);
            } catch (IOException e) {
                Trace.trace(this, METHOD, e);
                Logger.getInstance().logFailureState("Reduce to basic rules of module failed!",
                    address, "IO access problems");
                throw e;
            } catch (IllegalArgumentException e) {
                Trace.trace(this, METHOD, e);
                Logger.getInstance().logFailureState("Reduce to basic rules of module failed!",
                    address, "unknown reason");
                throw e;
            } catch (ArgumentException e) {
                Trace.trace(this, METHOD, e);
                Logger.getInstance().logFailureState("Reduce to basic rules of module failed!",
                    address, "module creation failed");
                throw new IllegalArgumentException(e.getMessage());
            } catch (ParsingException e) {
                Trace.trace(this, METHOD, e);
                Logger.getInstance().logFailureState("Reduce to basic rules of module failed!",
                    address, "module parsing failed");
                throw e;
            }
        } finally {
            this.processDec();
        }
     }


    /**
     * Get a certain module.
     *
     * @param  address  address of module
     * @return  wanted module
     * @throws  IOException    if loading was not successfull
     * @throws  ParsingException if loading the module lead to
     *           an parsing error
     */
    public final Module loadModule(
            final String address)
            throws ParsingException, IOException {
        this.processInc();
        try {
            return loadModule(new ModuleAddress(address));
        } finally {
            this.processDec();
        }
    }


    /**
     * Get a certain module.
     *
     * @param   address  address of module
     * @return  wanted module
     * @throws  IOException    if loading was not successfull
     * @throws  ParsingException if loading the module lead to
     *           an parsing error
     */
    public final Module loadModule(
            final ModuleAddress moduleAddress)
            throws ParsingException, IOException {
        final String METHOD = "loadModule(ModuleAddress)";
        this.processInc();
        try {
            // search in already loaded modules
            peek.setInput(null);      // stop peeking
            if (moduleAddress.getRuleVersion().greaterThan(
                    ModuleContext.ruleVersion)) {
                final String ERROR = "Loading of module " + moduleAddress.getAddress()
                    + " failed, because rule version is not supported by this kernel: "
                    + moduleAddress.getRuleVersion().getText() + "!";
                Logger.getInstance().logFailureState("Rule version not supported", 
                    moduleAddress.getAddress(), ERROR);
                throw new IOException(ERROR);
            }
            final ModuleProperties prop = modules.getModuleProperties(moduleAddress);
            synchronized (prop) {
                if (prop.isCheckedAndLoaded()) {
                    return prop.getModule();
                }

                // search in local file buffer
                try {
                    final Module mod = loadLocalModule(moduleAddress);
                    return mod;
                } catch (ModuleFileNotFoundException e) {     // file not found
                } catch (IOException e) {
                    Logger.getInstance().logFailureState("Loading of module failed!",
                    moduleAddress.getAddress(), "IO access problem");
                    throw e;
                }

                // try loading url directly
                if (makeLocalCopy(moduleAddress)) {
                    try {
                        final Module mod = loadLocalModule(moduleAddress);
                        return mod;
                    } catch (IOException e) {
                        Trace.trace(this, METHOD, e);
                        throw e;
                    } catch (ModuleFileNotFoundException e) {
                        Trace.trace(this, METHOD, e);
                        throw new IOException(e.getMessage());
                    }
                }
                final String ERROR = "Loading of module " + moduleAddress.getAddress()
                    + " failed, no access to this module!";
                Trace.trace(this, METHOD, ERROR);
                Logger.getInstance().logFailureState("Module URL not found", moduleAddress.getAddress(), ERROR);
                throw new IOException(
                    ModuleConstants.MODULE_NOT_FOUND + moduleAddress);
             }
         } finally {
             this.processDec();
         }
     }


    /**
     * Load a certain module.
     *
     * @param  module   this is the current module.
     * @param  spec     specification of wanted module.
     * @return wanted module
     * @throws ArgumentException    loading was not successfull.
     * @throws ParsingException     loading another module lead to
     *          an parsing error.
     * @throws IOException          <code>spec</code> didn't lead
     *          to a valid module address
     */
    public final Module loadModule(final Module module,
            final Specification spec)
            throws ArgumentException, ParsingException {

        final String METHOD = "loadModule(Module, Specification)";
        Trace.traceBegin(this, METHOD);
        Trace.trace(this, METHOD, spec);
        this.processInc();
        try {
            peek.setInput(null);  // stop peeking
            final ModuleAddress[] modulePaths
                = ModuleAddress.getModulePaths(module, spec);
            // search in already loaded modules
            for (int i = 0; i < modulePaths.length; i++) {
                final ModuleProperties prop
                    = modules.getModuleProperties(modulePaths[i]);
                synchronized (prop) {
                    if (prop.isCheckedAndLoaded()) {
                        return (prop.getModule());
                    }
                }
            }

            // search in local file buffer
            Trace.trace(this, METHOD, "searching file buffer");
            for (int i = 0; i < modulePaths.length; i++) {
                try {
                    final ModuleProperties prop
                        = modules.getModuleProperties(modulePaths[i]);
                    Trace.trace(this, METHOD, "synchronizing at prop=" + prop);
                    synchronized (prop) {
                        final Module mod = loadLocalModule(modulePaths[i]);
                        return mod;
                    }
                } catch (ModuleFileNotFoundException e) {  // file not found
                } catch (IOException e) {
                    Trace.trace(this, METHOD, e);
                    throw new ArgumentException(
                        ModuleConstants.CODE_METHOD_ARGUMENT_CHECK_FAILED,
                        e.toString(),
                        spec.getArgument(3).getArgument(i).getArgument(0));
                } catch (IllegalArgumentException e) {
                    Trace.trace(this, METHOD, e);
                    throw new ArgumentException(
                        ModuleConstants.CODE_METHOD_ARGUMENT_CHECK_FAILED,
                        e.toString(),
                        spec.getArgument(3).getArgument(i).getArgument(0));
                }
            }

            // try loading url directly
            for (int i = 0; i < modulePaths.length; i++) {
                try {
                    final ModuleProperties prop
                        = modules.getModuleProperties(modulePaths[i]);
                    synchronized (prop) {
                        if (makeLocalCopy(modulePaths[i])) {
                            final Module mod = loadLocalModule(modulePaths[i]);
                            return mod;
                        }
                    }
                 } catch (IOException e) {
                    Trace.trace(this, METHOD, e);
                    throw new ArgumentException(
                        ModuleConstants.CODE_METHOD_ARGUMENT_CHECK_FAILED,
                        e.toString(),
                        spec.getArgument(3).getArgument(i).getArgument(0));
                 } catch (ModuleFileNotFoundException e) {
                    Trace.trace(this, METHOD, e);
                    throw new ArgumentException(
                        ModuleConstants.CODE_METHOD_ARGUMENT_CHECK_FAILED,
                        e.toString(),
                        spec.getArgument(3).getArgument(i).getArgument(0));
                 } catch (IllegalArgumentException e) {
                    Trace.trace(this, METHOD, e);
                    throw new ArgumentException(
                        ModuleConstants.CODE_METHOD_ARGUMENT_CHECK_FAILED,
                        e.toString(),
                        spec.getArgument(3).getArgument(i).getArgument(0));
                 }
            }
            ArgumentException ex = new ArgumentException(
                ModuleConstants.CODE_METHOD_ARGUMENT_CHECK_FAILED,
                ModuleConstants.MODULE_NOT_FOUND + "\n" + spec,
                spec);
            Trace.trace(this, METHOD, ex);
            throw ex;
        } finally {
            this.processDec();
            Trace.traceEnd(this, METHOD);
        }
    }


    final void moduleCheckStarted(final ModuleAddress moduleAddress) {
        final ModuleProperties prop
            = this.modules.getModuleProperties(moduleAddress);
        if (prop != null) {
            prop.setProgressState(ModuleState.STATE_CHECKING);
            ModuleContext.listener.stateChanged(prop);
        } else {
            throw new IllegalStateException("moduleAddress not found: "
                + moduleAddress);
        }
    }


    public final List getAllCheckedModules() {
        return this.modules.getAllCheckedModules();
    }


    public final boolean loadPreviouslyCheckedModules() {
        this.processInc();
        try {
            final List list = Settings.getInstance().getPreviouslyCheckedModules();
            boolean errors = false;
            for (int i = 0; i < list.size(); i++) {
                try {
                    loadModule((String) list.get(i));
                } catch (IOException e) {
                    errors = true;
                } catch (ParsingException e) {
                    errors = true;
                }
            }
            return !errors;
        } finally {
            this.processDec();
        }
    }


    // TODO dynamic loading
    public final boolean loadAllModulesFromQedeq() {
        this.processInc();
        try {
            final String prefix = "http://www.qedeq.org/"
                + ModuleContext.getVersionDirectory() + "/";
            final String[] list = new String[] {
                prefix + "propaxiom_1.00.00_1.00.00.qedeq",
                prefix + "proptheo1_1.00.00_1.00.00.qedeq",
                prefix + "proptheo2_1.00.00_1.00.00.qedeq",
                prefix + "prophilbert2_1.00.00_1.00.00.qedeq",
                prefix + "prophilbert3_1.00.00_1.00.00.qedeq",
                prefix + "prophilbert1_1.00.00_1.00.00.qedeq",
                prefix + "predaxiom_1.00.00_1.00.00.qedeq",
                prefix + "subst_1.00.00_1.01.00.qedeq",
                prefix + "prophilbert1_1.00.00_1.02.00.qedeq",
                prefix + "prophilbert2_1.00.00_1.02.00.qedeq",
                prefix + "prophilbert3_1.00.00_1.02.00.qedeq",
                prefix + "predtheo1_1.00.00_1.00.00.qedeq",
                prefix + "predtheo2_1.00.00_1.00.00.qedeq",
                prefix + "predtheo2_1.00.00_1.02.00.qedeq",
                prefix + "lukaxiom_1.00.00_1.00.00.qedeq",
                prefix + "luktheo1_1.00.00_1.00.00.qedeq",
                prefix + "properror1_1.00.00_1.00.00.qedeq",
                prefix + "properror2_1.00.00_1.00.00.qedeq",
                prefix + "properror3_1.00.00_1.00.00.qedeq"
            };
            boolean errors = false;
            for (int i = 0; i < list.length; i++) {
                try {
                    loadModule((String) list[i]);
                } catch (IOException e) {
                    errors = true;
                } catch (ParsingException e) {
                    errors = true;
                }
            }
            return !errors;
        } finally {
            this.processDec();
        }
    }


    /**
     * Load a module.
     *
     * @param   address   module address
     * @return  loaded module
     * @throws  ModuleFileNotFoundException    if local file was not found
     * @throws  IOException if loading failed
     * @throws  ParsingException if loading the module lead to
     *           an parsing error
     */
    private final Module loadLocalModule(
            final ModuleAddress moduleAddress)
            throws ModuleFileNotFoundException, IOException, ParsingException  {
        final String METHOD = "loadLocalModule";
        final StringBuffer buffer = new StringBuffer();
        final File file = new File(moduleAddress.getLocalName());
        if (file.canRead()) {
            ModuleProperties prop = modules.getModuleProperties(moduleAddress);
            if (prop.getState() == ModuleState.STATE_UNDIFINED) {
                prop.setProgressState(ModuleState.STATE_LOADING_FROM_BUFFER);
                ModuleContext.listener.addModule(prop);
            } else {
                prop.setProgressState(ModuleState.STATE_LOADING_FROM_BUFFER);
                ModuleContext.listener.stateChanged(prop);
            }
            try {
                Utility.loadFile(file, buffer);
            } catch (IOException ioException) {
                Trace.trace(this, METHOD, ioException);
                prop.setFailureState(ModuleState.STATE_LOADING_FROM_BUFFER_FAILED, ioException);
                ModuleContext.listener.stateChanged(prop);
                throw ioException;
            }
            final TextInput in = new TextInput(buffer,
                moduleAddress.getAddress(),
                moduleAddress.getLocalName());
            final Argument argument;
            final TextInput oldIn = peek.getInput();
            peek.setInput(in);
            prop.setProgressState(ModuleState.STATE_LOADING_INTO_MEMORY);
            ModuleContext.listener.stateChanged(prop);
            try {
                try {
                    argument = (new ModuleCreator(in)).readArgument();
                } catch (ParsingException ex) {
                    Trace.trace(this, METHOD, ex);
                    prop.setFailureState(ModuleState.STATE_LOADING_INTO_MEMORY_FAILED, ex);
                    ModuleContext.listener.stateChanged(prop);
                    throw ex;
                }
                if (!(argument instanceof Module)) {
                    final ParsingException ex = new ParsingException(
                        ModuleConstants.CODE_PARSER_UNEXPECTED_ARGUMENT,
                        ModuleConstants.NO_FOLLOWING_TEXT_ALLOWED,
                        in.getRow(),
                        in.getColumn(),
                        in.getPosition(),
                        in.getLine(), in.getLocalAddress());
                    Trace.trace(this, METHOD, ex);
                    prop.setFailureState(ModuleState.STATE_LOADING_INTO_MEMORY_FAILED, ex);
                    ModuleContext.listener.stateChanged(prop);
                    throw ex;
                }
                in.skipWhiteSpace();
                if (in.getChar() != Character.MAX_VALUE) {
                    final ParsingException ex = new ParsingException(
                        ModuleConstants
                            .CODE_PARSER_NO_FURTHER_ARGUMENTS_EXPECTED,
                        ModuleConstants.NO_FOLLOWING_TEXT_ALLOWED,
                        in.getRow(),
                        in.getColumn(),
                        in.getPosition(),
                        in.getLine(), in.getLocalAddress());
                    Trace.trace(this, METHOD, ex);
                    prop.setFailureState(ModuleState.STATE_LOADING_INTO_MEMORY_FAILED, ex);
                    ModuleContext.listener.stateChanged(prop);
                    throw ex;
                }
                try {
                    ((Module) argument).setModuleAddress(moduleAddress);
                } catch (ArgumentException e) {
                    final ParsingException ex = new ParsingException(
                        ModuleConstants
                            .CODE_METHOD_ARGUMENT_CHECK_FAILED,
                        e.getDescription(),
                        0,
                        0,
                        0,
                        e.getArgument().toString(), moduleAddress.getLocalName());
                    Trace.trace(this, METHOD, ex);
                    prop.setFailureState(ModuleState.STATE_LOADING_INTO_MEMORY_FAILED, ex);
                    ModuleContext.listener.stateChanged(prop);
                    throw ex;
                }
            } finally {
                peek.setInput(oldIn);
            }
            prop.setCheckedAndLoaded((Module) argument);
            modules.setCheckedAndLoaded(prop);
            ModuleContext.listener.stateChanged(prop);
            return (Module) argument;
        } else {
            Trace.trace(this, METHOD, "file=" + file.getCanonicalPath());
            throw new ModuleFileNotFoundException("file not found");
        }
    }


    /**
     * Make local copy of a module if it is no file address.
     *
     * @param   address   module address
     * @return  success?
     * @throws  IOException    if address was malformed
     */
    private final synchronized boolean makeLocalCopy(
            final ModuleAddress moduleAddress)
            throws IOException {
        final String METHOD = "makeLocalCopy";
        Trace.traceBegin(this, METHOD);
        ModuleProperties prop = modules.getModuleProperties(moduleAddress);

        if (prop.getState() == ModuleState.STATE_UNDIFINED) {
            prop.setProgressState(ModuleState.STATE_LOADING_FROM_WEB);
            ModuleContext.listener.addModule(prop);
        } else {
            prop.setProgressState(ModuleState.STATE_LOADING_FROM_WEB);
            ModuleContext.listener.stateChanged(prop);
        }
        
        if (moduleAddress.isFileAddress()) {
            return true;
        }
        FileOutputStream out = null;
        InputStream in = null;
        try {
            final URLConnection connection = moduleAddress.getURL().openConnection();
            in = connection.getInputStream();
            if (!moduleAddress.getURL().equals(connection.getURL())) {
                throw new FileNotFoundException("\"" + moduleAddress.getURL()
                    + "\" was substituted by " + "\"" + connection.getURL()
                    + "\" from server");
            }
            {   // assure existence of directories
                String dir = moduleAddress.getLocalName();
                final File f = new File(dir).getParentFile();
                f.mkdirs();
            }
            out = new FileOutputStream(moduleAddress.getLocalName());
            final byte[] buffer = new byte[4096];
            int bytesRead;
            if ((bytesRead = in.read(buffer)) != -1) {
                int i = 0;
                final String first = new String(buffer);
                while (i < first.length() && Character.isWhitespace(first.charAt(i))) {
                    i++;
                }
                if (i + 6 >= first.length()
                        || !first.substring(i, i + 6).equals("MODULE")) {
                    throw new FileNotFoundException(
                        "this is no valid \".qedeq\" module file");
                }
                out.write(buffer, 0, bytesRead);
            } else {
                throw new IOException("module file has 0 bytes length");
            }
            // continue writing
            while ((bytesRead = in.read(buffer)) != -1) {
                out.write(buffer, 0, bytesRead);
            }
        } catch (IOException e) {
            Trace.trace(this, METHOD, e);
            try {
                out.close();
            } catch (Exception ex) {}
            try {
                new File(moduleAddress.getLocalName()).delete();
            } catch (Exception ex) {}
            prop.setFailureState(ModuleState.STATE_LOADING_FROM_WEB_FAILED, e);
            ModuleContext.listener.stateChanged(prop);
            Trace.trace(this, METHOD, "Couldn't access " + moduleAddress.getURL());
            return false;
        } finally {
            try {
                out.close();
            } catch (Exception e) {};
            try {
                in.close();
            } catch (Exception e) {};
            Trace.traceEnd(this, METHOD);
        }
        return true;
    }

    /**
     * Increment intern process counter.
     */
    private final void processInc() {
        synchronized (syncToken) {
            this.processCounter++;
        }
    }


    /**
     * Decrement intern process counter.
     */
    private final void processDec() {
        synchronized (syncToken) {
            this.processCounter--;
        }
    }


    /**
     * Get buffer directory for qedeq module files.
     *
     * @return  buffer directory.
     */
    public static String getBufferDirectory() {
        return Settings.getInstance().getBufferDirectory();
    }


    /**
     * Get directory for generated files.
     *
     * @return  generation directory
     */
    public static final String getGenerationDirectory() {
        return Settings.getInstance().getGenerationDirectory();
    }


    /**
     * @param string
     */
    public final ModuleProperties getModuleProperties(final String address) {
        
        try {
            final ModuleProperties prop = modules.getModuleProperties(address);
            return prop;
        } catch (IOException e) {
            Trace.trace(this, "getModuleProperties", e);
            return null;
        }
    }





    private static final class ModuleFileNotFoundException extends Exception {
        ModuleFileNotFoundException(final String message) {
            super(message);        }

    }


    /**
     * Encapsulates all modules.
     */
    private class Modules {


        /** properties of modules; key: ModuleAddress, value: ModuleProperties.*/
        private final Map moduleProperties = new HashMap();

        /** dependencies of modules; key: ModuleProperties, value: List of ModuleProperties.*/
        private final Map dependencies = new HashMap();


        /** 
         * Get module properties for an url. If it is unkown it will be created. 
         *  
         * @param   url     location of module
         * @return  module properties
         * @throws  IOException     url is mailformed
         */
        final ModuleProperties getModuleProperties(final String url) throws IOException {
            synchronized (moduleProperties) {
                final ModuleAddress address = new ModuleAddress(url);
                if (moduleProperties.containsKey(address)) {
                    return (ModuleProperties) moduleProperties.get(address);
                } else {
                    final ModuleProperties prop = new ModuleProperties(address);
                    moduleProperties.put(address, prop);
                    return prop;
                }
            }
        }


        /** 
         * Get module properties for an module address. If it is unkown it will be created. 
         *  
         * @param   address     module address
         * @return  module properties
         * @throws  IOException     url is mailformed
         */
        final ModuleProperties getModuleProperties(final ModuleAddress address) {
            synchronized (moduleProperties) {
                if (moduleProperties.containsKey(address)) {
                    return (ModuleProperties) moduleProperties.get(address);
                } else {
                    final ModuleProperties prop = new ModuleProperties(address);
                    moduleProperties.put(address, prop);
                    return prop;
                }
            }
        }


        /**
         * Remove all modules from memory.
         */
        final void removeAllModules() {
            final String METHOD = "removeAllModules";
            Trace.traceBegin(this, METHOD);
            try {
                synchronized (moduleProperties) {
                    for (final Iterator iterator
                            = moduleProperties.entrySet().iterator();
                            iterator.hasNext(); ) {
                        Map.Entry entry = (Map.Entry) iterator.next();
                        final ModuleProperties prop = (ModuleProperties) entry.getValue();
                        Trace.trace(this, METHOD, "remove " +  prop);
                        ModuleContext.listener.removeModule(prop);
                    }
                    moduleProperties.clear();
                }
            } catch (RuntimeException e) {
                Trace.trace(this, METHOD, e);
            } finally {
                Trace.traceEnd(this, METHOD);
            }
        }


        /**
         * Remove a module and all its dependants from memory.
         * 
         * @param   prop
         */
        final void removeModuleAndDependents(final ModuleProperties prop) {
            final String METHOD = "removeModuleAndDependents";
            Trace.traceBegin(this, METHOD);
            try {
                synchronized (moduleProperties) {
                    Trace.trace(this, METHOD, "remove module and dependents of " +  prop.getAddress());
//                    if (!moduleProperties.containsKey(prop)) {
//                        throw new IllegalArgumentException("unknown module: " + prop.getAddress());
//                    }
                    if (!prop.isCheckedAndLoaded()) {
                        Trace.trace(this, METHOD, "removing " +  prop.getAddress());
                        ModuleContext.listener.removeModule(prop); 
                        moduleProperties.remove(prop.getModuleAddress());
                    } else {                   
                        final List usedby = (List) this.dependencies.get(prop);
                        Trace.trace(this, METHOD, "used by size=" + usedby.size()); 
                        for (int i = usedby.size() - 1; i >= 0; i--) {
                            final ModuleProperties p = (ModuleProperties) usedby.get(i);
                            Trace.trace(this, METHOD, "remove referenced module: " +  p.getAddress());
                            ModuleContext.listener.removeModule(p);
                            this.moduleProperties.remove(p.getModuleAddress());
                            removeDependencies(p);
                            Trace.trace(this, METHOD, "module number=" + moduleProperties.size()); 
                        }
                        Trace.trace(this, METHOD, "module number=" + moduleProperties.size());
                        Trace.trace(this, METHOD, "removing module itself: " +  prop.getAddress());
                        ModuleContext.listener.removeModule(prop); 
                        moduleProperties.remove(prop.getModuleAddress());
                        Trace.trace(this, METHOD, "module number=" + moduleProperties.size());
                        removeDependencies(prop);
                    }
                }
            } catch (RuntimeException e) {
                Trace.trace(this, METHOD, e);
            } finally {
                Trace.traceEnd(this, METHOD);
            }
        }


        private void removeDependencies(final ModuleProperties prop) {
            final String METHOD = "removeDependencies";
            final Map dependency = prop.getModule().getReferencedModules();
            for (final Iterator iterator 
                    = dependency.entrySet().iterator();
                    iterator.hasNext(); ) {
                Map.Entry entry = (Map.Entry) iterator.next();
                final ModuleAddress addr = (ModuleAddress) entry.getKey();
                ModuleProperties p = (ModuleProperties) moduleProperties.get(addr);
                Trace.trace(this, METHOD, "   removing dependency from: " + p.getAddress());
                final List usedby2 = (List) this.dependencies.get(p);
                if (usedby2.remove(prop)) {
                    Trace.trace(this, METHOD, "    entry removed");
                } else {
                    Trace.trace(this, METHOD, "    no entry removed");
                } 
                this.dependencies.remove(prop);
            }
        }


        /**
         * Remember module dependencies.
         * 
         * @param   prop    module, that is just checked and loaded
         */
        public void setCheckedAndLoaded(final ModuleProperties prop) {
            final String METHOD = "setCheckedAndLoaded";
            Trace.traceBegin(this, METHOD);
            try {
                synchronized (moduleProperties) {
                    Trace.trace(this, METHOD, "adding dependencies for " + prop.getAddress());
                    this.dependencies.put(prop, new ArrayList());
                    final Map dependency = prop.getModule().getReferencedModules();
                    for (final Iterator iterator 
                            = dependency.entrySet().iterator();
                            iterator.hasNext(); ) {
                        Map.Entry entry = (Map.Entry) iterator.next();
                        final ModuleAddress addr = (ModuleAddress) entry.getKey();
                        ModuleProperties p = (ModuleProperties) moduleProperties.get(addr);
                        Trace.trace(this, METHOD, "   adding: " + p.getAddress());
                        final List usedby = (List) this.dependencies.get(p);
                        usedby.add(prop);
                    }
                }
            } finally {
                Trace.traceEnd(this, METHOD);
            }
            
        }



        /**
         * Get list of all successfully checked modules.
         * 
         * @return  list of all successfully checked modules.
         */
        final List getAllCheckedModules() {
            final String METHOD = "getAllModules";
            Trace.traceBegin(this, METHOD);
            try {
                List list = new ArrayList();
                synchronized (moduleProperties) {
                    for (final Iterator iterator
                            = moduleProperties.entrySet().iterator();
                            iterator.hasNext(); ) {
                        Map.Entry entry = (Map.Entry) iterator.next();
                        final ModuleProperties prop = (ModuleProperties) entry.getValue();
                        if (prop.getState() == ModuleState.STATE_LOADED_AND_VERIFIED) {
                            list.add(prop.getAddress());
                        }
                    }
                }
                return list;
            } finally {
                Trace.traceEnd(this, METHOD);
            }
        }



    }



    /**
     * This thread watches the reading process of a {@link TextInput} object.
     * After creating a <code>PeekTextInput</code> instance and starting
     * the thread by calling the <code>start</code>-Method, an object
     * to watch could be set by calling <code>setInput</code>.
     * It logs the reading position after 10 seconds on log level 1 (if
     * there is a {@link TextInput} object.
     */
    private class PeekTextInput extends Thread {

        private TextInput reader = null;

        private long maximum;

        private boolean stopped = false;

        private Thread myThread = null;

        /**
         * Set object to watch (could be <code>null</code>.
         *
         * @param   reader  look at this object
         */
        private final void setInput(final TextInput reader) {
            synchronized (this) {   // inform waiting threads
                this.reader = reader;
                if (reader != null) {
                    this.maximum = reader.getMaximumPosition();
                }
                if (myThread != null) {
                    myThread.interrupt();
                }
                else {
                    this.notifyAll();
                }
            }
        }


        /**
         * Get the current {@link TextInput} object.
         *
         * @return  watched object (could be <code>null</code>).
         */
        private final TextInput getInput() {
            return reader;
        }


       /**
        * Main loop.
        */
        public void run() {
            final String METHOD = "run";
            myThread = Thread.currentThread();
            while (!stopped) {
                synchronized (this) {
                    try {
                        this.wait(10000);
                    } catch (InterruptedException e) {
                        continue;
                    }
                    if (reader == null) {
                        continue;
                    }
                    final long position = reader.getPosition();
                    String percent = "" + (position * 100 / maximum);
                    try {
                        ModuleProperties prop = modules.getModuleProperties(
                            reader.getAddress());
                        if (prop != null) {
                            prop.setCompleteness((int) (position * 100 / maximum));
                            ModuleContext.listener.stateChanged(prop);
                        } else {
                            Trace.trace(this, METHOD, "not found: " + reader.getAddress());
                        }
                    } catch (IOException e) {
                        Trace.trace(this, METHOD, e);
                    }
/*                    
                    switch (percent.length()) {
                    case 1: percent = "  " + percent;
                        break;
                    case 2: percent = " " + percent;
                        break;
                    }
                    String row = "" + reader.getRow();
                    switch (row.length()) {
                    case 1: row = "        " + row;
                        break;
                    case 2: row = "       " + row;
                        break;
                    case 3: row = "      " + row;
                        break;
                    case 4: row = "     " + row;
                        break;
                    case 5: row = "    " + row;
                        break;
                    case 6: row = "   " + row;
                        break;
                    case 7: row = "  " + row;
                        break;
                    case 8: row = " " + row;
                        break;
                    }
                    Logger.getInstance().logMessage("  " + percent + "% read, line number "
                        + row + " of:\n\t" + reader.getAddress());
*/
                    if (position == maximum) {
                        reader = null;
                    }
                }
            }
        }


        /**
         * Call this method to stop this thread.
         */
        public void stopPeek() {
            synchronized (this) {   // inform waiting threads
                stopped = true;
                if (myThread != null) {
                    myThread.interrupt();
                }
                else {
                    this.notifyAll();
                }
            }
        }

    }


}
