package de.ids_mannheim.korap.query.spans;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.lucene.index.AtomicReaderContext;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.TermContext;
import org.apache.lucene.search.spans.Spans;
import org.apache.lucene.util.Bits;

import de.ids_mannheim.korap.query.SpanDistanceQuery;

/**
 * Enumeration of span matches, whose two child spans have a specific range of
 * distance (within a min and a max distance) and can be in any order. The unit
 * distance is a token position.
 * 
 * @author margaretha
 * */
public class UnorderedTokenDistanceSpans extends UnorderedDistanceSpans {

    /**
     * Constructs UnorderedTokenDistanceSpans for the given SpanDistanceQuery.
     * 
     * @param query a SpanDistanceQuery
     * @param context
     * @param acceptDocs
     * @param termContexts
     * @throws IOException
     */
    public UnorderedTokenDistanceSpans(SpanDistanceQuery query,
            AtomicReaderContext context, Bits acceptDocs,
            Map<Term, TermContext> termContexts) throws IOException {
        super(query, context, acceptDocs, termContexts);
    }

    @Override
    protected boolean prepareLists() throws IOException {

        if (firstSpanList.isEmpty() && secondSpanList.isEmpty()) {
            if (hasMoreFirstSpans && hasMoreSecondSpans
                    && ensureSameDoc(firstSpans, secondSpans)) {
                firstSpanList.add(new CandidateSpan(firstSpans));
                secondSpanList.add(new CandidateSpan(secondSpans));
                currentDocNum = firstSpans.doc();
                hasMoreFirstSpans = firstSpans.next();
                hasMoreSecondSpans = secondSpans.next();
            } else {
                hasMoreSpans = false;
                return false;
            }
        } else if (firstSpanList.isEmpty() && hasMoreFirstSpans
                && firstSpans.doc() == currentDocNum) {
            firstSpanList.add(new CandidateSpan(firstSpans));
            hasMoreFirstSpans = firstSpans.next();
        } else if (secondSpanList.isEmpty() && hasMoreSecondSpans
                && secondSpans.doc() == currentDocNum) {
            secondSpanList.add(new CandidateSpan(secondSpans));
            hasMoreSecondSpans = secondSpans.next();
        }
        return true;
    }

    @Override
    protected boolean setCandidateList(List<CandidateSpan> candidateList,
            Spans candidate, boolean hasMoreCandidates,
            List<CandidateSpan> targetList) throws IOException {

        if (!targetList.isEmpty()) {
            CandidateSpan target = targetList.get(0);
            while (hasMoreCandidates && candidate.doc() == target.getDoc()
                    && isWithinMaxDistance(target, candidate)) {
                candidateList.add(new CandidateSpan(candidate));
                hasMoreCandidates = candidate.next();
            }
        }
        return hasMoreCandidates;
    }

    /**
     * Tells if the target and candidate spans are not too far from each other
     * (within the maximum distance).
     * 
     * @param target a target span
     * @param candidate a candidate span
     * @return <code>true</code> if the target and candidate spans are within
     *         the maximum distance, <code>false</code> otherwise.
     */
    protected boolean isWithinMaxDistance(CandidateSpan target, Spans candidate) {
        // left candidate
        if (candidate.end() < target.getStart()
                && candidate.end() + maxDistance <= target.getStart()) {
            return false;
        }
        // right candidate
        if (candidate.start() > target.getEnd()
                && target.getEnd() + maxDistance <= candidate.start()) {
            return false;
        }
        return true;
    }

    @Override
    protected List<CandidateSpan> findMatches(CandidateSpan target,
            List<CandidateSpan> candidateList) {

        List<CandidateSpan> matches = new ArrayList<>();
        int actualDistance;
        for (CandidateSpan cs : candidateList) {
            if (minDistance == 0
                    &&
                    // intersection
                    target.getStart() < cs.getEnd()
                    && cs.getStart() < target.getEnd()) {
                matches.add(createMatchCandidate(target, cs, true));
                continue;
            }

            // left candidate
            if (cs.getEnd() < target.getStart())
                actualDistance = target.getStart() - cs.getEnd() + 1;
            else
                // right candidate
                actualDistance = cs.getStart() - target.getEnd() + 1;

            if (minDistance <= actualDistance && actualDistance <= maxDistance)
                matches.add(createMatchCandidate(target, cs, false));
        }
        return matches;
    }

    @Override
    protected void updateList(List<CandidateSpan> candidateList) {
        candidateList.remove(0);
    }

}
