/*
 * 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.
 */

package com.sun.jini.discovery.internal;

import com.sun.jini.discovery.DatagramBufferFactory;
import com.sun.jini.discovery.DiscoveryProtocolException;
import com.sun.jini.discovery.MulticastAnnouncement;
import com.sun.jini.discovery.MulticastRequest;
import com.sun.jini.discovery.UnicastResponse;
import java.io.ByteArrayOutputStream;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.DataOutput;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.UTFDataFormatException;
import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CoderResult;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Set;
import net.jini.core.constraint.ClientAuthentication;
import net.jini.core.constraint.ClientMaxPrincipal;
import net.jini.core.constraint.ClientMaxPrincipalType;
import net.jini.core.constraint.ClientMinPrincipal;
import net.jini.core.constraint.ClientMinPrincipalType;
import net.jini.core.constraint.Confidentiality;
import net.jini.core.constraint.ConstraintAlternatives;
import net.jini.core.constraint.Delegation;
import net.jini.core.constraint.DelegationAbsoluteTime;
import net.jini.core.constraint.DelegationRelativeTime;
import net.jini.core.constraint.Integrity;
import net.jini.core.constraint.InvocationConstraint;
import net.jini.core.constraint.InvocationConstraints;
import net.jini.core.constraint.ServerAuthentication;
import net.jini.core.constraint.ServerMinPrincipal;
import net.jini.core.lookup.ServiceID;
import net.jini.core.lookup.ServiceRegistrar;
import net.jini.io.MarshalledInstance;
import net.jini.io.UnsupportedConstraintException;

/**
 * Provides utility methods for plaintext data operations.
 */
public class Plaintext {

    private static final int MAX_USHORT = 0xFFFF;
    private static final int SHORT_LEN = 2;
    private static final int SERVICE_ID_LEN = 16;

    private static final Charset utf = Charset.forName("UTF-8");
    private static final Set supportedConstraints = new HashSet();
    static {
	supportedConstraints.add(Integrity.NO);
	supportedConstraints.add(Confidentiality.NO);
	supportedConstraints.add(ClientAuthentication.NO);
	supportedConstraints.add(ServerAuthentication.NO);
	supportedConstraints.add(Delegation.NO);
	supportedConstraints.add(ClientMaxPrincipal.class);
	supportedConstraints.add(ClientMaxPrincipalType.class);
	supportedConstraints.add(ClientMinPrincipal.class);
	supportedConstraints.add(ClientMinPrincipalType.class);
	supportedConstraints.add(ServerMinPrincipal.class);
	supportedConstraints.add(DelegationAbsoluteTime.class);
	supportedConstraints.add(DelegationRelativeTime.class);
    }

    /* Uninstantiable */
    private Plaintext() {
    }

    /**
     * Returns the given integer value as an unsigned short, throwing an
     * IllegalArgumentException if the value is negative or too large.
     */
    public static short intToUshort(int i) {
	if (i < 0 || i > MAX_USHORT) {
	    throw new IllegalArgumentException("invalid value: " + i);
	}
	return (short) i;
    }

    /**
     * Returns an integer with the unsigned value of the given short.
     */
    public static int ushortToInt(short s) {
	return s & MAX_USHORT;
    }

    /**
     * Returns a byte array containing the UTF encoding of the given string.
     */
    public static byte[] toUtf(String s) throws UTFDataFormatException {
	try {
	    ByteArrayOutputStream bout = new ByteArrayOutputStream(s.length());
	    DataOutput dout = new DataOutputStream(bout);
	    dout.writeUTF(s);
	    return bout.toByteArray();
	} catch (UTFDataFormatException e) {
	    throw e;
	} catch (IOException e) {
	    throw new AssertionError(e);
	}
    }

    /**
     * Writes the given string to the provided buffer in UTF format, starting
     * at the buffer's current position and not exceeding its limit.  If the
     * encoding of the given string exceeds the buffer's limit, then a
     * BufferOverflowException is thrown and the position of the buffer is left
     * unchanged from its initial value.
     */
    public static void putUtf(ByteBuffer buf, String s)
	throws UTFDataFormatException
    {
	ByteBuffer dup = buf.duplicate();
	dup.putShort((short) 0);
	int start = dup.position();
	CoderResult cr = 
	    utf.newEncoder().encode(CharBuffer.wrap(s), dup, true);
	if (cr.isUnderflow()) {
	    buf.putShort(intToUshort(dup.position() - start));
	    buf.position(dup.position());
	} else if (cr.isOverflow()) {
	    throw new BufferOverflowException();
	} else {
	    throw new UTFDataFormatException(cr.toString());
	}
    }

    /**
     * Returns string read from the given buffer in UTF format , starting at
     * the buffer's current position and not exceeding its limit.  If the
     * string's encoding extends past the buffer's limit, then a
     * BufferUnderflowException is thrown and the position of the buffer is
     * left unchanged from its initial value.
     */
    public static String getUtf(ByteBuffer buf) throws UTFDataFormatException {
	ByteBuffer dup = buf.duplicate();
	int len = ushortToInt(dup.getShort());
	if (len > dup.remaining()) {
	    throw new BufferUnderflowException();
	}
	dup.limit(dup.position() + len);
	try {
	    String s = utf.newDecoder().decode(dup).toString();
	    buf.position(dup.position());
	    return s;
	} catch (CharacterCodingException e) {
	    throw (UTFDataFormatException)
		new UTFDataFormatException().initCause(e);
	}
    }

    /**
     * Returns normally if the given constraints can be satisfied by a
     * plaintext-based format/protocol (such as net.jini.discovery.plaintext,
     * or version 1 of the discovery protocols); otherwise, throws an
     * UnsupportedConstraintException .  Null constraints are considered
     * equivalent to empty constraints.
     */
    public static void checkConstraints(InvocationConstraints constraints)
	throws UnsupportedConstraintException
    {
	if (constraints == null) {
	    return;
	}
	for (Iterator i = constraints.requirements().iterator(); i.hasNext(); )
	{
	    InvocationConstraint c = (InvocationConstraint) i.next();
	    if (!supported(c)) {
		throw new UnsupportedConstraintException(
			      "unsupported constraint: " + c);
	    }
	}
    }

    /**
     * Encodes multicast request according to the net.jini.discovery.plaintext
     * format.
     */
    public static void encodeMulticastRequest(MulticastRequest request,
					      DatagramBufferFactory bufs)
	throws IOException
    {
	try {
	    LinkedList groups = new LinkedList();
	    groups.addAll(Arrays.asList(request.getGroups()));
	    do {
		ByteBuffer buf = bufs.newBuffer();

		// write client host
		putUtf(buf, request.getHost());

		// write client port
		buf.putShort(intToUshort(request.getPort()));

		// write first lookup group
		int ngroups = 0;
		int ngroupsPos = buf.position();
		buf.putShort((short) 0);
		if (!groups.isEmpty()) {
		    putUtf(buf, (String) groups.removeFirst());
		    ngroups++;
		}

		// calculate size of known service ID list
		ServiceID[] ids = request.getServiceIDs();
		int maxIds = Math.min(ids.length, MAX_USHORT);
		int maxIdsLen = SHORT_LEN + maxIds * SERVICE_ID_LEN;

		// write additional lookup groups, as space allows
		if (buf.remaining() > maxIdsLen && !groups.isEmpty()) {
		    int slim = buf.limit();
		    buf.limit(slim - maxIdsLen);
		    try {
			do {
			    putUtf(buf, (String) groups.getFirst());
			    groups.removeFirst();
			    ngroups++;
			} while (!groups.isEmpty() && ngroups < MAX_USHORT);
		    } catch (BufferOverflowException e) {
		    }
		    buf.limit(slim);
		}
		buf.putShort(ngroupsPos, intToUshort(ngroups));

		// write known service IDs
		int nids = Math.min(
		    maxIds,
		    (buf.remaining() - SHORT_LEN) / SERVICE_ID_LEN);
		buf.putShort(intToUshort(nids));
		for (int i = 0; i < nids; i++) {
		    ServiceID id = ids[i];
		    buf.putLong(id.getMostSignificantBits());
		    buf.putLong(id.getLeastSignificantBits());
		}
	    } while (!groups.isEmpty());

	} catch (RuntimeException e) {
	    throw new DiscoveryProtocolException(null, e);
	}
    }

    /**
     * Decodes multicast request according to the net.jini.discovery.plaintext
     * format.
     */
    public static MulticastRequest decodeMulticastRequest(ByteBuffer buf)
	throws IOException
    {
	try {
	    // read client host
	    String host = getUtf(buf);

	    // read client port
	    int port = ushortToInt(buf.getShort());

	    // read lookup groups
	    String[] groups = new String[ushortToInt(buf.getShort())];
	    for (int i = 0; i < groups.length; i++) {
		groups[i] = getUtf(buf);
	    }

	    // read known service IDs
	    ServiceID[] ids = new ServiceID[ushortToInt(buf.getShort())];
	    for (int i = 0; i < ids.length; i++) {
		long hi = buf.getLong();
		long lo = buf.getLong();
		ids[i] = new ServiceID(hi, lo);
	    }

	    return new MulticastRequest(host, port, groups, ids);

	} catch (RuntimeException e) {
	    throw new DiscoveryProtocolException(null, e);
	}
    }

    /**
     * Encodes multicast announcement according to the
     * net.jini.discovery.plaintext format.
     */
    public static void encodeMulticastAnnouncement(
					    MulticastAnnouncement announcement,
					    DatagramBufferFactory bufs)
	throws IOException
    {
	try {
	    LinkedList groups = new LinkedList();
	    groups.addAll(Arrays.asList(announcement.getGroups()));
	    do {
		ByteBuffer buf = bufs.newBuffer();
		int slim = buf.limit();
		buf.limit(slim - SERVICE_ID_LEN);

		// write sequence number
		buf.putLong(announcement.getSequenceNumber());

		// write LUS host
		putUtf(buf, announcement.getHost());

		// write LUS port
		buf.putShort(intToUshort(announcement.getPort()));

		// write LUS member groups, as space allows
		int ngroups = 0;
		int ngroupsPos = buf.position();
		buf.putShort((short) 0);
		try {
		    while (!groups.isEmpty() && ngroups < MAX_USHORT) {
			putUtf(buf, (String) groups.getFirst());
			groups.removeFirst();
			ngroups++;
		    }
		} catch (BufferOverflowException e) {
		    if (ngroups == 0) {
			throw e;
		    }
		}
		buf.putShort(ngroupsPos, intToUshort(ngroups));

		// write LUS service ID
		ServiceID id = announcement.getServiceID();
		buf.limit(slim);
		buf.putLong(id.getMostSignificantBits());
		buf.putLong(id.getLeastSignificantBits());

	    } while (!groups.isEmpty());

	} catch (RuntimeException e) {
	    throw new DiscoveryProtocolException(null, e);
	}
    }

    /**
     * Decodes multicast announcement according to the
     * net.jini.discovery.plaintext format.
     */
    public static MulticastAnnouncement decodeMulticastAnnouncement(
							    ByteBuffer buf)
	throws IOException
    {
	try {
	    // read sequence number
	    long seq = buf.getLong();

	    // read LUS host
	    String host = getUtf(buf);

	    // read LUS port
	    int port = ushortToInt(buf.getShort());

	    // read LUS member groups
	    String[] groups = new String[ushortToInt(buf.getShort())];
	    for (int i = 0; i < groups.length; i++) {
		groups[i] = getUtf(buf);
	    }

	    // read LUS service ID
	    long idhi = buf.getLong();
	    long idlo = buf.getLong();

	    return new MulticastAnnouncement(
		seq, host, port, groups, new ServiceID(idhi, idlo));

	} catch (RuntimeException e) {
	    throw new DiscoveryProtocolException(null, e);
	}
    }

    /**
     * Writes unicast response according to the net.jini.discovery.plaintext
     * format.
     */
    public static void writeUnicastResponse(OutputStream out,
					    UnicastResponse response,
					    Collection context)
	throws IOException
    {
	try {
	    DataOutput dout = new DataOutputStream(out);

	    // write LUS host
	    dout.writeUTF(response.getHost());

	    // write LUS port
	    dout.writeShort(intToUshort(response.getPort()));

	    // write LUS member groups
	    String[] groups = response.getGroups();
	    dout.writeInt(groups.length);
	    for (int i = 0; i < groups.length; i++) {
		dout.writeUTF(groups[i]);
	    }

	    // write LUS proxy
	    new ObjectOutputStream(out).writeObject(
		new MarshalledInstance(response.getRegistrar(), context));
	} catch (RuntimeException e) {
	    throw new DiscoveryProtocolException(null, e);
	}
    }

    /**
     * Reads unicast response according to the net.jini.discovery.plaintext
     * format.
     */
    public static UnicastResponse readUnicastResponse(
					    InputStream in,
					    ClassLoader defaultLoader,
					    boolean verifyCodebaseIntegrity,
					    ClassLoader verifierLoader,
					    Collection context)
	throws IOException, ClassNotFoundException
    {
	try {
	    DataInput din = new DataInputStream(in);

	    // read LUS host
	    String host = din.readUTF();

	    // read LUS port
	    int port = din.readUnsignedShort();

	    // read LUS member groups
	    String[] groups = new String[din.readInt()];
	    for (int i = 0; i < groups.length; i++) {
		groups[i] = din.readUTF();
	    }

	    // read LUS proxy
	    MarshalledInstance mi = 
		(MarshalledInstance) new ObjectInputStream(in).readObject();
	    ServiceRegistrar reg = (ServiceRegistrar) mi.get(
		defaultLoader,
		verifyCodebaseIntegrity,
		verifierLoader,
		context);

	    return new UnicastResponse(host, port, groups, reg);

	} catch (RuntimeException e) {
	    throw new DiscoveryProtocolException(null, e);
	}
    }

    private static boolean supported(InvocationConstraint ic) {
	if (ic instanceof ConstraintAlternatives) {
	    ConstraintAlternatives ca = (ConstraintAlternatives) ic;
	    for (Iterator i = ca.elements().iterator(); i.hasNext(); ) {
		if (supported((InvocationConstraint) i.next())) {
		    return true;
		}
	    }
	    return false;
	}
	return supportedConstraints.contains(ic) ||
	       supportedConstraints.contains(ic.getClass());
    }
}
