
package org.biomoby.shared.data;

import org.biomoby.registry.meta.Registry;
import org.biomoby.shared.MobyDataType;
import org.biomoby.shared.MobyException;
import java.io.*;

/**	
 * A convenience class generally intended for the transmission of 
 * binary data that will be Base64 encoded to fit in the MOBY XML envelope.
 * It will also decode Base64 and UUEncoded strings.
 *
 * All members of the object can be accessed as they normally would be in 
 * a MobyDataComposite (including the Base64 representation of the bytes in the "contents" member), 
 * but the getBytes() and getObject() method, for
 * convenience, return the bytes encoded by the "contents" member of the class.
 *
 * Note: changing the "contents" member's underlying StringBuffer changes the byte output!
 *
 * This class could be used to handle a JPEG's raw data for example.
 */

public class MobyDataBytes extends MobyDataComposite{

    public static final String BASE64_DATATYPE = "text-base64"; 
    public static final String ENCODED_MEMBER_NAME = "content"; 

    /** How much is read from a input stream (e.g. file) at once */
    public static final int BYTE_READ_SIZE = 4096;  

    /**
     * Construct the object using a DOM fragment.
     *
     * @throws IllegalArgumentException if the element is not a text-base64 tag, or doesn't inherit from it
     */
    public MobyDataBytes(org.w3c.dom.Element element) throws MobyException{
	this(element, null);
    }

    public MobyDataBytes(org.w3c.dom.Element element, Registry registry) throws MobyException{
	super(MobyDataType.getDataType(BASE64_DATATYPE, registry), getName(element));
	setId(getId(element));
	addNamespace(getNamespace(element, registry));
	
	MobyDataType inputDataType = MobyDataType.getDataType(element.getLocalName(), registry);
	if(!inputDataType.inheritsFrom(MobyDataType.getDataType(BASE64_DATATYPE, registry))){
	    throw new MobyException("The given tag ("+ element.getLocalName() +
				    ") does not inherit from " + BASE64_DATATYPE +
				    " in the MOBY Object Class Ontology, cannot " +
				    "consider its contents as binary data");
	}
	setDataType(inputDataType);

	// Now, we know we have one field that represents the bytes, 
	// and there may be others which we will treat as regular composite members
	populateMembersFromDOM(element, registry);

	MobyDataObject contents = get(ENCODED_MEMBER_NAME);
	if(contents == null){
	    throw new MobyException("The encoded binary member (" + ENCODED_MEMBER_NAME + 
				    ") of the given element (" +
				    element.getLocalName()+
				    ") was missing, but must exist to be a proper " +
				    BASE64_DATATYPE + " object");
	}
	if(!(contents instanceof MobyDataString)){
	    throw new MobyException("The encoded binary member (" + ENCODED_MEMBER_NAME + 
				    ") of the given element (" +
				    element.getLocalName()+
				    ") was not a String primitive as expected");
	}
    }

    /**
     * C-tor to use when you have binary data to encode
     * 
     * @throws MobyException if the data provided is null, or could not be encoded(?!)
     */
    public MobyDataBytes(String name, byte[] data) throws MobyException{
	this(name, data, (Registry) null);
    }

    public MobyDataBytes(String name, byte[] data, Registry registry) throws MobyException{
	super(MobyDataType.getDataType(BASE64_DATATYPE, registry), name);
	storeBytes(data);
    }

    /**
     * C-tor to use when you have binary data to encode
     */
    public MobyDataBytes(String name, byte[] data, MobyDataType inputDataType) throws MobyException{
	this(name, data);
	if(!inputDataType.inheritsFrom(MobyDataType.getDataType(BASE64_DATATYPE, inputDataType.getRegistry()))){
	    throw new MobyException("The given data type ("+ inputDataType.getName() +
				    ") does not inherit from " + BASE64_DATATYPE +
				    " in the MOBY Object Class Ontology, cannot " +
				    "consider the data given as binary data");
	}
	setDataType(inputDataType);
    }

    /**
     * C-tor to use when you have received text-encoded binary.
     *
     * Currently Base64 encoding is acceptable, UU Decoding has yet to be implemented..
     */
    public MobyDataBytes(String name, CharSequence data){
	this(name, data, (Registry) null);
    }

    public MobyDataBytes(String name, CharSequence data, Registry registry){
	super(MobyDataType.getDataType(BASE64_DATATYPE, registry), name);
	setDataType(MobyDataType.getDataType(BASE64_DATATYPE, registry));
	// TODO: add check that the data is valid Base64 format!
	put(ENCODED_MEMBER_NAME, new MobyDataString(ENCODED_MEMBER_NAME, data, registry));
    }

    /**
     * C-tor to use when you have received text-encoded binary.
     *
     * Currently Base64 encoding is acceptable, UU Decoding has yet to be implemented..
     */
    public MobyDataBytes(String name, CharSequence data, MobyDataType inputDataType) throws MobyException{
	this(name, data);
	if(!inputDataType.inheritsFrom(MobyDataType.getDataType(BASE64_DATATYPE, inputDataType.getRegistry()))){
	    throw new MobyException("The given data type ("+ inputDataType.getName() +
				    ") does not inherit from " + BASE64_DATATYPE +
				    " in the MOBY Object Class Ontology, cannot " +
				    "consider the data given as binary data");
	}
	setDataType(inputDataType);
    }

    /**
     * C-tor to use when you want to encode a resource, such as an image file.
     *
     * @param resourceURL the URL of the resource to encode, such as "file:..." or "http:..."
     */
    public MobyDataBytes(String name, java.net.URL resourceURL) throws MobyException, IOException{
	this(name, resourceURL, (Registry) null);
    }

    public MobyDataBytes(String name, java.net.URL resourceURL, Registry registry) throws MobyException, IOException{
	super(MobyDataType.getDataType(BASE64_DATATYPE, registry), name);
	if(resourceURL == null){
	    return;
	}
	InputStream inputStream = resourceURL.openStream();
	storeBytes(readStream(inputStream));
    }

    protected void storeBytes(byte[] bytes) throws MobyException{
	if(bytes == null){
	    throw new MobyException("The given byte data to Base64 encode was null");  
	}
	String contents = new String(org.biomoby.client.util.Base64Coder.encode(bytes));
	if(contents == null){
	     throw new MobyException("The byte contents could not be encoded in Base64 format");
	}
	put(ENCODED_MEMBER_NAME, new MobyDataString(ENCODED_MEMBER_NAME, contents));
    }

    /**
     * C-tor to use when you want to encode a resource, such as an image file.
     *
     * @param resourceURL the URL of the resource to encode, such as "file:..." or "http:..."
     */
    public MobyDataBytes(String name, java.net.URL resourceURL, MobyDataType inputDataType) throws IOException, MobyException{
	this(name, resourceURL);
	if(!inputDataType.inheritsFrom(MobyDataType.getDataType(BASE64_DATATYPE, inputDataType.getRegistry()))){
	    throw new MobyException("The given data type ("+ inputDataType.getName() +
				    ") does not inherit from " + BASE64_DATATYPE +
				    " in the MOBY Object Class Ontology, cannot " +
				    "consider the data at the given URL as binary data");
	}
	setDataType(inputDataType);
    }

    protected byte[] readStream(InputStream in) throws IOException {
	byte buffer[] = new byte[BYTE_READ_SIZE];
	ByteArrayOutputStream byteStream = new ByteArrayOutputStream(buffer.length);
	
	while (true) {
	    int r = in.read(buffer);
	    if (r<=0) {
		break;
	    }
	    byteStream.write(buffer, 0, r);
	}
	return byteStream.toByteArray();
    }


    /**
     * Convenience method that returns an InputStream for use in image loaders,
     * file savers, etc.
     */
    public java.io.InputStream getInputStream(){
	return new java.io.ByteArrayInputStream(getBytes());
    }
    
    /**
     * @return byte[] representing the (mutable) underlying data in this object
     */
    public Object getObject(){
	return getBytes();
    }

    /**
     * The same as getObject, but doesn't require a cast of the result
     */
    public byte[] getBytes(){
	return org.biomoby.client.util.Base64Coder.decode(((MobyDataString) get(ENCODED_MEMBER_NAME)).getValue());
    }    
}
