/*
 * Copyright (c) 2007 Mockito contributors
 * This program is made available under the terms of the MIT License.
 */
package org.mockito.internal.stubbing.answers;

import org.mockito.exceptions.Reporter;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import java.io.Serializable;

/**
 * Returns the passed parameter identity at specified index.
 *
 * <p>The <code>argumentIndex</code> represents the index in the argument array of the invocation.</p>
 * <p>If this number equals -1 then the last argument is returned.</p>
 *
 * @see org.mockito.AdditionalAnswers
 * @since 1.9.5
 */
public class ReturnsArgumentAt implements Answer<Object>, Serializable {

    private static final long serialVersionUID = -589315085166295101L;

    public static final int LAST_ARGUMENT = -1;

    private final int wantedArgumentPosition;

    /**
     * Build the identity answer to return the argument at the given position in the argument array.
     *
     * @param wantedArgumentPosition The position of the argument identity to return in the invocation.
     *                      Using <code>-1</code> indicates the last argument.
     */
    public ReturnsArgumentAt(int wantedArgumentPosition) {
        this.wantedArgumentPosition = checkWithinAllowedRange(wantedArgumentPosition);
    }

    public Object answer(InvocationOnMock invocation) throws Throwable {
        validateIndexWithinInvocationRange(invocation);
        return invocation.getArguments()[actualArgumentPosition(invocation)];
    }


    private int actualArgumentPosition(InvocationOnMock invocation) {
        return returningLastArg() ?
                lastArgumentIndexOf(invocation) :
                argumentIndexOf(invocation);
    }

    private boolean returningLastArg() {
        return wantedArgumentPosition == LAST_ARGUMENT;
    }

    private int argumentIndexOf(InvocationOnMock invocation) {
        return wantedArgumentPosition;
    }

    private int lastArgumentIndexOf(InvocationOnMock invocation) {
        return invocation.getArguments().length - 1;
    }

    private int checkWithinAllowedRange(int argumentPosition) {
        if (argumentPosition != LAST_ARGUMENT && argumentPosition < 0) {
            new Reporter().invalidArgumentRangeAtIdentityAnswerCreationTime();
        }
        return argumentPosition;
    }

    public int wantedArgumentPosition() {
        return wantedArgumentPosition;
    }

    public void validateIndexWithinInvocationRange(InvocationOnMock invocation) {
        if (!argumentPositionInRange(invocation)) {
            new Reporter().invalidArgumentPositionRangeAtInvocationTime(invocation,
                                                                        returningLastArg(),
                                                                        wantedArgumentPosition);
        }
    }

    private boolean argumentPositionInRange(InvocationOnMock invocation) {
        int actualArgumentPosition = actualArgumentPosition(invocation);
        if (actualArgumentPosition < 0) {
            return false;
        }
        if (!invocation.getMethod().isVarArgs()) {
            return invocation.getArguments().length > actualArgumentPosition;
        }
        // for all varargs accepts positive ranges
        return true;
    }

    public Class returnedTypeOnSignature(InvocationOnMock invocation) {
        int actualArgumentPosition = actualArgumentPosition(invocation);

        if(!invocation.getMethod().isVarArgs()) {
            return invocation.getMethod().getParameterTypes()[actualArgumentPosition];
        }

        Class<?>[] parameterTypes = invocation.getMethod().getParameterTypes();
        int varargPosition = parameterTypes.length - 1;

        if(actualArgumentPosition < varargPosition) {
            return parameterTypes[actualArgumentPosition];
        } else {
            return parameterTypes[varargPosition].getComponentType();
        }
    }
}
