/*
 * Copyright (c) 2007 Mockito contributors
 * This program is made available under the terms of the MIT License.
 */

package org.mockito.internal.invocation;

import java.util.LinkedList;
import java.util.List;

import org.mockito.internal.util.collections.ListUtil;
import org.mockito.internal.util.collections.ListUtil.Filter;
import org.mockito.internal.verification.api.InOrderContext;
import org.mockito.invocation.Invocation;
import org.mockito.invocation.Location;

public class InvocationsFinder {

    public List<Invocation> findInvocations(List<Invocation> invocations, InvocationMatcher wanted) {
        return ListUtil.filter(invocations, new RemoveNotMatching(wanted));
    }

    public List<Invocation> findAllMatchingUnverifiedChunks(List<Invocation> invocations, InvocationMatcher wanted, InOrderContext orderingContext) {
        List<Invocation> unverified = removeVerifiedInOrder(invocations, orderingContext);
        return ListUtil.filter(unverified, new RemoveNotMatching(wanted));
    }

    /**
     * some examples how it works:
     * 
     * Given invocations sequence:
     * 1,1,2,1
     * 
     * if wanted is 1 and mode is times(2) then returns
     * 1,1  
     * 
     * if wanted is 1 and mode is atLeast() then returns
     * 1,1,1
     * 
     * if wanted is 1 and mode is times(x), where x != 2 then returns
     * 1,1,1
     */
    public List<Invocation> findMatchingChunk(List<Invocation> invocations, InvocationMatcher wanted, int wantedCount, InOrderContext context) {
        List<Invocation> unverified = removeVerifiedInOrder(invocations, context);
        List<Invocation> firstChunk = getFirstMatchingChunk(wanted, unverified);
        
        if (wantedCount != firstChunk.size()) {
            return this.findAllMatchingUnverifiedChunks(invocations, wanted, context);
        } else {
            return firstChunk;
        }
    }

    private List<Invocation> getFirstMatchingChunk(InvocationMatcher wanted, List<Invocation> unverified) {
        List<Invocation> firstChunk = new LinkedList<Invocation>();
        for (Invocation invocation : unverified) {
            if (wanted.matches(invocation)) {
                firstChunk.add(invocation);
            } else if (!firstChunk.isEmpty()) {
                break;
            }
        }
        return firstChunk;
    }
    
    public Invocation findFirstMatchingUnverifiedInvocation( List<Invocation> invocations, InvocationMatcher wanted, InOrderContext context ){
        for( Invocation invocation : removeVerifiedInOrder( invocations, context )){
            if( wanted.matches( invocation )){
                return invocation;
            }
        }
        return null;
    }
    
    public Invocation findSimilarInvocation(List<Invocation> invocations, InvocationMatcher wanted) {
        Invocation firstSimilar = null;
        for (Invocation invocation : invocations) {
            if (!wanted.hasSimilarMethod(invocation)) {
                continue;
            }
            if (firstSimilar == null) {
                firstSimilar = invocation;
            }
            if (wanted.hasSameMethod(invocation)) {
                return invocation;
            }
        }
        
        return firstSimilar;
    }
    
    public Invocation findFirstUnverified(List<Invocation> invocations) {
        return findFirstUnverified(invocations, null);
    }
    
    Invocation findFirstUnverified(List<Invocation> invocations, Object mock) {
        for (Invocation i : invocations) {
            boolean mockIsValid = mock == null || mock == i.getMock();
            if (!i.isVerified() && mockIsValid) {
                return i;
            }
        }
        return null;
    }

    public Location getLastLocation(List<Invocation> invocations) {
        if (invocations.isEmpty()) {
            return null;
        } else {
            Invocation last = invocations.get(invocations.size() - 1);
            return last.getLocation();
        }
    }
    
    public Invocation findPreviousVerifiedInOrder(List<Invocation> invocations, InOrderContext context) {
        LinkedList<Invocation> verifiedOnly = ListUtil.filter(invocations, new RemoveUnverifiedInOrder(context));
        
        if (verifiedOnly.isEmpty()) {
            return null;
        } else {
            return verifiedOnly.getLast();
        }
    }
    
    private List<Invocation> removeVerifiedInOrder(List<Invocation> invocations, InOrderContext orderingContext) {
        List<Invocation> unverified = new LinkedList<Invocation>();
        for (Invocation i : invocations) {
            if (orderingContext.isVerified(i)) {
                unverified.clear();
            } else {
                unverified.add(i);
            }
        }
        return unverified;
    }
    
    private static class RemoveNotMatching implements Filter<Invocation> {
        private final InvocationMatcher wanted;

        private RemoveNotMatching(InvocationMatcher wanted) {
            this.wanted = wanted;
        }

        public boolean isOut(Invocation invocation) {
            return !wanted.matches(invocation);
        }
    }

    private static class RemoveUnverifiedInOrder implements Filter<Invocation> {
        private final InOrderContext orderingContext;

        public RemoveUnverifiedInOrder(InOrderContext orderingContext) {
            this.orderingContext = orderingContext;
        }

        public boolean isOut(Invocation invocation) {
            return !orderingContext.isVerified(invocation);
        }
    }

    /**
     * i3 is unverified here:
     * 
     * i1, i2, i3
     *     v
     *     
     * all good here:
     * 
     * i1, i2, i3
     *     v   v
     * 
     * @param context
     * @param orderedInvocations
     */
    public Invocation findFirstUnverifiedInOrder(InOrderContext context, List<Invocation> orderedInvocations) {
        Invocation candidate = null;
        for(Invocation i : orderedInvocations) {
            if (!context.isVerified(i)) {
                candidate = candidate != null ? candidate : i;
            } else {
                candidate = null;
            }
        }
        return candidate;
    }
}