package de.renew.shadowcompiler;

import java.io.File;
import java.util.LinkedHashSet;
import java.util.Properties;
import java.util.Set;

import org.apache.log4j.Logger;

import de.renew.simulatorontology.loading.Finder;
import de.renew.simulatorontology.loading.NetNotFoundException;
import de.renew.simulatorontology.loading.PathlessFinder;
import de.renew.simulatorontology.shadow.ShadowNet;
import de.renew.simulatorontology.shadow.ShadowNetLoader;
import de.renew.simulatorontology.shadow.ShadowNetSystem;
import de.renew.util.PathEntry;
import de.renew.util.StringUtil;


/**
 * Tries to load shadow nets from registered {@link Finder}
 * sources. The loader lets the registered finders look
 * for these files in the directories specified by the
 * {@code de.renew.netPath} system property.
 * <p>
 * DefaultShadowNetLoader.java <br>
 * Created: Thu Jan 17  2002 <br>
 * Modified: 12/29/2004 to enable use of finders by Till Kothe <br>
 * @author Michael Duvigneau
 * @author Till Kothe
 **/
public class DefaultShadowNetLoader implements ShadowNetLoader {
    /** The logger for this class. */
    private static final Logger LOGGER = Logger.getLogger(DefaultShadowNetLoader.class);

    //We need to use a LinkedHashSet otherwise we can not guaranty 
    //that the first registered finder (SNS) is asked first.
    private final Set<Finder> _finders = new LinkedHashSet<>();
    private final Set<PathlessFinder> _pathlessFinders = new LinkedHashSet<>();

    /**
     * An array of path entries denoting the directories where to
     * look for serialized {@code ShadowNetSystem} files.
     **/
    private PathEntry[] _netSource;

    /**
     * Creates a new {@code ShadowNetLoader}. Uses the given properties to configure
     * the net path. If the given properties are {@code null}, the default
     * properties are used instead. <br>
     * See also: {@link #configureNetPath(Properties)}
     *
     * @param props the property set to configure the {@code netPath} with
     *              or {@code null} if the default properties should be used
     */
    public DefaultShadowNetLoader(Properties props) {
        // TODO: the following maybe needs to go somewhere else
        // register a new default SNSFinder
        registerFinder(new SNSFinder());

        if (props != null) {
            configureNetPath(props);
        } else {
            configureNetPath(System.getProperties());
        }
    }

    /**
     * Configures the net search path from a given property set.
     * See also: {@link #setNetPath(String)}
     *
     * @param props the property set to extract the
     *              {@code de.renew.netPath} property from
     **/
    public void configureNetPath(Properties props) {
        setNetPath(props.getProperty("de.renew.netPath", System.getProperty("user.dir")));
    }

    /**
     * Sets a search path (like the CLASSPATH) to look for shadow
     * net system files when a net is missing. The directories in
     * the path are separated by {@link File#pathSeparatorChar}.
     **/
    void setNetPath(String path) {
        setNetPath(StringUtil.splitPaths(path));
    }

    /**
     * Sets search paths (like the CLASSPATH) to look for net
     * drawing files when a drawing is missing. Each String in the
     * array denotes exactly one directory to search.
     **/
    void setNetPath(String[] paths) {
        _netSource = StringUtil.canonizePaths(paths);
        for (PathEntry pathEntry : _netSource) {
            LOGGER.debug(
                "Shadow net loader source"
                    + (pathEntry.isClasspathRelative ? " (relative to CLASSPATH): " : ": ")
                    + pathEntry.path);
        }
    }

    //    /**
    //     * Creates an array of path entries from an array of path strings.
    //     * Strings starting with the reserved word <code>"CLASSPATH"</code> are
    //     * converted to classpath-relative path entries. All other path strings
    //     * are converted to path entries that point to canonized directory
    //     * names.
    //     *
    //     * @param paths  the <code>String</code> array with path names.
    //     * @return an array of <code>PathEntry</code> objects.
    //     **/
    //    public static PathEntry[] canonizePaths(String[] paths) {
    //        if (paths == null) {
    //            return new PathEntry[0];
    //        }
    //        PathEntry[] canonizedEntries = new PathEntry[paths.length];
    //        for (int i = 0; i < paths.length; ++i) {
    //            if (paths[i].trim().startsWith("CLASSPATH" + File.separator)) {
    //                canonizedEntries[i] = new PathEntry(paths[i].trim()
    //                                                            .substring(9
    //                                                                       + File.separator
    //                                                                         .length()),
    //                                                    true);
    //            } else if (paths[i].trim().equals("CLASSPATH")) {
    //                canonizedEntries[i] = new PathEntry("", true);
    //            } else {
    //                canonizedEntries[i] = new PathEntry(StringUtil.makeCanonical(paths[i]),
    //                                                    false);
    //            }
    //        }
    //        return canonizedEntries;
    //    }

    /**
     * Creates an OS-dependent path string from the given array of path
     * entries. This is the reverse function of
     * {@code paths = canonizePaths(StringUtil.splitPaths(pathString))}.
     *
     * @param paths an array of {@code PathEntry} objects
     * @return a path {@code String} including all entries of the given array or
     *         {@code ""} if the array is empty or non-existent
     **/
    public static String asPathString(PathEntry[] paths) {
        if (paths == null || paths.length == 0) {
            return "";
        }
        boolean first = true;
        StringBuilder builder = new StringBuilder();
        for (PathEntry path : paths) {
            if (path != null) {
                if (first) {
                    first = false;
                } else {
                    builder.append(File.pathSeparator);
                }
                if (path.isClasspathRelative) {
                    builder.append("CLASSPATH");
                    if (!"".equals(path.path)) {
                        builder.append(File.separator);
                    }
                }
                builder.append(path.path);
            }
        }
        return builder.toString();
    }

    @Override
    public ShadowNetSystem loadShadowNetSystem(String netName) throws NetNotFoundException {
        ShadowNetSystem loaded = findShadowNetSystemFile(netName);
        if (loaded != null) {
            return loaded;
        }
        throw new NetNotFoundException(netName);
    }

    @Override
    public ShadowNet loadShadowNet(String netName, ShadowNetSystem netSystem)
        throws NetNotFoundException
    {
        ShadowNetSystem foundNetSystem = loadShadowNetSystem(netName);
        ShadowNet foundNet = foundNetSystem.elements().iterator().next();
        if (foundNet != null && netName.equals(foundNet.getName())) {
            ShadowNetSystemCompiler.getInstance().switchNetSystem(foundNet, netSystem);
            return foundNet;
        }
        throw new NetNotFoundException(netName);
    }

    /**
     * Tries to find and load a net file for the given net name
     * from the configured net path or registered pathless finders.
     * The {@link Finder}s registered with this {@code
     * DefaultShadowNetLoader} are used to accomplish
     * the path-relative search.
     * For each net path entry the finders are asked
     * for the given net until one is found.
     * FIRST, the {@link PathlessFinder}s are queried.
     * (FIXME: this should be afterwards or in between).
     * The shadow net is loaded and returned. <br>
     * See also: {@link #configureNetPath(Properties)}
     *
     * @param name the name of the net to load
     * @return the fresh shadow net, if one was loaded -
     *         {@code null}, if no matching file could be found
     */
    public ShadowNetSystem findShadowNetSystemFile(String name) {
        ShadowNetSystem theNetSystem; // this will be returned. will be null if none is found.

        // forall pathless finders
        for (PathlessFinder finder : _pathlessFinders) {
            LOGGER.debug("Searching for net " + name + " in pathless finder " + finder);
            theNetSystem = finder.findNetFile(name);
            if (theNetSystem != null && !theNetSystem.elements().isEmpty()) {
                // Net has been found. Return the containing net system!
                return theNetSystem;
            }
        }

        // forall netPath
        for (PathEntry pathEntry : _netSource) {
            StringBuilder builder = new StringBuilder();
            builder.append(pathEntry.path);
            if (!"".equals(pathEntry.path)) {
                builder.append(File.separator);
            }
            builder.append(name);
            String path = builder.toString();

            // path is net's name WITHOUT extension!
            // forall finder
            for (Finder finder : _finders) {
                LOGGER.debug("Searching for net " + builder + " in finder " + finder);
                if (pathEntry.isClasspathRelative) {
                    theNetSystem = finder.findNetClasspathRel(name, path);
                } else {
                    theNetSystem = finder.findNetFile(name, path);
                }
                if (theNetSystem != null && !theNetSystem.elements().isEmpty()) {
                    // Net has been found. Return the containing net system!
                    return theNetSystem;
                }
            }
        }

        return null;
    }

    /**
     * Registers a new {@code Finder} which will be used when nets are
     * requested.
     * At most one instance of every {@code Finder} implementation will be
     * registered.
     *
     * @param finder the {@code Finder} to register
     * @return {@code true} if the finder was added - {@code false}
     *         if it wasn't, i.e. a finder of the same type was already registered
     */
    public boolean registerFinder(Finder finder) {
        return _finders.add(finder);
    }

    /**
     * Removes a {@code Finder} from the list of {@code Finder}s
     * which are used to load nets.
     *
     * @param finder the {@code Finder} to remove
     * @return {@code true} if the finder has been successfully removed -
     *         {@code false} if it wasn't in the list of the registered finders
     */
    public boolean removeFinder(Finder finder) {
        return _finders.remove(finder);
    }

    /**
     * Registers a new {@code PathlessFinder} which will be used when nets are
     * requested.
     * At most one instance of every {@code PathlessFinder} implementation will be
     * registered.
     * @param finder the {@code PathlessFinder} to register.
     * @return {@code true} if the finder was added - {@code false}
     *         if it wasn't, i.e. a finder of the same type was already registered
     */
    public boolean registerPathlessFinder(PathlessFinder finder) {
        return _pathlessFinders.add(finder);
    }

    /**
     * Removes a {@code PathlessFinder} from the list of {@code Finder}s
     * which are used to load nets.
     *
     * @param finder the {@code PathlessFinder} to remove
     * @return {@code true} if the finder has been successfully removed -
     *         {@code false} if it wasn't in the list of the registered finders
     */
    public boolean removePathlessFinder(PathlessFinder finder) {
        return _pathlessFinders.remove(finder);
    }
}