/*
 * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.security.ldap.ppolicy;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

import netscape.ldap.ber.stream.BERChoice;
import netscape.ldap.ber.stream.BERElement;
import netscape.ldap.ber.stream.BEREnumerated;
import netscape.ldap.ber.stream.BERInteger;
import netscape.ldap.ber.stream.BERIntegral;
import netscape.ldap.ber.stream.BERSequence;
import netscape.ldap.ber.stream.BERTag;
import netscape.ldap.ber.stream.BERTagDecoder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.dao.DataRetrievalFailureException;

/**
 * Represents the response control received when a <tt>PasswordPolicyControl</tt> is used
 * when binding to a directory. Currently tested with the OpenLDAP 2.3.19 implementation
 * of the LDAP Password Policy Draft. It extends the request control with the control
 * specific data. This is accomplished by the properties <tt>timeBeforeExpiration</tt>,
 * <tt>graceLoginsRemaining</tt>.
 * <p>
 *
 *
 * @author Stefan Zoerner
 * @author Luke Taylor
 *
 * @see org.springframework.security.ldap.ppolicy.PasswordPolicyControl
 * @see <a href="http://www.ibm.com/developerworks/tivoli/library/t-ldap-controls/">Stefan
 * Zoerner's IBM developerworks article on LDAP controls.</a>
 */
public class PasswordPolicyResponseControl extends PasswordPolicyControl {
	// ~ Static fields/initializers
	// =====================================================================================

	private static final Log logger = LogFactory
			.getLog(PasswordPolicyResponseControl.class);

	// ~ Instance fields
	// ================================================================================================

	private final byte[] encodedValue;

	private PasswordPolicyErrorStatus errorStatus;

	private int graceLoginsRemaining = Integer.MAX_VALUE;
	private int timeBeforeExpiration = Integer.MAX_VALUE;

	// ~ Constructors
	// ===================================================================================================

	/**
	 * Decodes the Ber encoded control data. The ASN.1 value of the control data is:
	 *
	 * <pre>
	 *    PasswordPolicyResponseValue ::= SEQUENCE {       warning [0] CHOICE {
	 *           timeBeforeExpiration [0] INTEGER (0 .. maxInt),
	 *           graceAuthNsRemaining [1] INTEGER (0 .. maxInt) } OPTIONAL,       error   [1] ENUMERATED {
	 *           passwordExpired             (0),          accountLocked               (1),
	 *           changeAfterReset            (2),          passwordModNotAllowed       (3),
	 *           mustSupplyOldPassword       (4),          insufficientPasswordQuality (5),
	 *           passwordTooShort            (6),          passwordTooYoung            (7),
	 *           passwordInHistory           (8) } OPTIONAL }
	 * </pre>
	 *
	 */
	public PasswordPolicyResponseControl(byte[] encodedValue) {
		this.encodedValue = encodedValue;

		// PPolicyDecoder decoder = new JLdapDecoder();
		PPolicyDecoder decoder = new NetscapeDecoder();

		try {
			decoder.decode();
		}
		catch (IOException e) {
			throw new DataRetrievalFailureException("Failed to parse control value", e);
		}
	}

	// ~ Methods
	// ========================================================================================================

	/**
	 * Returns the unchanged value of the response control. Returns the unchanged value of
	 * the response control as byte array.
	 */
	@Override
	public byte[] getEncodedValue() {
		return this.encodedValue;
	}

	public PasswordPolicyErrorStatus getErrorStatus() {
		return this.errorStatus;
	}

	/**
	 * Returns the graceLoginsRemaining.
	 *
	 * @return Returns the graceLoginsRemaining.
	 */
	public int getGraceLoginsRemaining() {
		return this.graceLoginsRemaining;
	}

	/**
	 * Returns the timeBeforeExpiration.
	 *
	 * @return Returns the time before expiration in seconds
	 */
	public int getTimeBeforeExpiration() {
		return this.timeBeforeExpiration;
	}

	/**
	 * Checks whether an error is present.
	 *
	 * @return true, if an error is present
	 */
	public boolean hasError() {
		return this.errorStatus != null;
	}

	/**
	 * Checks whether a warning is present.
	 *
	 * @return true, if a warning is present
	 */
	public boolean hasWarning() {
		return (this.graceLoginsRemaining != Integer.MAX_VALUE)
				|| (this.timeBeforeExpiration != Integer.MAX_VALUE);
	}

	public boolean isExpired() {
		return this.errorStatus == PasswordPolicyErrorStatus.PASSWORD_EXPIRED;
	}

	public boolean isChangeAfterReset() {
		return this.errorStatus == PasswordPolicyErrorStatus.CHANGE_AFTER_RESET;
	}

	public boolean isUsingGraceLogins() {
		return this.graceLoginsRemaining < Integer.MAX_VALUE;
	}

	/**
	 * Determines whether an account locked error has been returned.
	 *
	 * @return true if the account is locked.
	 */
	public boolean isLocked() {
		return this.errorStatus == PasswordPolicyErrorStatus.ACCOUNT_LOCKED;
	}

	/**
	 * Create a textual representation containing error and warning messages, if any are
	 * present.
	 *
	 * @return error and warning messages
	 */
	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder("PasswordPolicyResponseControl");

		if (hasError()) {
			sb.append(", error: ").append(this.errorStatus.getDefaultMessage());
		}

		if (this.graceLoginsRemaining != Integer.MAX_VALUE) {
			sb.append(", warning: ").append(this.graceLoginsRemaining)
					.append(" grace logins remain");
		}

		if (this.timeBeforeExpiration != Integer.MAX_VALUE) {
			sb.append(", warning: time before expiration is ")
					.append(this.timeBeforeExpiration);
		}

		if (!hasError() && !hasWarning()) {
			sb.append(" (no error, no warning)");
		}

		return sb.toString();
	}

	// ~ Inner Interfaces
	// ===============================================================================================

	private interface PPolicyDecoder {
		void decode() throws IOException;
	}

	// ~ Inner Classes
	// ==================================================================================================

	/**
	 * Decoder based on Netscape ldapsdk library
	 */
	private class NetscapeDecoder implements PPolicyDecoder {
		public void decode() throws IOException {
			int[] bread = { 0 };
			BERSequence seq = (BERSequence) BERElement
					.getElement(new SpecificTagDecoder(),
							new ByteArrayInputStream(
									PasswordPolicyResponseControl.this.encodedValue),
							bread);

			int size = seq.size();

			if (logger.isDebugEnabled()) {
				logger.debug("PasswordPolicyResponse, ASN.1 sequence has " + size
						+ " elements");
			}

			for (int i = 0; i < seq.size(); i++) {
				BERTag elt = (BERTag) seq.elementAt(i);

				int tag = elt.getTag() & 0x1F;

				if (tag == 0) {
					BERChoice warning = (BERChoice) elt.getValue();

					BERTag content = (BERTag) warning.getValue();
					int value = ((BERInteger) content.getValue()).getValue();

					if ((content.getTag() & 0x1F) == 0) {
						PasswordPolicyResponseControl.this.timeBeforeExpiration = value;
					}
					else {
						PasswordPolicyResponseControl.this.graceLoginsRemaining = value;
					}
				}
				else if (tag == 1) {
					BERIntegral error = (BERIntegral) elt.getValue();
					PasswordPolicyResponseControl.this.errorStatus = PasswordPolicyErrorStatus
							.values()[error.getValue()];
				}
			}
		}

		class SpecificTagDecoder extends BERTagDecoder {
			/** Allows us to remember which of the two options we're decoding */
			private Boolean inChoice = null;

			@Override
			public BERElement getElement(BERTagDecoder decoder, int tag,
					InputStream stream, int[] bytesRead, boolean[] implicit)
							throws IOException {
				tag &= 0x1F;
				implicit[0] = false;

				if (tag == 0) {
					// Either the choice or the time before expiry within it
					if (this.inChoice == null) {
						setInChoice(true);

						// Read the choice length from the stream (ignored)
						BERElement.readLengthOctets(stream, bytesRead);

						int[] componentLength = new int[1];
						BERElement choice = new BERChoice(decoder, stream,
								componentLength);
						bytesRead[0] += componentLength[0];

						// inChoice = null;
						return choice;
					}
					else {
						// Must be time before expiry
						return new BERInteger(stream, bytesRead);
					}
				}
				else if (tag == 1) {
					// Either the graceLogins or the error enumeration.
					if (this.inChoice == null) {
						// The enumeration
						setInChoice(false);

						return new BEREnumerated(stream, bytesRead);
					}
					else {
						if (this.inChoice.booleanValue()) {
							// graceLogins
							return new BERInteger(stream, bytesRead);
						}
					}
				}

				throw new DataRetrievalFailureException("Unexpected tag " + tag);
			}

			private void setInChoice(boolean inChoice) {
				this.inChoice = Boolean.valueOf(inChoice);
			}
		}
	}

	/** Decoder based on the OpenLDAP/Novell JLDAP library */

	// private class JLdapDecoder implements PPolicyDecoder {
	//
	// public void decode() throws IOException {
	//
	// LBERDecoder decoder = new LBERDecoder();
	//
	// ASN1Sequence seq = (ASN1Sequence)decoder.decode(encodedValue);
	//
	// if(seq == null) {
	//
	// }
	//
	// int size = seq.size();
	//
	// if(logger.isDebugEnabled()) {
	// logger.debug("PasswordPolicyResponse, ASN.1 sequence has " +
	// size + " elements");
	// }
	//
	// for(int i=0; i < size; i++) {
	//
	// ASN1Tagged taggedObject = (ASN1Tagged)seq.get(i);
	//
	// int tag = taggedObject.getIdentifier().getTag();
	//
	// ASN1OctetString value = (ASN1OctetString)taggedObject.taggedValue();
	// byte[] content = value.byteValue();
	//
	// if(tag == 0) {
	// parseWarning(content, decoder);
	//
	// } else if(tag == 1) {
	// // Error: set the code to the value
	// errorCode = content[0];
	// }
	// }
	// }
	//
	// private void parseWarning(byte[] content, LBERDecoder decoder) {
	// // It's the warning (choice). Parse the number and set either the
	// // expiry time or number of logins remaining.
	// ASN1Tagged taggedObject = (ASN1Tagged)decoder.decode(content);
	// int contentTag = taggedObject.getIdentifier().getTag();
	// content = ((ASN1OctetString)taggedObject.taggedValue()).byteValue();
	// int number;
	//
	// try {
	// number = ((Long)decoder.decodeNumeric(new ByteArrayInputStream(content),
	// content.length)).intValue();
	// } catch(IOException e) {
	// throw new LdapDataAccessException("Failed to parse number ", e);
	// }
	//
	// if(contentTag == 0) {
	// timeBeforeExpiration = number;
	// } else if (contentTag == 1) {
	// graceLoginsRemaining = number;
	// }
	// }
	// }
}
