// Copyright (C) 2014 Guibing Guo
//
// This file is part of LibRec.
//
// LibRec is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// LibRec 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with LibRec. If not, see <http://www.gnu.org/licenses/>.
//

package net.librec.recommender.baseline;

import com.google.common.collect.BiMap;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.Reader;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.librec.common.LibrecException;
import net.librec.data.DataModel;
import net.librec.recommender.AbstractRecommender;
import net.librec.similarity.TestSimilarity;


public class DistanceAwareRecommender extends AbstractRecommender {

    
    private Map<Long, Double[]> hmUserPos;
    private Map<Long, Double[]> hmItemPos;
    private DataModel dataModel;
    
    private static final double maxDistance = 25.00;  //Tuning factor
    
    /**
     * Setup needs to load contextual data before starting the predictions.
     * @throws LibrecException 
     */
    @Override
    protected void setup() throws LibrecException {
        super.setup();
        dataModel = context.getDataModel();
        openFileUser();
        openFileItem();
    }


    @Override
    protected void trainModel() throws LibrecException {

    }

    /**
     * This function predicts the value of a event for a user based solely on distance rating. 
     * Calculates the mean distance of all the previous events attended by the user and calculates
     * the rating based of the difference of the distance event-user to the mean distance.
     * @param userIdx user index
     * @param itemIdx item index
     * @return predictive rating for user userIdx on item itemIdx
     * @throws LibrecException if error occurs during predicting
     */
    @Override
    protected double predict(int userIdx, int itemIdx) throws LibrecException {
        double d,dTotal = 0.0;
        int count = 0;
        Long otherItemIndexMeetup;
        double distanceRating;
        //dataModel.getUserMappingData is a BiMap that holds their ID of each item or user and the ID of the datafile passed.
        // Because we load contextual data by the side using same ID as the datafile, we need to un-translate the ID.
        BiMap<Integer,String> itemMapping = dataModel.getItemMappingData().inverse();
        Long userIndexMeetup = Long.parseLong(dataModel.getUserMappingData().inverse().get(userIdx));
        Long itemIndexMeetup = Long.parseLong(itemMapping.get(itemIdx));
        
        Double[] posUser = hmUserPos.get(userIndexMeetup);
        Double[] posItem = hmItemPos.get(itemIndexMeetup);
        
        if(posUser == null || posItem == null || (posUser[0] == 0.0 && posUser[1] == 0.0) || (posItem[0] == 0.0 && posItem[1] == 0.0)){
            return globalMean; //If missing data or position is  globalmean (data is present but 0.0 is wrong data returned by meetup API), we can't know distance.
        }
        
        List<Integer> itemSet = trainMatrix.getColumns(userIdx); 
        //Important! we use trainMatrix to get all the relations of a user with other items. Do not use data loaded by the side to do this because you could break the train-test split
        
        // Algorithm: calculate mean event-user distance of the events attended by the user. Rating is calculated by how close is to the mean.
        //  This should work with other statistical measures too.
        for(Integer item: itemSet){
            otherItemIndexMeetup = Long.parseLong(itemMapping.get(item));
            Double[] posOtherItem = hmItemPos.get(otherItemIndexMeetup);
            
            if(posOtherItem == null || (posOtherItem[0] == 0.0 && posOtherItem[1] == 0.0)){
               continue;  
            }
        
            d = distance(posUser[0],posOtherItem[0],posUser[1],posOtherItem[1],0.0,0.0);
            dTotal += d;
            count++;
        }
        
        d = distance(posUser[0],posItem[0],posUser[1],posItem[1],0.0,0.0);
        if(count == 0){
            distanceRating = d; //distance user-item
        }else{
            distanceRating = Math.abs((dTotal/count) - d); //Average distance - distance user-item    
        }
        distanceRating = 1.0/Math.sqrt(1.0+distanceRating);  // Transformation from [0,+inf] to [-1,1]. Sqrt to make the function smoother
        distanceRating *= 2;
        distanceRating -= 1; 
        return distanceRating;
    }
    

    /**
     * Calculate distance between two points in latitude and longitude taking
     * into account height difference. If you are not interested in height
     * difference pass 0.0. Uses Haversine method as its base.
     * 
     * lat1, lon1 Start point lat2, lon2 End point el1 Start altitude in meters
     * el2 End altitude in meters
     * @returns Distance in Kilometers
     */
    public static double distance(double lat1, double lat2, double lon1,
            double lon2, double el1, double el2) {

        final int R = 6371; // Radius of the earth

        double latDistance = Math.toRadians(lat2 - lat1);
        double lonDistance = Math.toRadians(lon2 - lon1);
        double a = Math.sin(latDistance / 2) * Math.sin(latDistance / 2)
                + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
                * Math.sin(lonDistance / 2) * Math.sin(lonDistance / 2);
        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        double distance = R * c * 1000; // convert to meters

        double height = el1 - el2;

        distance = Math.pow(distance, 2) + Math.pow(height, 2);

        return Math.sqrt(distance)/1000.0d;
    }
 
    public void openFileUser(){
        hmUserPos = new HashMap<Long, Double[]>();
        String[] splitLine;
        Long user;
        double lat,lon;
        try {
            
            Reader targetReader = new FileReader(conf.get("dfs.data.dir")+"\\MembersLatLonChicago.txt");
            BufferedReader br = new BufferedReader(targetReader);
            String line = br.readLine();
            while (line != null && !line.isEmpty()) {
                splitLine = line.split(",");
                user = Long.parseLong(splitLine[1]);
                if(!hmUserPos.containsKey(user)){
                    lat = Double.parseDouble(splitLine[2]);
                    lon = Double.parseDouble(splitLine[3]);
                    hmUserPos.put(user, new Double[] {lat,lon});
                }
                line = br.readLine();
            }
        } catch (Exception ex) {
            Logger.getLogger(TestSimilarity.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
    
    public void openFileItem(){
        hmItemPos = new LinkedHashMap<Long, Double[]>();
        String[] splitLine;
        Long item;
        double lat,lon;
        String line;
        try {
            
            BufferedReader br = new BufferedReader(new FileReader(conf.get("dfs.data.dir")+"\\ItemsLatLonChicago.txt"));
            line = br.readLine();
            while (line != null && !line.isEmpty()) {
                splitLine = line.split(",");
                item = Long.parseLong(splitLine[1]);
                if(!hmItemPos.containsKey(item)){
                    lat = Double.parseDouble(splitLine[2]);
                    lon = Double.parseDouble(splitLine[3]);
                    hmItemPos.put(item, new Double[] {lat,lon});
                }
                line = br.readLine();
            }
        } catch (Exception ex) {
            Logger.getLogger(TestSimilarity.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
    
    /**
     * Function that reads a file containing user or item position information in the format id,lat,lon. 
     * @param collectionPos
     * @param filename 
     */
    public void openFile(Map<Long, Double[]> collectionPos, String filename){
        collectionPos = new LinkedHashMap<Long, Double[]>();
        String[] splitLine;
        Long id;
        double lat,lon;
        String line;
        try {
            BufferedReader br = new BufferedReader(new FileReader(conf.get("dfs.data.dir")+ "\\" + filename));
            line = br.readLine();
            while (line != null && !line.isEmpty()) {
                splitLine = line.split(",");
                id = Long.parseLong(splitLine[1]);
                if(!collectionPos.containsKey(id)){
                    lat = Double.parseDouble(splitLine[2]);
                    lon = Double.parseDouble(splitLine[3]);
                    collectionPos.put(id, new Double[] {lat,lon});
                }
                line = br.readLine();
            }
        } catch (Exception ex) {
            Logger.getLogger(TestSimilarity.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
    

    
    
 
    
    
    
}
