/*-------------------------------------------------------------------------------
Copyright (c)2000 TogetherSoft Corporation. Patents pending. All rights reserved.
-------------------------------------------------------------------------------*/

package com.togethersoft.modules.comparedependencies;

import java.util.Vector;
import java.util.Hashtable;
import java.util.Enumeration;

import com.togethersoft.openapi.util.Comparing;
import com.togethersoft.openapi.ide.IdeScript;
import com.togethersoft.openapi.ide.IdeContext;
import com.togethersoft.openapi.ide.IdeAccess;
import com.togethersoft.openapi.ide.message.IdeMessageType;
import com.togethersoft.openapi.ide.message.IdeMessageManagerAccess;
import com.togethersoft.openapi.ide.project.IdeProjectManagerAccess;
import com.togethersoft.openapi.rwi.RwiVisitorAdapter;
import com.togethersoft.openapi.rwi.RwiLink;
import com.togethersoft.openapi.rwi.RwiModelAccess;
import com.togethersoft.openapi.rwi.RwiModel;
import com.togethersoft.openapi.rwi.RwiProperty;
import com.togethersoft.openapi.rwi.RwiPackage;
import com.togethersoft.openapi.rwi.RwiDiagram;
import com.togethersoft.openapi.rwi.RwiShapeType;
import com.togethersoft.openapi.rwi.RwiElement;
import com.togethersoft.openapi.rwi.enum.RwiPackageEnumeration;
import com.togethersoft.openapi.rwi.enum.RwiLinkEnumeration;
import com.togethersoft.openapi.uml.UmlModelAccess;

/**
 * The module which checks manual dependencies between packages against automatic.
 * <br>
 * This is useful to see that no unplanned dependencies crept into a design. It does this by checking that
 * every dependency derieved from the source code has a matching userdrawn dependency
 * <br>
 * It generates automatic dependencies for the whole project, then checks that
 * every automatic dependency have 'twin' manual dependency. Those which have 'twins'
 * are removed from model, others are reported in message window.
 * <br>
 * <br>
 * <em>Problems:</em>
 * <ul>
 * <li> Automatic package dependencies are propagated up along package hierarchy, so if
 * package <code>package1.package11</code> depends on <code>package2.package22</code>,
 * then automatic dependencies will be created also between <code>package1.package11</code>
 * and <code>package2</code>, between <code>package1</code> and <code>package2</code>
 * and to/from default package. Default package is filtered out, but it's hard to know whether
 * dependency betwen packeges was infered from dependencies between classes or between
 * subpackages.
 * <li> Often links are not shown anywhere at default diagram. To get link shown one must have
 * both link ends shown on diagram. If packages are not subpackages of same package there is no
 * diagram which shows both by default
 * </ul>
 *
 * @author TogetherSoft
 */
public class CompareDependencies extends RwiVisitorAdapter implements IdeScript
{
   /**
    * Runs this module.
    *
    * @param context the IdeContext instance containing
    * the selection information at the moment the module was called.
    */
    public void run(IdeContext context)
    {
        // Check if there is open project
        if (IdeProjectManagerAccess.getProjectManager().getActiveProject() == null)
        {
            // show modal window with error description
            IdeMessageManagerAccess.printMessage
                (IdeMessageType.ERROR_MODAL, "No open project.");
            return;
        }

        myNewLinks = 0;
        myRemovedLinks = 0;

        // Update package dependencies, this is the same as calling this
        // from the diagram speedmenu.
        UmlModelAccess.getModel().updatePackageDependencies();
        RwiModel model = RwiModelAccess.getModel();
        for ( RwiPackageEnumeration packageEnum = model.rootPackages();
              packageEnum.hasMoreElements(); )
        {
            // visit all elements, callsback to "visitPackage" below
            packageEnum.nextRwiPackage().accept(this);
        }

        performChanges();

        // information message
        if ((myNewLinks == 0) && (myRemovedLinks == 0))
	    {
		    IdeMessageManagerAccess.printMessage
		    (IdeMessageType.INFORMATION, "Current state is up to date.");
        }
    }

    /**
    * RwiVisitor method used to process package.
    * @param pkg RWI package
    * @return always null
    */
    public Object visitPackage(RwiPackage pkg)
    {
        // Return if package doesn't contain source file
        if (!pkg.hasProperty(RwiProperty.DIRECTORY_PACKAGE))
        {
            return null;
        }

        // Return if package is not part of the model, e.g. used only for import
        if (!pkg.hasProperty(RwiProperty.MODEL))
        {
            return null;
        }

        // Package dependencies are actually links between correspondent
        // physical diagrams
        RwiDiagram physicalDiagram = pkg.getPhysicalDiagram();

	    // every package has a diagram by default,
        // e.g. readonly directory may prevent this
        if (physicalDiagram != null)
        {
            RwiLinkEnumeration linkEnum = physicalDiagram.outgoingLinks();

            Hashtable storage = new Hashtable();
            while (linkEnum.hasMoreElements())
            {
                RwiLink dependency = linkEnum.nextRwiLink();
                if (!Comparing.equal(RwiShapeType.DEPENDENCY,
                dependency.getProperty(RwiProperty.SHAPE_TYPE)))
                {
                    continue;
                }
                if (dependency.getDestination() instanceof RwiDiagram)
                {
                    processLink(dependency, storage);
                }
            }
            generateReport(storage);
        }

        // Recurse into subpackages
        for ( RwiPackageEnumeration subpackageEnum = pkg.subpackages();
            subpackageEnum.hasMoreElements(); )
        {
            subpackageEnum.nextRwiPackage().accept(this);
        }
        return null;
    }

    /**
     * Place where actual work gets done.
     * @param dependency link to be checked against currently accumulated data
     * @param history essential information about already processed links.
     *        Maps destination to link
     */
    private void processLink(RwiLink dependency, Hashtable history)
    {
        // getting source and destination elements of link
        RwiElement source = dependency.getSource();
        RwiElement dest = dependency.getDestination();

        // Filter ou links to/from top level (default) package,
        // no place to show them
        if (Comparing.equal(dest.getProperty(RwiProperty.FULL_NAME), ""))
        {
            return;
        }
        if (Comparing.equal(source.getProperty(RwiProperty.FULL_NAME), ""))
        {
            return;
        }

        // history table keeps track of visited links and found links.
        // It maps link destination to link itself
        RwiLink oldLink = (RwiLink)history.get(dest);
        if (oldLink == null)
        {
            history.put(dest, dependency);
        }
        else
        {
            if (dependency.hasProperty(AUTOMATIC))
            {
                if (!oldLink.hasProperty(AUTOMATIC))
                {
                    // If there is already a manual link, this has priority
                    // over the automatic one.
                    // So, remove the automatic link
                    removeLink(dependency);
                }
                else
                {
                    // Unexpected situation: two automatic dependencies
                    // between same packages should not happen, issue warning
                    warning("Several automatic links ", dependency);
                }
            }
            else
            {
                // manual dependency
                if (oldLink.hasProperty(AUTOMATIC))
                {
                    // Same as above: Manually created dependency link
                    // has priority so remove the automatic one
                    history.put(dest, dependency);
                    removeLink(oldLink);
                }
            }
        }
        // At the end history contains manual dependencies when found
        // and automatic dependencies to be reported
    }

    /**
     * Output messages about each automatic link which doesn't have
     * correspondent manual link.
     * @param history previously collected data, maps destination to found link.
     */
    private void generateReport(Hashtable history)
    {
        for (Enumeration linkEnum = history.elements(); linkEnum.hasMoreElements(); )
        {
            RwiLink link = (RwiLink)linkEnum.nextElement();
            if (link.hasProperty(AUTOMATIC))
            {
                error("Unspecified dependency link ", link);
                myNewLinks++;
            }
        }
    }

    /**
     * Queue link for later removal. Removed is delayed
     * to protect all active enumerations.
     * @param link link to be removed
     */
    private void removeLink(RwiLink link)
    {
        myRemoveList.addElement(link);
        myRemovedLinks++;
    }

    /**
     * Final step--make all pending changes persistent. Actually removes
     * elements queued for removal.
     */
    private void performChanges()
    {
        for (Enumeration removeEnum = myRemoveList.elements(); removeEnum.hasMoreElements(); )
        {
            RwiElement element = (RwiElement)removeEnum.nextElement();
            element.delete();
        }
    }

    /**
     * Output warning message.
     * @param prefix leading part of message to output. It will be appended by link information
     * @param link link to be hyperlinked to resulting message
     */
    private void warning(String prefix, RwiLink link)
    {
        message(IdeMessageType.WARNING, prefix, link);
    }

    /**
     * Output error message.
     * @param prefix leading part of message to output. It will be appended by link information
     * @param link link to be hyperlinked to resulting message
     */
    private void error(String prefix, RwiLink link)
    {
        message(IdeMessageType.ERROR, prefix, link);
    }

    /**
     * Output message hyperlinked to specific link.
     * @param messageType type of this message as defined in <code>IdeMessageType</code>
     * @param prefix leading part of message to output. It will be appended by link information
     * @param link link to be hyperlinked to resulting message
     * @see com.togethersoft.openapi.ide.message.IdeMessageType
     */
    private void message(int messageType, String prefix, RwiLink link)
    {
        // First we build message
        RwiElement toSelect = link;

        String sourceName = link.getSource().getProperty(RwiProperty.FULL_NAME);
        if (sourceName == null || sourceName.length() == 0) {
            sourceName = "<unnamed>";
        }

        String destName = link.getDestination().getProperty(RwiProperty.FULL_NAME);
        if (destName == null || destName.length() == 0) {
            destName = "<unnamed>";
        }

        String text = prefix + " between " + sourceName + " and " + destName;

        // We need url to link message message to element
        String url = null;
        RwiDiagram diagram = null;
        boolean searchForDiagram = true;

        // Workaround: findDiagramFor() called on diagram returns
        // diagram itself. We need containing diagram, where our
        // target diagram is shown
        if (toSelect instanceof RwiDiagram)
        {
            RwiPackage pkg = ((RwiDiagram)toSelect).getContainingPackage();

            if (pkg != null)
            {
                pkg = pkg.getContainingPackage();
            }

            if (pkg != null)
            {
                diagram = pkg.getPhysicalDiagram();
            }

            searchForDiagram = false;
        }
        else if (toSelect instanceof RwiLink)
        { // And same for link from diagram
            RwiLink innerLink = (RwiLink)toSelect;
            if (innerLink.getSource() instanceof RwiDiagram)
            {
                RwiPackage pkg =
                    ((RwiDiagram)innerLink.getSource()).getContainingPackage();

                if (pkg != null)
                {
                    pkg = pkg.getContainingPackage();
                }

                if (pkg != null)
                {
                    diagram = pkg.getPhysicalDiagram();
                }

                searchForDiagram = false;
            }
        }

        // If no workaround used, find diagram in usual way
        if (searchForDiagram)
        {
            diagram = RwiModelAccess.getModel().findDiagramFor(toSelect);
        }

        //Now we have enough information to create url
        if (diagram != null)
        {
            url = IdeAccess.getIdeManager().buildDiagramURL(diagram, toSelect);
        }

        IdeMessageManagerAccess.printMessage(messageType, text, url, null);
    }

    /**
    * Property which differentiates automatic dependencies from manual.
    */
    private static final String AUTOMATIC = "automatic";

    /**
    * Container to store links marked for removal
    */
    private Vector myRemoveList = new Vector();

    // counters for links
    private int myNewLinks = 0;
    private int myRemovedLinks = 0;

}
