// Copyright 1997 The Open Group Research Institute.  All rights reserved.

package krb5.lib;

import krb5.lib.asn1.*;
import java.util.Vector;

/**
 * This defines principal name
 */
public class PrincipalName implements Cloneable {
    /**
	 * Name type
	 * @see krb5.lib.Krb5
	 */
	private int nameType;
	
	/**
	 * Name 
	 * @see java.lang.String
	 */
	private String[] nameStrings;  //Principal names don't mutate often
	
	/**
	 * Realm
	 * @see krb5.lib.Realm
	 */
	private Realm nameRealm;  //optional; a null realm means use default
	//Note: the nameRealm is not included in the default ASN.1 encoding


	/**
	 * Class constructor
	 */
	private PrincipalName() {
	}

	/**
	 * Class constructor
	 *
	 * @param nameParts is of type String[]
	 * @param type is of type int
	 * @see java.lang.String
	 */
	public PrincipalName(String[] nameParts, int type) {
		nameStrings = nameParts;
		nameType = type;
		nameRealm = null;  
	}

	/**
	 * Class constructor
	 *
	 * @param nameParts is of type String[]
	 * @param realm is of type String
	 * @param type is of type int
	 * @see java.lang.String
	 */
	public PrincipalName(String[] nameParts, String realm, int type) throws RealmException {
		nameStrings = nameParts;
		nameRealm = new Realm(realm);  
		nameType = type;
	}


	/**
	 * Class constructor specifying the name with type = Krb5.KRB_NT_UNKNOWN)
	 *
	 * @param nameParts is of type String[]
	 * @see java.lang.String
	 * @see krb5.lib.Krb5
	 */
	public PrincipalName(String[] nameParts) {
		this(nameParts, Krb5.KRB_NT_UNKNOWN);
	}

	/**
	 * Clones this object
	 *
	 * @return Object is a result
	 * @see java.lang.Object
	 */
	public Object clone() {
		PrincipalName new_principalName = new PrincipalName();
		new_principalName.nameType = nameType;
		if (nameStrings != null) {
			new_principalName.nameStrings =
				new String[nameStrings.length];
			for (int i = 0; i < nameStrings.length; i++)
				new_principalName.nameStrings[i] =
					new String(nameStrings[i]);
		}
		if (nameRealm != null) {
			new_principalName.nameRealm = (Realm)nameRealm.clone();
		}
		return new_principalName;
	}

	/**
	 * Determines whether 2 principal names are the same or not
	 *
	 * @return boolean is a result
	 * @param other is of type PrincipalName
	 * @see krb5.lib.PrincipalName
	 */public boolean equals(PrincipalName other) {
		if (!equalsWithoutRealm(other))
			return false;
		if ((nameRealm != null && other.nameRealm == null) ||
			(nameRealm == null && other.nameRealm != null))
			return false;
		if (nameRealm != null && other.nameRealm != null) {
			if (!nameRealm.equals(other.nameRealm))
				return false;
		}
		return true;
	}


    /**
	 * Determines whether 2 principal names are the same without realm
	 *
	 * @return boolean is a result
	 * @param other is of type PrincipalName
	 * @see krb5.lib.PrincipalName
	 * @see krb5.lib.Realm
	 */
	public boolean equalsWithoutRealm(PrincipalName other) {
		//XXX should only compare known to unknown
		if (nameType != Krb5.KRB_NT_UNKNOWN &&
			other.nameType != Krb5.KRB_NT_UNKNOWN &&
			nameType != other.nameType)
			return false;
		if ((nameStrings != null && other.nameStrings == null) ||
			(nameStrings == null && other.nameStrings != null))
			return false;
		if (nameStrings != null && other.nameStrings != null) {
			if (nameStrings.length != other.nameStrings.length)
				return false;
			for (int i = 0; i < nameStrings.length; i++)
				if (!nameStrings[i].equals(other.nameStrings[i]))
					return false;
		}
		return true;
	}

	/**
	 * Class constructor specifying encoding reference
	 *
	 * @param ref is of type EncodeRef
	 * @exception Asn1Exception an exception
	 * @see krb5.lib.asn1.EncodeRef
	 * @see krb5.lib.Asn1Exception
	 */
	public PrincipalName(EncodeRef ref) throws Asn1Exception {
		Vector tempStrings = new Vector();
		nameRealm = null;
		EncodeRef subRef = decode.Sequence(ref);
		if (decode.Tag(subRef) == 0) {
			nameType = decode.Integer(subRef.startOfData());
			subRef.next();
		}
		else
			throw new Asn1Exception(Krb5.ASN1_BAD_ID);
		if (decode.Tag(subRef) == 1) {
			EncodeRef subSubRef = decode.Sequence(subRef.startOfData());
			while (subSubRef.isMoreInSequence()) {
				tempStrings.addElement(
					decode.GeneralString(subSubRef));
				subSubRef.next();
			}
			if (tempStrings.size() > 0) {
				nameStrings = new String[tempStrings.size()];
				tempStrings.copyInto(nameStrings);
			}
			subRef.next();
		}
		else
			throw new Asn1Exception(Krb5.ASN1_BAD_ID);
		if (subRef.isMoreInSequence())
			throw new Asn1Exception(Krb5.ASN1_BAD_ID);
	}

	//XXX empty components are skipped; this may be wrong
	//XXX this is protected because the definition of a principal
	//string is fixed
	/**
	 * Parses the given name 
	 *
	 * @return String[] is a result
	 * @param name is of type String
	 * @see java.lang.String
	 */
	protected static String[] parseName(String name) {
		Vector tempStrings = new Vector();

		int i = 0;
		int component_beginning = 0;
		while (i < name.length()) {
			if (name.charAt(i) == Krb5.NAME_COMPONENT_SEPARATOR) {
				if (i > 0 && name.charAt(i - 1) == '\\') {
					name = name.substring(0, i - 1) +
						name.substring(i, name.length());
					continue;
				}
				else {
					if (component_beginning < i)
						tempStrings.addElement(
							name.substring(component_beginning, i));
					component_beginning = i + 1;
				}
			}
			else if (name.charAt(i) == Krb5.NAME_REALM_SEPARATOR) {
				if (i > 0 && name.charAt(i - 1) == '\\') {
					name = name.substring(0, i - 1) +
						name.substring(i, name.length());
					continue;
				}
				else {
					if (component_beginning < i)
						tempStrings.addElement(
							name.substring(component_beginning, i));
					component_beginning = i + 1;
					break;
				}
			}
			i++;
		}
		if (i == name.length())
			if (component_beginning < i)
				tempStrings.addElement(
					name.substring(component_beginning, i));
		String[] result = new String[tempStrings.size()];
		tempStrings.copyInto(result);
		return result;
	}

	//XXX not yet implemented; should throw corresponding exception
	//XXX this is protected because the definition of a principal
	//string is fixed
	/**
     * Unparse the given array of strings
	 *
	 * @return String is a result
	 * @param nameStrings is of type String[]
	 * @see java.lang.String
	 */
	protected static String unparseName(String[] nameStrings) {
		return null;
	}

	/**
	 * Class constructor specifying name and type
	 *
	 * @param name is of type String
	 * @param type is of type int
	 * @exception RealmException an exception
	 * @see java.lang.String
	 * @see krb5.lib.RealmException
	 */	
	public PrincipalName(String name, int type)	throws RealmException {
		String[] nameParts = parseName(name);
		Realm tempRealm = null;
		String realmString = Realm.parseRealmAtSeparator(name);
		if (realmString != null)
			tempRealm = new Realm(name);

		//XXX need to parse/switch on type; don't do it for now
		switch (type) {
			case Krb5.KRB_NT_UNKNOWN:
			case Krb5.KRB_NT_PRINCIPAL:
			case Krb5.KRB_NT_SRV_INST:
			case Krb5.KRB_NT_SRV_HST:
			case Krb5.KRB_NT_SRV_XHST:
			case Krb5.KRB_NT_UID:
			default:
				nameStrings = nameParts;
				nameType = type;
				nameRealm = tempRealm;
		}
	}

	/**
	 * Class constructor with specifying name and type = Krb5.KRB_NT_UNKNOWN
	 *
	 * @param name is of type String
	 * @exception RealmException an exception
	 * @see java.lang.String
	 * @see krb5.lib.RealmException
	 * @see krb5.lib.Krb5
	 */
	public PrincipalName(String name) throws RealmException {
		this(name, Krb5.KRB_NT_UNKNOWN);
	}

	/**
	 * Class constructor with specifying name, realm and type = Krb5.KRB_NT_UNKNOWN
	 *
	 * @param name is of type String
	 * @param realm is of type String
	 * @exception RealmException an exception
	 * @see java.lang.String
	 * @see krb5.lib.Realm
	 * @see krb5.lib.RealmException
	 * @see krb5.lib.Krb5	 
	 */
	public PrincipalName(String name, String realm) throws RealmException {
		this(name, Krb5.KRB_NT_UNKNOWN);
		nameRealm = new Realm(realm);
	}

	/**
	 * Returns name type
	 * All the name type are defined in krb5.lib.Krb5
	 *
	 * @return int is a result
	 * @see krb5.lib.Krb5
	 */
	public int getNameType() {
		return nameType;
	}

	/**
	 * Returns name strings
	 *
	 * @return String[] is a result
	 * @see java.lang.String
	 */
	public String[] getNameStrings() {
		return nameStrings;
	}

	/**
	 * Returns principal name in a byte array
	 *
	 * @return byte[][] is a result
	 */
	public byte[][] toByteArray() {
		byte[][] result = new byte[nameStrings.length][];
		for (int i = 0; i < nameStrings.length; i++) {
			result[i] = new byte[nameStrings[i].length()];
			nameStrings[i].getBytes(0, nameStrings[i].length(),
				result[i], 0);
		}
		return result;
	}

	/**
	 * Returns realm string
	 *
	 * @return String is a result
	 * @see java.lang.String
	 */
	public String getRealmString() {
		if (nameRealm != null)
			return nameRealm.toString();
		return null;
	}

	/**
	 * Returns realm
	 *
	 * @return Realm is a result
	 * @see krb5.lib.Realm
	 */
	public Realm getRealm() {
		return nameRealm;
	}

	/**
	 * Sets realm 
	 *
	 * @param new_nameRealm is of type Realm
	 * @exception RealmException an exception
	 * @see krb5.lib.Realm
	 * @see krb5.lib.RealmException
	 */
	public void setRealm(Realm new_nameRealm) {
		nameRealm = new_nameRealm;
	}

	/**
	 * Sets realm string
	 *
	 * @param realmsString is of type String
	 * @exception RealmException an exception
	 * @see java.lang.String
	 * @see krb5.lib.RealmException
	 */
	public void setRealm(String realmsString) throws RealmException {
		nameRealm = new Realm(realmsString);
	}

	/**
	 * Returns salt value
	 *
	 * @return String is a result
	 * @see java.lang.String
	 */
	public String getSalt() {
		StringBuffer salt = new StringBuffer();
		//XXX may need to catch null realm case as an error
		if (nameRealm != null)
			salt.append(nameRealm.toString());
		for (int i = 0; i < nameStrings.length; i++)
			salt.append(nameStrings[i]);
		return salt.toString();
	}

	/**
	 * Converts principal name into printable format
	 *
	 * @return String is a result
	 * @see java.lang.String
	 */
	public String toString() {
		StringBuffer str = new StringBuffer();
		for (int i = 0; i < nameStrings.length; i++) {
			if (i > 0) 
				str.append(Krb5.NAME_COMPONENT_SEPARATOR);
			str.append(nameStrings[i]);
		}
		//XXX may need to catch null realm case as an error
		if (nameRealm != null)
			str.append(Krb5.NAME_REALM_SEPARATOR);
			str.append(nameRealm.toString());
		return str.toString();
	}

	/**
	 * Returns name string
	 *
	 * @return String is a result
	 * @see java.lang.String
	 */
	public String getNameString() {
		StringBuffer str = new StringBuffer();
		for (int i = 0; i < nameStrings.length; i++) {
			if (i > 0) 
				str.append("/");
			str.append(nameStrings[i]);
		}
		return str.toString();
	}

	/**
	 * Encodes the name string in asn1
	 *
	 * @return byte[][] is a result
	 * @exception Asn1Exception an exception
	 * @see krb5.lib.Asn1Exception
	 */
	public byte[][] asn1EncodeNameStrings() throws Asn1Exception {
		byte[][] result = new byte[nameStrings.length][];
		for (int i = 0; i < nameStrings.length; i++) {
			result[i] = encode.GeneralString(nameStrings[i]);
		}
		return result;
	}

	/**
	 * Encodes the data in asn1
	 *
	 * @return byte[] is a result
	 * @exception Asn1Exception an exception
	 * @see krb5.lib.Asn1Exception
	 */
	public byte[] asn1Encode() throws Asn1Exception {
		byte[][] name_strings = asn1EncodeNameStrings();
		byte[][] cname = {
			encode.prependExplicitTag(0, encode.Integer(nameType)),
			encode.prependExplicitTag(1, encode.Sequence(name_strings))
		};
		return encode.Sequence(cname);
	}

	/**
	 * Testing
	 *
	 * @param n is of type String
	 * @exception RealmException an exception
	 * @see java.lang.String
	 * @see krb5.lib.RealmException
	 */
	public static void test(String n) throws RealmException {
		System.out.println("name='" + n + "'");
		PrincipalName pn = new PrincipalName(n);
		String[] s = pn.getNameStrings();
		for (int i = 0; i < s.length; i++)
			System.out.println("part[" + i + "]='" + s[i] + "'");
		System.out.println("realm='" + pn.getRealmString() + "'");
		System.out.println();
	}

	//XXX should be removed for size
	/**
	 * Main
	 *
	 * @param args is of type String[]
	 * @exception RealmException an exception
	 * @see java.lang.String
	 * @see krb5.lib.RealmException
	 */
	public static void main(String[] args) throws RealmException {
		test("");
		test("/");
		test("@");
		test("/@");
		//test("@/"); //throws RealmException(Krb5.REALM_ILLCHAR)
		test(" ");
		test(" /");
		test(" @");
		test(" /@");
		//test(" @/"); //throws RealmException(Krb5.REALM_ILLCHAR)
		test("foo");
		test("foo/");
		test("foo@");
		test("foo/@");
		//test("foo@/"); //throws RealmException(Krb5.REALM_ILLCHAR)
		test("foo/bar");
		test("foo@bar");
		test("foo//bar");
		test("foo/@bar");
		//test("foo@/bar"); //throws RealmException(Krb5.REALM_ILLCHAR)
		test("foo@@bar");
		test("/foo");
		test("//foo");
		test("/foo/");
		test("///foo/");
		test("/////@");
		test("//foo////@");
		test("foo/bar@");
		test("foo/bar@dog");
		test("foo/bar/@");
		test("foo/bar/@dog");
		test("foo/bar/car@");
		test("foo/bar/car@dog");
		test("foo/bar/car/@");
		test("foo/bar/car/@dog");
		test("foo//car/@dog");
		test("foo/bar//@dog");
		test("//bar/car/@dog");
		System.out.println();
		System.out.println();
		test("foo\\/bar");
		test("foo\\@bar");
		test("foo\\/");
		test("foo\\@");
		test("\\/bar");
		test("\\@bar");
		test("foo/\\/bar@car");
		test("foo\\//bar@car");
		test("foo\\/\\/bar@car");
		test("krbtgt/bambam.osf.org");
		test("krbtgt/booboo.osf.org@booboo.osf.org");
		PrincipalName sname = new PrincipalName(
			"krbtgt/booboo.osf.org@booboo.osf.org");
	}
}

