// @@author A0130195M

package jfdi.logic.commands;

import jfdi.logic.events.SearchDoneEvent;
import jfdi.logic.interfaces.Command;
import jfdi.storage.apis.TaskAttributes;
import opennlp.tools.stemmer.PorterStemmer;
import org.apache.commons.lang3.tuple.ImmutableTriple;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.stream.Collectors;

/**
 * @author Liu Xinan
 */
public class SearchCommand extends Command {

    private static final PorterStemmer stemmer = new PorterStemmer();

    private HashSet<String> keywords;

    private ArrayList<TaskAttributes> results;

    private SearchCommand(Builder builder) {
        this.keywords = builder.keywords;
    }

    public HashSet<String> getKeywords() {
        return keywords;
    }

    public ArrayList<TaskAttributes> getResults() {
        return results;
    }

    public static class Builder {

        HashSet<String> keywords = new HashSet<>();

        public Builder addKeyword(String keyword) {
            this.keywords.add(keyword);
            return this;
        }

        public Builder addKeywords(Collection<String> keywords) {
            this.keywords.addAll(keywords);
            return this;
        }

        public SearchCommand build() {
            return new SearchCommand(this);
        }

    }

    @Override
    public void execute() {
        results = taskDb.getAll().stream()
                .map(this::constructCandidate)
                .filter(this::isValidCandidate)
                .sorted(this::candidateCompare)
                .map(ImmutableTriple::getRight)
                .collect(Collectors.toCollection(ArrayList::new));

        logger.info(String.format("Search completed. %d results found.", results.size()));

        eventBus.post(new SearchDoneEvent(results, keywords));
    }

    private ImmutableTriple<Long, Integer, TaskAttributes> constructCandidate(TaskAttributes task) {
        String[] parts = task.getDescription().split("\\s+");
        int wordCount = parts.length;

        long rank = Arrays.stream(parts)
                .map(stemmer::stem)
                .filter(this::isKeyword)
                .count();

        return new ImmutableTriple<Long, Integer, TaskAttributes>(rank, wordCount, task);
    }

    private boolean isValidCandidate(ImmutableTriple<Long, Integer, TaskAttributes> candidate) {
        return candidate.getLeft() > 0;
    }

    private boolean isKeyword(String word) {
        return keywords.stream()
                .map(stemmer::stem)
                .reduce(
                        false,
                        (isMatched, keyword) -> isMatched || word.equalsIgnoreCase(keyword),
                        (isPreviouslyMatched, isNowMatched) -> isPreviouslyMatched || isNowMatched
                );
    }

    private int candidateCompare(ImmutableTriple<Long, Integer, TaskAttributes> left,
                                 ImmutableTriple<Long, Integer, TaskAttributes> right) {

        if (left.getLeft() > right.getLeft()) {
            return -1;
        } else if (left.getLeft() < right.getLeft()) {
            return 1;
        } else {
            return left.getMiddle() - right.getMiddle();
        }
    }

    @Override
    public void undo() {
        assert false;
    }

}

@@ -1410,22 +1410,52 @@
         
-result
+ArrayList%3CTaskAttributes%3E fullMatche
 s = task
@@ -1507,28 +1507,37 @@
 s::construct
+FullMatch
 Can
-d
+c
 idate)%0A     
@@ -1755,230 +1755,1077 @@
     
-logger.info(String.format(%22Search completed. %25d results found.%22, results.size()));%0A%0A        eventBus.post(new SearchDoneEvent(results, keywords));%0A    %7D%0A%0A    private ImmutableTriple%3CLong, Integer, TaskAttributes%3E construct
+results = new ArrayList%3C%3E(fullMatches);%0A%0A        results.addAll(taskDb.getAll().stream()%0A                .map(this::constructStemMatchCandidate)%0A                .filter(this::isValidCandidate)%0A                .sorted(this::candidateCompare)%0A                .map(ImmutableTriple::getRight)%0A                .filter(task -%3E !fullMatches.contains(task))%0A                .collect(Collectors.toCollection(ArrayList::new)));%0A%0A        logger.info(String.format(%22Search completed. %25d results found.%22, results.size()));%0A%0A        eventBus.post(new SearchDoneEvent(results, keywords));%0A    %7D%0A%0A    private ImmutableTriple%3CLong, Integer, TaskAttributes%3E constructFullMatchCancidate(TaskAttributes task) %7B%0A        String%5B%5D parts = task.getDescription().split(%22%5C%5Cs+%22);%0A        int wordCount = parts.length;%0A%0A        long rank = Arrays.stream(parts)%0A            .filter(this::isFullMatch)%0A            .count();%0A%0A        return new ImmutableTriple%3CLong, Integer, TaskAttributes%3E(rank, wordCount, task);%0A    %7D%0A%0A    private ImmutableTriple%3CLong, Integer, TaskAttributes%3E constructStemMatch
 Cand
@@ -3063,23 +3063,25 @@
 this::is
-Keyword
+StemMatch
 )%0A      
@@ -3364,23 +3364,367 @@
 olean is
-Keyword
+FullMatch(String word) %7B%0A        return keywords.stream()%0A                .reduce(%0A                    false,%0A                    (isMatched, keyword) -%3E isMatched %7C%7C word.equalsIgnoreCase(keyword),%0A                    (isPreviouslyMatched, isNowMatched) -%3E isPreviouslyMatched %7C%7C isNowMatched%0A                );%0A    %7D%0A%0A    private boolean isStemMatch
 (String 

Improve search to rank full matches first