/*******************************************************************************
 * Copyright (c) 2009 SpringSource and others.
 * All rights reserved. This program and the accompanying materials 
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     Andrew Eisenberg - initial API and implementation
 *******************************************************************************/

package org.eclipse.ajdt.core.model;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.aspectj.ajdt.internal.core.builder.AjState;
import org.aspectj.asm.IProgramElement;
import org.aspectj.asm.IRelationship;
import org.aspectj.asm.IRelationshipMap;
import org.aspectj.asm.internal.RelationshipMap;
import org.eclipse.ajdt.core.AJLog;
import org.eclipse.ajdt.core.javaelements.AdviceElement;
import org.eclipse.ajdt.core.javaelements.DeclareElement;
import org.eclipse.ajdt.core.javaelements.IntertypeElement;
import org.eclipse.jdt.core.IJavaElement;

/**
 * @author Andrew Eisenberg
 * @created Feb 3, 2009
 *
 * Performs sanity checking on the crosscutting model.
 * 
 * The following rules are checked:
 * 
 * 1. Advice relationships target only methods, fields, and AJCodeElements
 * 2. Aspect Declarations (ITDs) target only types
 * 
 */
public class AJModelChecker {
    private AJModelChecker() {
        // not instantiable
    }
    
    
    public static void doModelCheckIfRequired(AJProjectModelFacade model) {
        if (shouldCheckModel()) {
            AJLog.logStart("Model sanity check for: " + model.getProject().getName());
            List problems = internalCheckModel(model);
            logProblems(problems);
            AJLog.logEnd(AJLog.MODEL, "Model sanity check for: " + model.getProject().getName());
        }
    }
    
    
    public static boolean shouldCheckModel() {
        // if the state listener is not null, this means that debug tracing is enabled.
        return AjState.stateListener != null;
    }
    
    
    private static void logProblems(List/*String*/ problems) {
        if (problems.size() == 0) {
            AJLog.log(AJLog.MODEL, "Crosscutting model sanity checked with no problems");
            return;
        }
        
        AJLog.log(AJLog.MODEL, "Crosscutting model sanity checked.  The following problems found:");
        for (Iterator probIter = problems.iterator(); probIter.hasNext();) {
            String problem = (String) probIter.next();
            AJLog.log(AJLog.MODEL, problem);
        }
        AJLog.log(AJLog.MODEL, "");
    }
    
    /**
     * iterates through all relationships and checks to see that it passes all rules
     * 
     * returns list of questionable relationships as strings.  An empty list is returned if the
     * model is OK. 
     */
    private static List/*String*/ internalCheckModel(AJProjectModelFacade model) {
        IRelationshipMap relationships = model.getAllRelationships();
        List/*String*/ problems = new ArrayList();
        if (relationships != null) {
            for (Iterator relIter = relationships.getEntries().iterator(); relIter.hasNext();) {
                String handle = (String) relIter.next();
                List relsForHandle = relationships.get(handle);
                for (Iterator relIter2 = relsForHandle.iterator(); relIter2
                        .hasNext();) {
                    IRelationship rel = (IRelationship) relIter2.next();
                    List res = invalidAdviceRelationsip(rel, model);
                    problems.addAll(res);
                    res = itdsNotOnType(rel, model);
                    problems.addAll(res);
                }
            }
        } else {
            problems.add("No relationshipes found");
        }
        
        return problems;
    }
    
//    private static List/*String*/ roundTripTest(IRelationship rel, AJProjectModelFacade model) {
//        List/*String*/ problems = new ArrayList();
//        
//    }
//    
//    private static List/*String*/ roundTripTest(IProgramElement, AJProjectModelFacade model) {
    
    private static List/*String*/ invalidAdviceRelationsip(IRelationship rel, AJProjectModelFacade model) {
        List/*String*/ problems = new ArrayList();
        if (rel.getKind() == IRelationship.Kind.ADVICE ||
                rel.getKind() == IRelationship.Kind.ADVICE_AFTER ||
                rel.getKind() == IRelationship.Kind.ADVICE_AFTERRETURNING ||
                rel.getKind() == IRelationship.Kind.ADVICE_AFTERTHROWING ||
                rel.getKind() == IRelationship.Kind.ADVICE_BEFORE ||
                rel.getKind() == IRelationship.Kind.ADVICE_AROUND) {
            
            IJavaElement elt = model.programElementToJavaElement(rel.getSourceHandle());
            if (!elt.exists()) {
                problems.add("Java Element does not exist: " + rel.getSourceHandle() + 
                        "\n\tIt is the source relationship of " + toRelString(rel) +
                        "\n\tThis may not actually be a problem if compiling broken code or advising static initializers.");
            }
            if (    elt.getElementType() == IJavaElement.COMPILATION_UNIT || 
                    elt.getElementType() == IJavaElement.CLASS_FILE) {
                problems.add("Java Element is wrong type (advice relationships should not contain any types or compilation units): " + 
                        rel.getSourceHandle() + 
                        "\n\tIt is the source relationship of " + toRelString(rel));
            }
            
            for (Iterator targetIter = rel.getTargets().iterator(); targetIter.hasNext();) {
                String target = (String) targetIter.next();
                elt = model.programElementToJavaElement(target);
                if (!elt.exists()) {
                    problems.add("Java Element does not exist: " + target + 
                            "\n\tIt is the source relationship of " + toRelString(rel) +
                            "\n\tThis may not actually be a problem if compiling broken code or advising static initializers.");
                }
                if (elt != AJProjectModelFacade.ERROR_JAVA_ELEMENT && 
                       (elt.getElementType() == IJavaElement.COMPILATION_UNIT || 
                        elt.getElementType() == IJavaElement.CLASS_FILE)) {
                    problems.add("Java Element is wrong type (advice relationships should not contain any types or compilation units): " + 
                            target + 
                            "\n\tIt is the source relationship of " + toRelString(rel));
                }
            }
                
        }
        return problems;
    }
    private static List/*String*/ itdsNotOnType(IRelationship rel, AJProjectModelFacade model) {
        List/*String*/ problems = new ArrayList();
        if (rel.getKind() == IRelationship.Kind.DECLARE_INTER_TYPE) {
            
            IJavaElement elt = model.programElementToJavaElement(rel.getSourceHandle());
            if (!elt.exists()) {
                problems.add("Java Element does not exist: " + rel.getSourceHandle() + 
                        "\n\tIt is the source relationship of " + toRelString(rel) +
                        "\n\tThis may not actually be a problem if compiling broken code.");
            }
            if (elt != AJProjectModelFacade.ERROR_JAVA_ELEMENT &&
                   (elt.getElementType() == IJavaElement.FIELD || 
                    elt.getElementType() == IJavaElement.METHOD || 
                    elt.getElementType() == IJavaElement.LOCAL_VARIABLE || 
                    elt.getElementType() == IJavaElement.INITIALIZER || 
                    elt.getElementType() == IJavaElement.COMPILATION_UNIT || 
                    elt.getElementType() == IJavaElement.CLASS_FILE) &&
                    !(elt instanceof IntertypeElement || elt instanceof DeclareElement)) {
                problems.add("Java Element is wrong type (ITD relationships should only contain types and intertype elements): " + 
                        rel.getSourceHandle() + 
                        "\n\tIt is the source relationship of " + toRelString(rel));
            }
            
            for (Iterator targetIter = rel.getTargets().iterator(); targetIter.hasNext();) {
                String target = (String) targetIter.next();
                elt = model.programElementToJavaElement(target);
                if (!elt.exists()) {
                    problems.add("Java Element does not exist: " + target + 
                            "\n\tIt is the source relationship of " + toRelString(rel) +
                            "\n\tThis may not actually be a problem if compiling broken code.");
                }
                if (elt != AJProjectModelFacade.ERROR_JAVA_ELEMENT &&
                       (elt.getElementType() == IJavaElement.FIELD || 
                        elt.getElementType() == IJavaElement.METHOD || 
                        elt.getElementType() == IJavaElement.LOCAL_VARIABLE || 
                        elt.getElementType() == IJavaElement.INITIALIZER || 
                        elt.getElementType() == IJavaElement.COMPILATION_UNIT || 
                        elt.getElementType() == IJavaElement.CLASS_FILE) &&
                        !(elt instanceof IntertypeElement || elt instanceof DeclareElement)) {
                    problems.add("Java Element is wrong type (ITD relationships should only contain types and intertype elements): " + 
                            target + 
                            "\n\tIt is the source relationship of " + toRelString(rel));
                }
            }
                
        }
        return problems;
    }

    private static String toRelString(IRelationship rel) {
        StringBuffer sb = new StringBuffer();
        sb.append(rel.getSourceHandle());
        sb.append(" --");
        sb.append(rel.getName());
        sb.append("--> ");
        for (Iterator targetIter = rel.getTargets().iterator(); targetIter.hasNext();) {
            String target = (String) targetIter.next();
            sb.append(target);
            if (targetIter.hasNext()) {
                sb.append(",   ");
            }
        }
        return sb.toString();
    }
}
