package cz.muni.disa.adamiss.se.operators;

import cz.muni.disa.adamiss.core.common.enums.ParameterName;
import cz.muni.disa.adamiss.core.objects.AdamissObject;
import cz.muni.disa.adamiss.core.objects.Operator;
import cz.muni.disa.adamiss.core.objects.Transaction;
import cz.muni.disa.adamiss.core.objects.TransactionFile;


import javafx.util.Pair;

import java.util.*;

import static cz.muni.disa.adamiss.se.helper.Helper.*;

/**
 * Implementation of PrefixSpan algorithm for frequent sequence mining  http://hanj.cs.illinois.edu/pdf/span01.pdf
 * @author Lukáš Suchánek, 433654
 *
 */

public class PrefixSpan implements Operator {

    private Map<List<AdamissObject>, Integer> results = new HashMap<>();

    @Override
    public TransactionFile apply(TransactionFile transactionFile, Map<String, Object> params) {

        int minSup = (Integer) params.get(ParameterName.miningThreshold.toString());
        results = this.get1set(transactionFile, minSup);

        Map<List<AdamissObject>, Integer> result = new HashMap<>(results);
        List<Pair<Integer, List<AdamissObject>>> transactions = new ArrayList<>();
        int i = 0;
        for(Transaction transaction: transactionFile.getTransactions()){
            transactions.add(new Pair<>(i, transaction.getData()));
            i++;
        }

        for(List<AdamissObject> pattern: result.keySet()){
            List<Pair<Integer, List<AdamissObject>>> projection = project(pattern, transactions);
            prefixSpan(projection, pattern, minSup);
        }

        return getTransactionFileFromResults(results);
    }

    /**
     * Implementation of PrefixSpan recursion
     * @param projection projection of pattern into sequence database
     * @param pattern pattern projected into database
     * @param minSup mining threshold
     */
    public void prefixSpan(List<Pair<Integer, List<AdamissObject>>> projection, List<AdamissObject> pattern, int minSup){
        Map<List<AdamissObject>, Integer> frequent1item = getNextFrequent1Sequences(projection, minSup);

        if(frequent1item.isEmpty())return;
        for(Map.Entry<List<AdamissObject>, Integer> entry: frequent1item.entrySet()){
            List<AdamissObject> newPattern = new ArrayList<>(pattern);
            newPattern.addAll(entry.getKey());
            results.put(newPattern, entry.getValue());
            List<Pair<Integer, List<AdamissObject>>> p = get1Projection(entry.getKey(), projection);
            prefixSpan(p, newPattern, minSup);
        }

    }

    /**
     * Creating projection
     * @param pattern pattern to project
     * @param transactions database
     * @return created projection
     */
    public List<Pair<Integer, List<AdamissObject>>> get1Projection(List<AdamissObject> pattern, List<Pair<Integer, List<AdamissObject>>> transactions){
        List<Pair<Integer, List<AdamissObject>>> projection = new ArrayList<>();

        for(Pair<Integer, List<AdamissObject>> transaction: transactions){
            int i = Collections.indexOfSubList(transaction.getValue(), pattern);
            if(i == 0){
                Transaction newTransaction = new Transaction(transaction.getValue().subList(1, transaction.getValue().size()));
                if(!newTransaction.getData().isEmpty()){
                    projection.add(new Pair<>(transaction.getKey(), newTransaction.getData()));
                }
            }
        }
        return projection;
    }

    /**
     * Find all frequent items on first place in sequence
     * @param projection projection of database
     * @param minSup mining threshold
     * @return all frequent items
     */
    public Map<List<AdamissObject>, Integer> getNextFrequent1Sequences(List<Pair<Integer, List<AdamissObject>>> projection, int minSup){
        final Map<List<AdamissObject>, Integer> result = new HashMap<>();
        Map<List<AdamissObject>, Set<Integer>> patternOccurrences = new HashMap<>();
        for(Pair<Integer, List<AdamissObject>> pattern: projection ){
            AdamissObject o = pattern.getValue().get(0);
            List<AdamissObject> key = new ArrayList<>();
            key.add(o);
            if(patternOccurrences.containsKey(key)){
                patternOccurrences.get(key).add(pattern.getKey());
            }else {
                Set<Integer> set = new HashSet<>();
                set.add(pattern.getKey());
                patternOccurrences.put(key, set);
            }
        }
        for(Map.Entry<List<AdamissObject>, Set<Integer>> entry: patternOccurrences.entrySet()){
            if(entry.getValue().size()>=minSup){
                result.put(entry.getKey(), entry.getValue().size());
            }
        }

        return result;
    }

    /**
     * create database projection
     * @param pattern pattern to project into database
     * @param transactions database
     * @return projected database
     */
    public List<Pair<Integer, List<AdamissObject>>> project(List<AdamissObject> pattern, List<Pair<Integer, List<AdamissObject>>> transactions){
        List<Pair<Integer, List<AdamissObject>>> projection = new ArrayList<>();

        for(Pair<Integer, List<AdamissObject>> transaction: transactions){
            List<Integer> occurrences = getAllOccurrences(pattern, transaction.getValue());
            for(Integer position: occurrences){
                Transaction newTransaction = new Transaction(transaction.getValue().subList(position+1, transaction.getValue().size()));
                if(!newTransaction.getData().isEmpty()){
                    projection.add(new Pair<>(transaction.getKey(), newTransaction.getData()));
                }
            }

        }
        return projection;
    }


    /**
     * Mine all frequent 1-sequences from sequence database
     * @param database sequence database
     * @param minSup mining threshold
     * @return frequent 1-sequences
     */
    private Map<List<AdamissObject>, Integer> get1set(TransactionFile database, Integer minSup){
        Map<List<AdamissObject>, Integer> result = new HashMap<>();
        Iterator<Transaction> iterator = database.getIterator();
        while (iterator.hasNext()) {
            Transaction transaction = iterator.next();
            List<AdamissObject> objects = transaction.getData();
            List<AdamissObject> added = new ArrayList<>();
            for (AdamissObject o : objects) {
                if(added.contains(o)) continue;
                added.add(o);
                List<AdamissObject> key = new ArrayList<>();
                key.add(o);
                increaseSup(result, key);
            }
        }
        removeInfrequent(result, minSup);
        return result;
    }

    /**
     * find all positions of all occurrences of pattern in sequence
     * @param pattern
     * @param sequence
     * @return list of positions
     */
    private List<Integer> getAllOccurrences(List<AdamissObject> pattern, List<AdamissObject> sequence){
        List<Integer> occurrences = new ArrayList<>();
        for(int i=0; i<sequence.size(); i++){
            if(sequence.get(i).equals(pattern.get(0))){
                occurrences.add(i);
            }
        }
        return occurrences;
    }

    public Map<List<AdamissObject>, Integer> getResults() {
        return results;
    }
}
