/**
 *  BlueCove - Java library for Bluetooth
 *  Copyright (C) 2007-2008 Vlad Skarzhevskyy
 *
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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.
 *
 *  @author vlads
 *  @version $Id: OBEXClientSessionImpl.java 2644 2008-12-23 06:48:40Z skarzhevskyy $
 */
package com.intel.bluetooth.obex;

import java.io.IOException;
import java.util.Vector;

import javax.microedition.io.StreamConnection;
import javax.obex.Authenticator;
import javax.obex.ClientSession;
import javax.obex.HeaderSet;
import javax.obex.Operation;
import javax.obex.ResponseCodes;

import com.intel.bluetooth.DebugLog;
import com.intel.bluetooth.Utils;

/**
 * ClientSession implementation. See <a href="http://bluetooth.com/Bluetooth/Learn/Technology/Specifications/">Bluetooth
 * Specification Documents</A> for details.
 * 
 * <p>
 * <b><u>Your application should not use this class directly.</u></b>
 * 
 */
public class OBEXClientSessionImpl extends OBEXSessionBase implements ClientSession {

	private OBEXClientOperation operation;

	private static final String FQCN = OBEXClientSessionImpl.class.getName();

	private static final Vector fqcnSet = new Vector();

	static {
		fqcnSet.addElement(FQCN);
	}

	/**
	 * Applications should not used this function.
	 * 
	 * @exception Error
	 *                if called from outside of BlueCove internal code.
	 */
	public OBEXClientSessionImpl(StreamConnection conn, OBEXConnectionParams obexConnectionParams) throws IOException,
			Error {
		super(conn, obexConnectionParams);
		Utils.isLegalAPICall(fqcnSet);
		this.requestSent = false;
		this.isConnected = false;
		this.operation = null;
	}

	public HeaderSet createHeaderSet() {
		return OBEXSessionBase.createOBEXHeaderSet();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.obex.ClientSession#connect(javax.obex.HeaderSet)
	 */
	public HeaderSet connect(HeaderSet headers) throws IOException {
		return connectImpl(headers, false);
	}

	private HeaderSet connectImpl(HeaderSet headers, boolean retry) throws IOException {
		validateCreatedHeaderSet(headers);
		if (isConnected) {
			throw new IOException("Session already connected");
		}
		byte[] connectRequest = new byte[4];
		connectRequest[0] = OBEXOperationCodes.OBEX_VERSION;
		connectRequest[1] = 0; /* Flags */
		connectRequest[2] = OBEXUtils.hiByte(obexConnectionParams.mtu);
		connectRequest[3] = OBEXUtils.loByte(obexConnectionParams.mtu);
		writePacketWithFlags(OBEXOperationCodes.CONNECT, connectRequest, (OBEXHeaderSetImpl) headers);

		byte[] b = readPacket();
		if (b.length < 6) {
			if (b.length == 3) {
				throw new IOException("Invalid response from OBEX server " + OBEXUtils.toStringObexResponseCodes(b[0]));
			}
			throw new IOException("Invalid response from OBEX server");
		}
		int serverMTU = OBEXUtils.bytesToShort(b[5], b[6]);
		if (serverMTU < OBEXOperationCodes.OBEX_MINIMUM_MTU) {
			throw new IOException("Invalid MTU " + serverMTU);
		}
		if (serverMTU < this.mtu) {
			this.mtu = serverMTU;
		}
		DebugLog.debug("mtu selected", this.mtu);

		OBEXHeaderSetImpl responseHeaders = OBEXHeaderSetImpl.readHeaders(b[0], b, 7);

		Object connID = responseHeaders.getHeader(OBEXHeaderSetImpl.OBEX_HDR_CONNECTION);
		if (connID != null) {
			this.connectionID = ((Long) connID).longValue();
		}

		validateAuthenticationResponse((OBEXHeaderSetImpl) headers, responseHeaders);
		if ((!retry) && (responseHeaders.getResponseCode() == ResponseCodes.OBEX_HTTP_UNAUTHORIZED)
				&& (responseHeaders.hasAuthenticationChallenge())) {
			HeaderSet replyHeaders = OBEXHeaderSetImpl.cloneHeaders(headers);
			handleAuthenticationChallenge(responseHeaders, (OBEXHeaderSetImpl) replyHeaders);
			return connectImpl(replyHeaders, true);

		}
		if (responseHeaders.getResponseCode() == ResponseCodes.OBEX_HTTP_OK) {
			this.isConnected = true;
		}
		return responseHeaders;
	}

	public HeaderSet disconnect(HeaderSet headers) throws IOException {
		validateCreatedHeaderSet(headers);
		canStartOperation();
		if (!isConnected) {
			throw new IOException("Session not connected");
		}
		writePacket(OBEXOperationCodes.DISCONNECT, (OBEXHeaderSetImpl) headers);
		byte[] b = readPacket();
		this.isConnected = false;
		if (this.operation != null) {
			this.operation.close();
			this.operation = null;
		}
		return OBEXHeaderSetImpl.readHeaders(b[0], b, 3);
	}

	public void setConnectionID(long id) {
		if (id < 0 || id > 0xffffffffl) {
			throw new IllegalArgumentException("Invalid connectionID " + id);
		}
		this.connectionID = id;
	}

	public long getConnectionID() {
		return this.connectionID;
	}

	private void canStartOperation() throws IOException {
		if (!isConnected) {
			throw new IOException("Session not connected");
		}
		if (this.operation != null) {
			if (!this.operation.isClosed()) {
				throw new IOException("Client is already in an operation");
			}
			this.operation = null;
		}
	}

	public HeaderSet setPath(HeaderSet headers, boolean backup, boolean create) throws IOException {
		validateCreatedHeaderSet(headers);
		canStartOperation();
		return setPathImpl(headers, backup, create, false);
	}

	private HeaderSet setPathImpl(HeaderSet headers, boolean backup, boolean create, boolean authentRetry)
			throws IOException {
		byte[] request = new byte[2];
		request[0] = (byte) ((backup ? 1 : 0) | (create ? 0 : 2));
		request[1] = 0;
		// DebugLog.debug("setPath b[3]", request[0]);
		writePacketWithFlags(OBEXOperationCodes.SETPATH_FINAL, request, (OBEXHeaderSetImpl) headers);

		byte[] b = readPacket();
		OBEXHeaderSetImpl responseHeaders = OBEXHeaderSetImpl.readHeaders(b[0], b, 3);
		validateAuthenticationResponse((OBEXHeaderSetImpl) headers, responseHeaders);
		if (!authentRetry && (responseHeaders.getResponseCode() == ResponseCodes.OBEX_HTTP_UNAUTHORIZED)
				&& (responseHeaders.hasAuthenticationChallenge())) {
			OBEXHeaderSetImpl retryHeaders = OBEXHeaderSetImpl.cloneHeaders(headers);
			handleAuthenticationChallenge(responseHeaders, retryHeaders);
			return setPathImpl(retryHeaders, backup, create, true);
		} else {
			return responseHeaders;
		}
	}

	public Operation get(HeaderSet headers) throws IOException {
		validateCreatedHeaderSet(headers);
		canStartOperation();
		this.operation = new OBEXClientOperationGet(this, (OBEXHeaderSetImpl) headers);
		return this.operation;
	}

	public Operation put(HeaderSet headers) throws IOException {
		validateCreatedHeaderSet(headers);
		canStartOperation();
		this.operation = new OBEXClientOperationPut(this, (OBEXHeaderSetImpl) headers);
		return this.operation;
	}

	public HeaderSet delete(HeaderSet headers) throws IOException {
		validateCreatedHeaderSet(headers);
		canStartOperation();
		return deleteImp(headers, false);
	}

	HeaderSet deleteImp(HeaderSet headers, boolean authentRetry) throws IOException {
		writePacket(OBEXOperationCodes.PUT_FINAL, (OBEXHeaderSetImpl) headers);
		byte[] b = readPacket();
		OBEXHeaderSetImpl responseHeaders = OBEXHeaderSetImpl.readHeaders(b[0], b, 3);
		validateAuthenticationResponse((OBEXHeaderSetImpl) headers, responseHeaders);
		if (!authentRetry && (responseHeaders.getResponseCode() == ResponseCodes.OBEX_HTTP_UNAUTHORIZED)
				&& (responseHeaders.hasAuthenticationChallenge())) {
			OBEXHeaderSetImpl retryHeaders = OBEXHeaderSetImpl.cloneHeaders(headers);
			handleAuthenticationChallenge(responseHeaders, retryHeaders);
			return deleteImp(retryHeaders, true);
		} else {
			return responseHeaders;
		}
	}

	public void setAuthenticator(Authenticator auth) {
		if (auth == null) {
			throw new NullPointerException("auth is null");
		}
		this.authenticator = auth;
	}

	public void close() throws IOException {
		if (this.operation != null) {
			this.operation.close();
			this.operation = null;
		}
		super.close();
	}

}
