/*
 TagRecommender:
 A framework to implement and evaluate algorithms for the recommendation
 of tags.
 Copyright (C) 2013 Dominik Kowald, Emanuel Lacic
 
 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU Affero General Public License as
 published by the Free Software Foundation, either version 3 of the
 License, or (at your option) any later version.
 
 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU Affero General Public License for more details.
 
 You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package itemrecommendations;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import processing.BLLCalculator;
import common.Bookmark;
import common.Utilities;
import file.BookmarkReader;

/**
 * Tag-weighted, time-weighted and combined strategy used by Zheng & Li
 * @author elacic
 *
 */
public class HuangApproach {
	
	public static final double WEIGHT = 0.5;
	public static final double ALPHA = 1.0;
	
	Map<Integer, Map<Integer, List<Integer>>> userResourceTagMaping;
	Map<Integer, Map<Integer, List<Integer>>> resourceUserTagMaping;

	
	Map<Integer, Long> userFirstMapping;
	Map<Integer, Long> userLastMapping;

	Map<Integer, Map<Integer, Long>> userTagFirstMapping;
	Map<Integer, Map<Integer, Long>> userTagLastMapping;
	
	
	public Map<Integer, Map<Integer, List<Integer>>> getUserResourceTagMaping() {
		return userResourceTagMaping;
	}

	public Map<Integer, Map<Integer, List<Integer>>> getResourceUserTagMaping() {
		return resourceUserTagMaping;
	}

	public HuangApproach(List<Bookmark> trainList) {
		userResourceTagMaping = new HashMap<Integer, Map<Integer,List<Integer>>>();
		resourceUserTagMaping = new HashMap<Integer, Map<Integer,List<Integer>>>();

		userFirstMapping = new HashMap<Integer, Long>();
		userLastMapping = new HashMap<Integer, Long>();

		userTagFirstMapping = new HashMap<Integer, Map<Integer, Long>>();
		userTagLastMapping = new HashMap<Integer, Map<Integer, Long>>();

		
		for (Bookmark trainData : trainList) {
			Integer userID = trainData.getUserID();
			Integer resID = trainData.getResourceID();
			List<Integer> tags = trainData.getTags();
			Long day = TimeUnit.SECONDS.toDays(Long.parseLong(trainData.getTimestamp()));
			
			Map<Integer, List<Integer>> resourceTagMaping = userResourceTagMaping.get(userID);
			Map<Integer, List<Integer>> userTagMaping = resourceUserTagMaping.get(resID);

			
			if (resourceTagMaping == null) {
				resourceTagMaping = new HashMap<Integer, List<Integer>>();
			}
			if (userTagMaping == null) {
				userTagMaping = new HashMap<Integer, List<Integer>>();
			}
			
			fillUserFirst(userID, day);
			fillUserLast(userID, day);

			fillTagFirst(userID, tags, day);
			fillTagLast(userID, tags, day);
			
			resourceTagMaping.put(resID, tags);
			userTagMaping.put(userID, tags);
			
			userResourceTagMaping.put(userID, resourceTagMaping);
			resourceUserTagMaping.put(resID, userTagMaping);
		}
	}

	private void fillUserFirst(Integer userID, Long day) {
		if (! userFirstMapping.containsKey(userID) || day < userFirstMapping.get(userID)) {
			userFirstMapping.put(userID, day);
		}
	}
	
	private void fillUserLast(Integer userID, Long day) {
		if (! userLastMapping.containsKey(userID) || day > userLastMapping.get(userID)) {
			userLastMapping.put(userID, day);
		}
	}

	private void fillTagFirst(Integer userID, List<Integer> tags, Long day) {
		Map<Integer, Long> tagMap = userTagFirstMapping.get(userID);
		
		if (tagMap == null){
			tagMap = new HashMap<Integer, Long>();
		}
		
		for (Integer tag : tags) {
			if( ! tagMap.containsKey(tag) || day < tagMap.get(tag)) {
				tagMap.put(tag, day);
			}
		}
		
		userTagFirstMapping.put(userID, tagMap);
	}
	
	private void fillTagLast(Integer userID, List<Integer> tags, Long day) {
		Map<Integer, Long> tagMap = userTagLastMapping.get(userID);
		
		if (tagMap == null){
			tagMap = new HashMap<Integer, Long>();
		}
		
		for (Integer tag : tags) {
			if( ! tagMap.containsKey(tag) || day > tagMap.get(tag)) {
				tagMap.put(tag, day);
			}
		}
		
		userTagLastMapping.put(userID, tagMap);
	}
	

	
	/**
	 * Gets the tag weight for a given user and tag
	 * @param userID id of the user
	 * @param tag
	 * @return TF-IUF model calculated weight
	 */
	public Double getUserTagWeight(Integer userID, Integer tag) {
		return getUserTagTF(userID, tag) * getTagIUF(tag);
	}
	
	/**
	 * Gets the tag weight for a given resource and tag
	 * @param resourceID if of the resource
	 * @param tag
	 * @return TF-IIF model calculated weight
	 */
	public Double getItemTagWeight(Integer resourceID, Integer tag) {
		return getItemTagTF(resourceID, tag) * getTagIIF(tag);
	}
	

	private Double getUserTagTF(Integer userID, Integer tag) {
		return getTagTF(userID, tag, userResourceTagMaping);
	}
	private Double getTagIUF(Integer tag) {
		return getTagIIF(tag, userResourceTagMaping);
	}
	
	
	private Double getItemTagTF(Integer resourceID, Integer tag) {
		return getTagTF(resourceID, tag, resourceUserTagMaping);
	}
	private Double getTagIIF(Integer tag) {
		return getTagIIF(tag, resourceUserTagMaping);
	}
	
	/**
	 * Calculates the tag TF for an entity mapping
	 * @param entityId
	 * @param tag
	 * @param entityMaping
	 * @return
	 */
	private Double getTagTF(Integer entityId, Integer tag, Map<Integer, Map<Integer, List<Integer>>> entityMaping) {
		if (! entityMaping.containsKey(entityId)) {
			return 0.0;
		}
		
		List<Integer> tagsByEntity = getTagsByEntity(entityId, entityMaping);
		
		return Collections.frequency(tagsByEntity, tag) / (double) maximumFreqency(tagsByEntity);
	}

	private List<Integer> getTagsByEntity(Integer entityId, Map<Integer, Map<Integer, List<Integer>>> entityMaping) {
		Map<Integer, List<Integer>> entityTagMaping = entityMaping.get(entityId);
		
		List<Integer> tagsByEntity = new ArrayList<Integer>();
		
		for (List<Integer> tagsAtEntity : entityTagMaping.values()) {
			tagsByEntity.addAll(tagsAtEntity);
		}
		
		return tagsByEntity;
	}
	
	/**
	 * Calculates IIF for a tag in a entity mapping
	 * @param tag
	 * @param entityMaping
	 * @return
	 */
	private Double getTagIIF(Integer tag, Map<Integer, Map<Integer, List<Integer>>> entityMaping) {
		Set<Integer> allResources = entityMaping.keySet();
		
		Integer totalNumberOfResources = allResources.size();
		Integer numberOfTaggedResources = 0;
		
		for (Integer resID : allResources) {
			for (List<Integer> tags : entityMaping.get(resID).values()) {
				if (tags.contains(tag)) {
					numberOfTaggedResources++;
					break;
				}
			}
		}
		
		return Math.log(totalNumberOfResources / (double) numberOfTaggedResources);
	}
	
	
	/**
	 * Maximum frequency of items in the list
	 * @param list
	 * @return
	 */
	private static <T> Integer maximumFreqency(List<T> list) {
	    Map<T, Integer> map = new HashMap<>();

	    for (T t : list) {
	        Integer val = map.get(t);
	        map.put(t, val == null ? 1 : val + 1);
	    }

	    Entry<T, Integer> max = null;

	    for (Entry<T, Integer> e : map.entrySet()) {
	        if (max == null || e.getValue() > max.getValue())
	            max = e;
	    }

	    return max.getValue();
	}
	
	private double getScaledRecency(Integer user, Integer tag) {
		long current = TimeUnit.SECONDS.toDays(System.currentTimeMillis());
		
		long tagLast = 0; 
		if (userTagLastMapping.containsKey(user) && userTagLastMapping.get(user).containsKey(tag)) {
			tagLast = userTagLastMapping.get(user).get(tag);
		}
		
		long userFirst = userFirstMapping.get(user);
		
		return  1 - ((current - tagLast) / (double) (current - userFirst));
	}
	
	private double getScaledDuration(Integer user, Integer tag) {
		long tagFirst = 0; 
		if (userTagFirstMapping.containsKey(user) && userTagFirstMapping.get(user).containsKey(tag)) {
			tagFirst = userTagFirstMapping.get(user).get(tag);
		}
		
		long tagLast = 0; 
		if (userTagLastMapping.containsKey(user) && userTagLastMapping.get(user).containsKey(tag)) {
			tagLast = userTagLastMapping.get(user).get(tag);
		}
		
		long userFirst = userFirstMapping.get(user);
		
		long userLast = userLastMapping.get(user);
		
		
		return (tagLast - tagFirst) / (double) (userLast - userFirst);
	}
	
	private double getTagFRD(Integer user, Integer tag, List<Integer> tagsByEntity) {
		return Collections.frequency(tagsByEntity, tag) * 
				( ALPHA + WEIGHT * getScaledRecency(user, tag) + (1 - WEIGHT) * getScaledDuration(user, tag) );
	}
	
	public double getScaledTagFRD(Integer user, Integer tag) {
		
		List<Integer> tagsByEntity = getTagsByEntity(user, userResourceTagMaping);
		Set<Integer> uniqueTagsByEntity = new HashSet<Integer>(tagsByEntity);
		
		double maxTagFRD = Integer.MIN_VALUE;
		for (Integer userTag : uniqueTagsByEntity) {
			double uniqueTagFRD = getTagFRD(user, userTag, tagsByEntity);
			maxTagFRD = (uniqueTagFRD > maxTagFRD) ? uniqueTagFRD : maxTagFRD;
		}
		
		return getTagFRD(user, tag, tagsByEntity) / maxTagFRD;
	}
	
	
}