/**
* OLAT - Online Learning and Training<br>
* http://www.olat.org
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br>
* University of Zurich, Switzerland.
* <p>
*/ 

package org.olat.course.assessment;

import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.olat.core.id.Identity;
import org.olat.core.id.IdentityEnvironment;
import org.olat.core.logging.Tracing;
import org.olat.core.util.nodes.INode;
import org.olat.core.util.tree.TreeVisitor;
import org.olat.core.util.tree.Visitor;
import org.olat.course.ICourse;
import org.olat.course.nodes.AssessableCourseNode;
import org.olat.course.nodes.CourseNode;
import org.olat.course.nodes.ProjectBrokerCourseNode;
import org.olat.course.nodes.STCourseNode;
import org.olat.course.nodes.ScormCourseNode;
import org.olat.course.nodes.iq.IQEditController;
import org.olat.course.run.scoring.ScoreEvaluation;
import org.olat.course.run.userview.UserCourseEnvironment;
import org.olat.course.run.userview.UserCourseEnvironmentImpl;
import org.olat.course.tree.CourseEditorTreeModel;
import org.olat.course.tree.CourseEditorTreeNode;
import org.olat.modules.ModuleConfiguration;

/**
 * Description:<br>
 * Helper methods for the course assessment system
 * <P>
 * Initial Date: Oct 28, 2004<br>
 * @author gnaegi
 */
public class AssessmentHelper {

	/**
	 * String to symbolize 'not available' or 'not assigned' in assessments
	 * details *
	 */
	public static final String DETAILS_NA_VALUE = "n/a";

	/** Highes score value supported by OLAT * */
	public static final float MAX_SCORE_SUPPORTED = 10000f;
	/** Lowest score value supported by OLAT * */
	public static final float MIN_SCORE_SUPPORTED = -10000f;
	//fxdiff VCRP-4: assessment overview with max score
	private final static DecimalFormat scoreFormat = new DecimalFormat("#0.###", new DecimalFormatSymbols(Locale.ENGLISH));

	/**
	 * Wraps an identity and it's score evaluation / attempts in a wrapper object
	 * for a given course node
	 * 
	 * @param identity
	 * @param localUserCourseEnvironmentCache
	 * @param course the course
	 * @param courseNode an assessable course node or null if no details and
	 *          attempts must be fetched
	 * @return a wrapped identity
	 */
	public static AssessedIdentityWrapper wrapIdentity(Identity identity, Map<Long,UserCourseEnvironment> localUserCourseEnvironmentCache, ICourse course,
			AssessableCourseNode courseNode) {
		// Try to get user course environment from local hash map cache. If not
		// successful
		// create the environment and add it to the map for later performance
		// optimization
		//synchronized (localUserCourseEnvironmentCache) { //o_clusterOK by:ld - no need to synchronized - only local variables
			UserCourseEnvironment uce = localUserCourseEnvironmentCache.get(identity.getKey());
			if (uce == null) {
				uce = createAndInitUserCourseEnvironment(identity, course);
				// add to cache for later usage
				localUserCourseEnvironmentCache.put(identity.getKey(), uce);
				if (Tracing.isDebugEnabled(AssessmentHelper.class)){
					Tracing.logDebug("localUserCourseEnvironmentCache hit failed, adding course environment for user::"
						+ identity.getName(), AssessmentHelper.class);
				}
			}
			return wrapIdentity(uce, courseNode);
		//}
	}

	/**
	 * Wraps an identity and it's score evaluation / attempts in a wrapper object
	 * for a given course node
	 * 
	 * @param uce The users course environment. Must be initialized
	 *          (uce.getScoreAccounting().evaluateAll() must be called previously)
	 * @param courseNode an assessable course node or null if no details and
	 *          attempts must be fetched
	 * @return a wrapped identity
	 */
	public static AssessedIdentityWrapper wrapIdentity(UserCourseEnvironment uce, AssessableCourseNode courseNode) {
		// Fetch attempts and details for this node if available
		Integer attempts = null;
		String details = null;
		if (courseNode != null) {
			if (courseNode.hasAttemptsConfigured()) {
				attempts = courseNode.getUserAttempts(uce);
			}
			if (courseNode.hasDetails()) {
				details = courseNode.getDetailsListView(uce);
				if (details == null) details = DETAILS_NA_VALUE;
			}
		}
		AssessedIdentityWrapper aiw = new AssessedIdentityWrapper(uce, attempts, details);
		return aiw;
	}

	/**
	 * Create a user course environment for the given user and course. After
	 * creation, the users score accounting will be initialized.
	 * 
	 * @param identity
	 * @param course
	 * @return Initialized user course environment
	 */
	static UserCourseEnvironment createAndInitUserCourseEnvironment(Identity identity, ICourse course) {
		// create an identenv with no roles, no attributes, no locale
		IdentityEnvironment ienv = new IdentityEnvironment(); 
		ienv.setIdentity(identity);
		UserCourseEnvironment uce = new UserCourseEnvironmentImpl(ienv, course.getCourseEnvironment());
		// Fetch all score and passed and calculate score accounting for the entire
		// course
		uce.getScoreAccounting().evaluateAll();
		return uce;
	}

	/**
	 * check the given node for assessability.
	 * @param node
	 * @return
	 */
	public static boolean checkIfNodeIsAssessable(CourseNode node) {
		if (node instanceof AssessableCourseNode) {
			if (node instanceof STCourseNode) {
				STCourseNode scn = (STCourseNode) node;
				if (scn.hasPassedConfigured() || scn.hasScoreConfigured()) { return true; }
			} else if (node instanceof ScormCourseNode) {
				ScormCourseNode scormn = (ScormCourseNode) node;
				if (scormn.hasScoreConfigured()) { return true; }
			} else if (node instanceof ProjectBrokerCourseNode) {
				return false;// TODO:cg 28.01.2010 ProjectBroker : no assessment-tool in V1.0 return always false
			} else {
				return true;
			}
		}
		return false;
	}

	/**
	 * Checks recursivley a course structure or a part of it for assessable nodes
	 * or for structure course nodes (subtype of assessable node), which
	 * 'hasPassedConfigured' or 'hasScoreConfigured' is true. If founds the first
	 * node that meets the criterias, it returns true.
	 * 
	 * @param node
	 * @return boolean
	 */
	public static boolean checkForAssessableNodes(CourseNode node) {
		if(checkIfNodeIsAssessable(node)) {
			return true;
		}
		// check children now
		int count = node.getChildCount();
		for (int i = 0; i < count; i++) {
			CourseNode cn = (CourseNode) node.getChildAt(i);
			if (checkForAssessableNodes(cn)) return true;
		}
		return false;
	}

	/**
	 * Get all assessable nodes including the root node (if assessable)
	 * 
	 * @param editorModel
	 * @param excludeNode Node that should be excluded in the list, e.g. the
	 *          current node or null if all assessable nodes should be used
	 * @return List of assessable course nodes
	 */
	public static List<CourseNode> getAssessableNodes(final CourseEditorTreeModel editorModel, final CourseNode excludeNode) {
		CourseEditorTreeNode rootNode = (CourseEditorTreeNode) editorModel.getRootNode();
		final List<CourseNode> nodes = new ArrayList<CourseNode>();
		// visitor class: takes all assessable nodes if not the exclude node and
		// puts
		// them into the nodes list
		Visitor visitor = new Visitor() {
			public void visit(INode node) {
				CourseEditorTreeNode editorNode = (CourseEditorTreeNode) node;
				CourseNode courseNode = editorModel.getCourseNode(node.getIdent());
				if (!editorNode.isDeleted() && (courseNode != excludeNode)) {
					if(checkIfNodeIsAssessable(courseNode)) {
						nodes.add(courseNode);
					}
				}
			}
		};
		// not visit beginning at the root node
		TreeVisitor tv = new TreeVisitor(visitor, rootNode, false);
		tv.visitAll();

		return nodes;
	}

	/**
	 * @param score The score to be rounded
	 * @return The rounded score for GUI presentation
	 */
	//fxdiff VCRP-4: assessment overview with max score
	public static String getRoundedScore(Float score) {
		if (score == null) return null;

		//cluster_OK the formatter is not multi-thread and costly to create
		synchronized(scoreFormat) {
			return scoreFormat.format(score);
		}
		//return Formatter.roundToString(score.floatValue(), 3);
	}

	public static final String KEY_TYPE = "type";
	public static final String KEY_IDENTIFYER = "identifyer";
	public static final String KEY_INDENT = "indent";

	public static final String KEY_TITLE_SHORT = "short.title";
	public static final String KEY_TITLE_LONG = "long.title";
	public static final String KEY_PASSED = "passed";
	public static final String KEY_SCORE = "score";
	public static final String KEY_SCORE_F = "fscore";
	public static final String KEY_ATTEMPTS = "attempts";
	public static final String KEY_DETAILS = "details";
	public static final String KEY_SELECTABLE = "selectable";
	//fxdiff VCRP-4: assessment overview with max score
	public static final String KEY_MIN = "minScore";
	public static final String KEY_MAX = "maxScore";
	

	
	/**
	 * Add all assessable nodes and the scoring data to a list. Each item in the list is an object array
	 * that has the following data:
	 * @param recursionLevel
	 * @param courseNode
	 * @param userCourseEnv
	 * @param discardEmptyNodes
	 * @param discardComments
	 * @return list of object arrays or null if empty
	 */
	static List<Map<String,Object>> addAssessableNodeAndDataToList(int recursionLevel, CourseNode courseNode, UserCourseEnvironment userCourseEnv, boolean discardEmptyNodes, boolean discardComments) {
		// 1) Get list of children data using recursion of this method
		List<Map<String, Object>> childrenData = new ArrayList<Map<String, Object>>(50);
		for (int i = 0; i < courseNode.getChildCount(); i++) {
			CourseNode child = (CourseNode) courseNode.getChildAt(i);
			List<Map<String, Object>> childData = addAssessableNodeAndDataToList( (recursionLevel + 1),  child, userCourseEnv, discardEmptyNodes, discardComments);
			if (childData != null)
				childrenData.addAll(childData);
		}
		
		// 2) Get data of this node only if
		// - it has any wrapped children  or
		// - it is of an assessable course node type
		boolean hasDisplayableValuesConfigured = false;
		boolean hasDisplayableUserValues = false;
		if ( (childrenData.size() > 0 || courseNode instanceof AssessableCourseNode) && !(courseNode instanceof ProjectBrokerCourseNode) ) {
		  // TODO:cg 04.11.2010 ProjectBroker : no assessment-tool in V1.0 , remove projectbroker completely form assessment-tool gui
			// Store node and user data in object array. This object array serves as data model for 
			// the user assessment overview table
			Map<String,Object> nodeData = new HashMap<String, Object>();
			// indent
			nodeData.put(KEY_INDENT, new Integer(recursionLevel));
			// course node data
			nodeData.put(KEY_TYPE, courseNode.getType());
			nodeData.put(KEY_TITLE_SHORT, courseNode.getShortTitle());
			nodeData.put(KEY_TITLE_LONG, courseNode.getLongTitle());
			nodeData.put(KEY_IDENTIFYER, courseNode.getIdent());
			
			if (courseNode instanceof AssessableCourseNode) {
				AssessableCourseNode assessableCourseNode = (AssessableCourseNode) courseNode;
				ScoreEvaluation scoreEvaluation = userCourseEnv.getScoreAccounting().getScoreEvaluation(courseNode);
				// details 
				if (assessableCourseNode.hasDetails()) {
					hasDisplayableValuesConfigured = true;
					String detailValue = assessableCourseNode.getDetailsListView(userCourseEnv);
					if (detailValue == null) {
						// ignore unset details in discardEmptyNodes mode
						nodeData.put(KEY_DETAILS, AssessmentHelper.DETAILS_NA_VALUE);
					} else {
						nodeData.put(KEY_DETAILS, detailValue);
						hasDisplayableUserValues = true;
					}
				}
				// attempts
				if (assessableCourseNode.hasAttemptsConfigured()) {
					hasDisplayableValuesConfigured = true;
					Integer attemptsValue = assessableCourseNode.getUserAttempts(userCourseEnv); 
					if (attemptsValue != null) {
						nodeData.put(KEY_ATTEMPTS, attemptsValue);
						if (attemptsValue.intValue() > 0) {
								// ignore attempts = 0  in discardEmptyNodes mode
								hasDisplayableUserValues = true;
						}
					}
				}
				// score
				if (assessableCourseNode.hasScoreConfigured()) {
					hasDisplayableValuesConfigured = true;
					Float score = scoreEvaluation.getScore();
					if (score != null) {
						//fxdiff VCRP-4: assessment overview with max score
						nodeData.put(KEY_SCORE, AssessmentHelper.getRoundedScore(score));
						nodeData.put(KEY_SCORE_F, score);
						hasDisplayableUserValues = true;
					}
					//fxdiff VCRP-4: assessment overview with max score
					if(!(assessableCourseNode instanceof STCourseNode)) {
						Float maxScore = assessableCourseNode.getMaxScoreConfiguration();
						nodeData.put(KEY_MAX, maxScore);
						Float minScore = assessableCourseNode.getMinScoreConfiguration();
						nodeData.put(KEY_MIN, minScore);
					}
					
				}
				// passed
				if (assessableCourseNode.hasPassedConfigured()) {
					hasDisplayableValuesConfigured = true;
					Boolean passed = scoreEvaluation.getPassed();
					if (passed != null) {
						nodeData.put(KEY_PASSED, passed);
						hasDisplayableUserValues = true;
					}
				}
				// selection command available
				AssessableCourseNode acn = (AssessableCourseNode) courseNode;
				if (acn.isEditableConfigured()) {
					// Assessable course nodes are selectable
					nodeData.put(KEY_SELECTABLE, Boolean.TRUE);
				} else {
					// assessable nodes that do not have score or passed are not selectable
					// (e.g. a st node with no defined rule
					nodeData.put(KEY_SELECTABLE, Boolean.FALSE);
				}
				if (!hasDisplayableUserValues && assessableCourseNode.hasCommentConfigured() && !discardComments) {
				  // comments are invisible in the table but if configured the node must be in the list
					// for the efficiency statement this can be ignored, this is the case when discardComments is true
					hasDisplayableValuesConfigured = true;
					if (assessableCourseNode.getUserUserComment(userCourseEnv) != null) {
						hasDisplayableUserValues = true;
					}
				}
			} else {
				// Not assessable nodes are not selectable. (e.g. a node that 
				// has an assessable child node but is itself not assessable)
				nodeData.put(KEY_SELECTABLE, Boolean.FALSE);
			}
			// 3) Add data of this node to mast list if node assessable or children list has any data.
			// Do only add nodes when they have any assessable element, otherwhise discard (e.g. empty course, 
			// structure nodes without scoring rules)! When the discardEmptyNodes flag is set then only
			// add this node when there is user data found for this node.
			if (childrenData.size() > 0 
					|| (discardEmptyNodes && hasDisplayableValuesConfigured && hasDisplayableUserValues)
					|| (!discardEmptyNodes && hasDisplayableValuesConfigured)) {
				List<Map<String, Object>> nodeAndChildren = new ArrayList<Map<String, Object>>();
				nodeAndChildren.add(nodeData);
				// 4) Add children data list to master list
				nodeAndChildren.addAll(childrenData);
				return nodeAndChildren;
			}
		}
		return null;
	}
	
	/**
	 * Evaluates if the results are visble or not in respect of the configured CONFIG_KEY_DATE_DEPENDENT_RESULTS parameter. <br>
	 * The results are always visible if no date dependent, 
	 * or if date dependent only in the period: startDate-endDate. 
	 * EndDate could be null, that is there is no restriction for the end date.
	 * 
	 * @return true if is visible.
	 */
	public static boolean isResultVisible(ModuleConfiguration modConfig) {
		boolean isVisible = false;
		Boolean showResultsActive = (Boolean)modConfig.get(IQEditController.CONFIG_KEY_DATE_DEPENDENT_RESULTS);
		if(showResultsActive!=null && showResultsActive.booleanValue()) {
		  Date startDate = (Date)modConfig.get(IQEditController.CONFIG_KEY_RESULTS_START_DATE);
		  Date endDate = (Date)modConfig.get(IQEditController.CONFIG_KEY_RESULTS_END_DATE);
		  Date currentDate = new Date();
		  if(currentDate.after(startDate) && (endDate==null || currentDate.before(endDate))) {
		  	isVisible = true;
		  }
		} else {
			isVisible = true;
		}
		return isVisible;
	}
	
}