/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2005 Sun Microsystems Inc. All Rights Reserved
 *
 * The contents of this file are subject to the terms
 * of the Common Development and Distribution License
 * (the License). You may not use this file except in
 * compliance with the License.
 *
 * You can obtain a copy of the License at
 * https://opensso.dev.java.net/public/CDDLv1.0.html or
 * opensso/legal/CDDLv1.0.txt
 * See the License for the specific language governing
 * permission and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL
 * Header Notice in each file and include the License file
 * at opensso/legal/CDDLv1.0.txt.
 * If applicable, add the following below the CDDL Header,
 * with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * $Id: DSAMECallbackHandler.java,v 1.7 2008/08/19 19:08:54 veiming Exp $
 *
 * Portions Copyrighted 2014-2016 ForgeRock AS.
 */

package com.sun.identity.authentication.service;

import static org.forgerock.openam.utils.Time.*;

import java.io.IOException;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;

import com.sun.identity.shared.debug.Debug;
import com.sun.identity.authentication.spi.PagePropertiesCallback;

/**
 * This class is the OpenAM implementation for Java
 * <code>CallbackHandler</code> and it can be passed to underlying 
 * auth services so that it may interact with other components to retrieve 
 * specific authentication data, such as usernames and passwords, or to display 
 * certain information, such as error and warning messages.
*/
public class DSAMECallbackHandler implements CallbackHandler {

    // this should be read by AuthContext in order to receive
    // what callbacks the module needs

    static Debug debug = Debug.getInstance("amCallback");
    AMLoginContext am;
    LoginState loginState;

    // this will be sent by AuthContext for module to read.
    Callback[] submitRequiredInfo = null;
    static AuthThreadManager authThreadManager ;
    String sid = null;
    private static final DSAMECallbackHandlerError HANDLER_ERROR =
            new DSAMECallbackHandlerError("return from DSAMECallback");

    /**
     * Creates <code>DSAMECallbackHandler</code> object and it associates 
     * login thread and login state with callback hndler 
     * @param am <code>AMLoginContext</code> for this callback
     */
    public DSAMECallbackHandler(AMLoginContext am) {
        this.am = am;
        this.authThreadManager= am.authThread;
        this.loginState = am.getLoginState();
    }
        
    private void setPageTimeout(Callback[] callbacks) {
        long pageTimeOut = getTimeOut(callbacks);
        loginState.setPageTimeOut(pageTimeOut);
        long lastCallbackSent = currentTimeMillis();
        loginState.setLastCallbackSent(lastCallbackSent);
    }
        
    /**
     * <p> Retrieves or displays the information requested in the
     * provided Callbacks.
     *
     * <p> This method implementation checks the
     * instance(s) of the <code>Callback</code> object(s) passed in
     * to retrieve or display the requested information.
     * @param callbacks an array of <code>Callback</code> objects provided
     *        by an underlying security service which contains
     *        the information requested to be retrieved or displayed.
     *
     * @exception java.io.IOException if an input or output error occurs.
     * @exception UnsupportedCallbackException if the implementation of this
     *            method does not support one or more of the Callbacks
     *            specified in the <code>callbacks</code> parameter.
     */
    public void handle(Callback[] callbacks)
        throws IOException, UnsupportedCallbackException {

        if (debug.messageEnabled()) {
            debug.message("callback handler method");
        }

        if ((callbacks.length == 1) &&
            (callbacks[0] instanceof LoginStateCallback)) {
            debug.message("LoginState Callbacks");
            ((LoginStateCallback) callbacks[0]).setLoginState(loginState);
        } else {
            if (am.isPureJAAS()) {
                setPageTimeout(callbacks);
                loginState.setSubmittedCallback(null,am) ;
                    loginState.setReceivedCallback(callbacks,am) ;
                Thread thread = Thread.currentThread();

                if (debug.messageEnabled()) {
                        debug.message("waiting for submitted info " + thread);
                }

                callbacks = am.submitCallbackInfo();
                // check if the thread had timedout 
                if (authThreadManager.isTimedOut(thread)) {
                    loginState.setTimedOut(true);
                    loginState.setReceivedCallback(null,am) ;
                    authThreadManager.removeFromHash(thread,"timedOutHash");
                    throw new IOException(AMAuthErrorCode.AUTH_TIMEOUT);
                }
                    // check if there is a timeout 
                checkLoginTimeout();

            } else {
                if (loginState.getSubmittedInfo() != null) {
                    debug.message("DSAMEHandler: found submitted callbacks !");
                        // check if there is a timeout 
                    checkLoginTimeout();
                    Callback[] callbacks2 = loginState.getSubmittedInfo();
                    copyCallbacks(callbacks, callbacks2);
                    loginState.setReceivedCallback_NoThread(null);
                } else {
                    setPageTimeout(callbacks);
                    loginState.setReceivedCallback_NoThread(callbacks) ;
                    debug.message("Set callbacks, throwing java.lang.Error.");
                    throw HANDLER_ERROR;
                }
             }

            if (debug.messageEnabled()) {
                debug.message("DSAMECAllbackhandler..."+ callbacks);
            }
            if (am.isPureJAAS()) {
                loginState.setReceivedCallback(null,am) ;
            }
        }
    } 

    /**
     * Clones callbacks from cb2 to cb1.
     * @param cb1 new callbacks will be cloned from.
     * @param cb2 original callbacks will be cloned to.
     */
    private void copyCallbacks(Callback[] cb1, Callback[] cb2) {
        int len1 = cb1.length;
        int len2 = cb2.length;

        if (len1 == len2) {
            for (int m = 0; m < len1; m++) {
                if (cb1[m] != cb2[m]) {
                    cb1[m] = cb2[m];
                }
            }
        } else {
            int indx1 = 0;
            int indx2 = 0;
            while (indx1 < len1 && cb1[indx1] instanceof PagePropertiesCallback) {
                indx1 ++;
            }

            while (indx2 < len2 && cb2[indx2] instanceof PagePropertiesCallback) {
                indx2 ++;
            }

            int n = len1 - indx1;
            if (n > len2 - indx2) {
                n = len2 - indx2;
            }
            for (int m = 0; m < n; m++ ) {
                if (cb1[indx1] != cb2[indx2]) {
                    cb1[indx1] = cb2[indx2];
                }
                indx1 ++;
                indx2 ++;
            }
        }
    }


    /**
     *  Returns timeout value , 60 secs assumed if no timeout value found 
     *  @param callbacks checked for timeout. 
     *  @return  timeout value for callbacks.
     */
    long getTimeOut(Callback[] callbacks) {

        long pageTimeOut = 60;

        if (callbacks != null && 
            callbacks[0] instanceof PagePropertiesCallback) {
            PagePropertiesCallback pagePropertyCallback
                = (PagePropertiesCallback) callbacks[0];
            pageTimeOut =
                new Integer(pagePropertyCallback.getTimeOutValue()).longValue();
        } 
        return pageTimeOut;
    }

    /**
      * Check login time out. LoginTimeoutException will be thrown if the login
      * time out has been reached.
      * @exception IOException must be thrown if the login timeout
      * has been reached.
      */
    void checkLoginTimeout() throws IOException {

        long lastCallbackSent = loginState.getLastCallbackSent();
        long pageTimeOut = loginState.getPageTimeOut();
        long now = currentTimeMillis();
        if ((lastCallbackSent + ((pageTimeOut-3)*1000)) < now) {
            debug.message("Page Timeout");
            loginState.setTimedOut(true);
            loginState.setReceivedCallback(null,am) ;
            throw new IOException(AMAuthErrorCode.AUTH_TIMEOUT);
        }
    }

    /**
     * This error is used to control the authentication processing: if there is a problem while processing the
     * callbacks this error prevents the auth framework consider this error as an authentication module failure.
     */
    public static class DSAMECallbackHandlerError extends Error {

        public DSAMECallbackHandlerError(String message) {
            super(message);
        }

        @Override
        public synchronized Throwable fillInStackTrace() {
            //As this is an expected error that's used for flow control, we do not need to populate the stacktrace.
            return this;
        }
    }
}
