/*
 * Copyright 2005, Sixth and Red River Software
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package com.sixrr.stockmetrics.classCalculators;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.jetbrains.annotations.NonNls;
import com.intellij.psi.JavaRecursiveElementVisitor;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiReferenceExpression;

public class LackOfCohesionOfMethodsClassCalculator extends ClassCalculator
{
	private static final
	@NonNls
	Set<String> boilerplateMethods = new HashSet<String>();

	static
	{
		//noinspection HardCodedStringLiteral
		Collections.addAll(boilerplateMethods, "toString", "equals", "hashCode", "finalize", "clone", "readObject", "writeObject");
	}

	@Override
	protected PsiElementVisitor createVisitor()
	{
		return new Visitor();
	}

	private class Visitor extends JavaRecursiveElementVisitor
	{

		@Override
		public void visitClass(PsiClass aClass)
		{
			super.visitClass(aClass);
			if(isConcreteClass(aClass))
			{
				final PsiMethod[] methods = aClass.getMethods();
				final Set<PsiMethod> applicableMethods = new HashSet<PsiMethod>();
				for(PsiMethod method : methods)
				{
					final String methodName = method.getName();
					if(!method.isConstructor() && !boilerplateMethods.contains(methodName))
					{
						applicableMethods.add(method);
					}
				}
				final Map<PsiMethod, Set<PsiField>> fieldsPerMethod = calculateFieldUsage(applicableMethods);
				final Map<PsiMethod, Set<PsiMethod>> linkedMethods = calculateMethodLinkage(applicableMethods);
				final Set<Set<PsiMethod>> components = calculateComponents(applicableMethods, fieldsPerMethod, linkedMethods);
				final int numComponents = components.size();
				postMetric(aClass, numComponents);
			}
		}
	}

	private static Set<Set<PsiMethod>> calculateComponents(
			Set<PsiMethod> applicableMethods, Map<PsiMethod, Set<PsiField>> fieldsPerMethod, Map<PsiMethod, Set<PsiMethod>> linkedMethods)
	{
		final Set<Set<PsiMethod>> components = new HashSet<Set<PsiMethod>>();
		while(applicableMethods.size() > 0)
		{
			final Set<PsiMethod> component = new HashSet<PsiMethod>();
			final Set<PsiField> fieldsUsed = new HashSet<PsiField>();
			final PsiMethod testMethod = applicableMethods.iterator().next();
			applicableMethods.remove(testMethod);
			component.add(testMethod);
			fieldsUsed.addAll(fieldsPerMethod.get(testMethod));
			while(true)
			{
				final Set<PsiMethod> methodsToAdd = new HashSet<PsiMethod>();
				for(PsiMethod method : applicableMethods)
				{
					if(overlaps(fieldsPerMethod.get(method), fieldsUsed) || overlaps(linkedMethods.get(method), component))
					{
						methodsToAdd.add(method);
						fieldsUsed.addAll(fieldsPerMethod.get(method));
					}
				}
				if(methodsToAdd.size() == 0)
				{
					break;
				}
				applicableMethods.removeAll(methodsToAdd);
				component.addAll(methodsToAdd);
			}
			components.add(component);
		}
		return components;
	}

	private static boolean overlaps(Set<?> set1, Set<?> set2)
	{
		for(Object element : set1)
		{
			if(set2.contains(element))
			{
				return true;
			}
		}
		return false;
	}

	private static Map<PsiMethod, Set<PsiField>> calculateFieldUsage(Set<PsiMethod> applicableMethods)
	{
		final Map<PsiMethod, Set<PsiField>> fieldsPerMethod = new HashMap<PsiMethod, Set<PsiField>>();
		for(PsiMethod method : applicableMethods)
		{
			final Set<PsiField> fields = calculateUsedFields(method);
			fieldsPerMethod.put(method, fields);
		}
		return fieldsPerMethod;
	}

	private static Set<PsiField> calculateUsedFields(PsiMethod method)
	{
		final FieldsUsedVisitor visitor = new FieldsUsedVisitor();
		method.accept(visitor);
		return visitor.getFieldsUsed();
	}

	private static class FieldsUsedVisitor extends JavaRecursiveElementVisitor
	{
		private final Set<PsiField> fieldsUsed = new HashSet<PsiField>();

		FieldsUsedVisitor()
		{
		}

		@Override
		public void visitReferenceExpression(PsiReferenceExpression referenceExpression)
		{
			super.visitReferenceExpression(referenceExpression);
			final PsiElement referent = referenceExpression.resolve();
			if(!(referent instanceof PsiField))
			{
				return;
			}
			final PsiField field = (PsiField) referent;
			fieldsUsed.add(field);
		}

		@SuppressWarnings({"ReturnOfCollectionOrArrayField"})
		public Set<PsiField> getFieldsUsed()
		{
			return fieldsUsed;
		}
	}

	private static Map<PsiMethod, Set<PsiMethod>> calculateMethodLinkage(Set<PsiMethod> applicableMethods)
	{
		final Map<PsiMethod, Set<PsiMethod>> linkages = new HashMap<PsiMethod, Set<PsiMethod>>();
		for(PsiMethod method : applicableMethods)
		{
			final Set<PsiMethod> linkedMethods = calculateLinkedMethods(method, applicableMethods);
			linkages.put(method, linkedMethods);
		}
		//add the transpose, since linkage is undirected
		for(PsiMethod method : applicableMethods)
		{
			final Set<PsiMethod> linkedMethods = linkages.get(method);
			for(PsiMethod linkedMethod : linkedMethods)
			{
				linkages.get(linkedMethod).add(method);
			}
		}
		return linkages;
	}

	private static Set<PsiMethod> calculateLinkedMethods(PsiMethod method, Set<PsiMethod> applicableMethods)
	{
		final MethodsUsedVisitor visitor = new MethodsUsedVisitor(applicableMethods);
		method.accept(visitor);
		return visitor.getMethodsUsed();
	}

	private static class MethodsUsedVisitor extends JavaRecursiveElementVisitor
	{
		private final Set<PsiMethod> applicableMethods;
		private final Set<PsiMethod> methodsUsed = new HashSet<PsiMethod>();

		MethodsUsedVisitor(Set<PsiMethod> applicableMethods)
		{
			this.applicableMethods = applicableMethods;
		}

		@Override
		public void visitMethodCallExpression(PsiMethodCallExpression callExpression)
		{
			super.visitMethodCallExpression(callExpression);
			final PsiMethod testMethod = callExpression.resolveMethod();
			if(applicableMethods.contains(testMethod))
			{
				methodsUsed.add(testMethod);
			}
		}

		@SuppressWarnings({"ReturnOfCollectionOrArrayField"})
		public Set<PsiMethod> getMethodsUsed()
		{
			return methodsUsed;
		}
	}
}
