/*
 * Copyright 2001-2004 The Apache Software Foundation.
 * 
 * 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.
 */

/**
 * @author Rick Rineholt
 */

package org.apache.axis.attachments;


import org.apache.axis.components.logger.LogFactory;
import org.apache.axis.utils.Messages;
import org.apache.commons.logging.Log;

import javax.activation.DataHandler;
import javax.activation.DataSource;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.util.StringTokenizer;


/**
 * This class is a single part for DIME mulitpart message.
<pre>
 DIME 1.0 format
 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 | VERSION |B|E|C| TYPE_T| OPT_T |         OPTIONS_LENGTH        |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |          ID_LENGTH          |             TYPE_LENGTH         |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                          DATA_LENGTH                          |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                                                               /
 /                       OPTIONS + PADDING                       /
 /                                                               |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                                                               /
 /                        ID + PADDING                           /
 /                                                               |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                                                               /
 /                        TYPE + PADDING                         /
 /                                                               |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                                                               /
 /                        DATA + PADDING                         /
 /                                                               |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 </pre>
 */

/**
 * Holds one attachment DIME part.
 */
public class DimeBodyPart {
    protected static Log log =
        LogFactory.getLog(DimeBodyPart.class.getName());

    protected Object data = null;
    protected DimeTypeNameFormat dtnf = null;
    protected byte[] type = null;
    protected byte[] id = null;
    static final byte POSITION_FIRST = (byte) 0x04;
    static final  byte POSITION_LAST = (byte) 0x02;
    private static final byte CHUNK = 0x01; //Means set the chunk bit
    private static final byte CHUNK_NEXT = 0x2; //Means this was part of a CHUNK
    private static final byte ONLY_CHUNK = -1;//Means only one chunk was sent
    private static final byte LAST_CHUNK = (byte)0;//Means many chunks were sent    
    private static int MAX_TYPE_LENGTH = (1 << 16) - 1;
    private static int MAX_ID_LENGTH = (1 << 16) - 1;

    static final long MAX_DWORD = 0xffffffffL;

    // fixme: don't use? is this for inheritance only? I can't find any
    //  classes that extend this
    protected DimeBodyPart() {} //do not use.

    /**
     * Create a DIME Attachment Part.
     * @param data a byte array containing the data as the attachment.
     * @param format the type format for the data.
     * @param type the type of the data
     * @param id  the ID for the DIME part.
     *
     */
    public DimeBodyPart(byte[] data, DimeTypeNameFormat format,
      String type, String id) {
        System.arraycopy(data, 0, this.data = new byte[ data.length], 0, data.length);
        this.dtnf = format;
        this.type = type.getBytes();
        if (this.type.length > MAX_TYPE_LENGTH)
            throw new IllegalArgumentException(Messages.getMessage
                    ("attach.dimetypeexceedsmax",
                    "" + this.type.length, "" + MAX_TYPE_LENGTH));
        this.id = id.getBytes();
        if (this.id.length > MAX_ID_LENGTH)
            throw new IllegalArgumentException(
                    Messages.getMessage("attach.dimelengthexceedsmax", "" + this.id.length,
                        "" + MAX_ID_LENGTH));
    }

    /**
     * Create a DIME Attachment Part.
     * @param dh the data for the attachment as a JAF datahadler.
     * @param format the type format for the data.
     * @param type the type of the data
     * @param id  the ID for the DIME part.
     *
     */
    public DimeBodyPart(DataHandler dh,
    DimeTypeNameFormat format, String type, String id) {
        this.data = dh;
        this.dtnf = format;
        if (type == null || type.length() == 0)
            type = "application/octet-stream";
        this.type = type.getBytes();
        if (this.type.length > MAX_TYPE_LENGTH)
            throw new IllegalArgumentException(Messages.getMessage(
                        "attach.dimetypeexceedsmax",
                        "" + this.type.length, "" + MAX_TYPE_LENGTH));
        this.id = id.getBytes();
        if (this.id.length > MAX_ID_LENGTH)
            throw new IllegalArgumentException(Messages.getMessage(
            "attach.dimelengthexceedsmax",
            "" + this.id.length, "" + MAX_ID_LENGTH));
    }

    /**
     * Create a DIME Attachment Part.
     * @param dh the data for the attachment as a JAF datahadler.
     *    The type and foramt is derived from the DataHandler.
     * @param id  the ID for the DIME part.
     *
     */
    public DimeBodyPart(DataHandler dh, String id) {
        this(dh, DimeTypeNameFormat.MIME, dh.getContentType(), id);

        String ct = dh.getContentType();

        if (ct != null) {
            ct = ct.trim();
            if (ct.toLowerCase().startsWith("application/uri")) {
                StringTokenizer st = new StringTokenizer(ct, " \t;");
                String t = st.nextToken(" \t;");

                if (t.equalsIgnoreCase("application/uri")) {
                    for (; st.hasMoreTokens();) {
                        t = st.nextToken(" \t;");
                        if (t.equalsIgnoreCase("uri")) {
                            t = st.nextToken("=");
                            if (t != null) {
                                t = t.trim();
                                if (t.startsWith("\"")) t =
                                  t.substring(1);

                                if (t.endsWith("\"")) t =
                                  t.substring(0, t.length() - 1);
                                this.type = t.getBytes();
                                this.dtnf = DimeTypeNameFormat.URI;
                            }
                            return;
                        } else if (t.equalsIgnoreCase("uri=")) {
                            t = st.nextToken(" \t;");
                            if (null != t && t.length() != 0) {
                                t = t.trim();
                                if (t.startsWith("\"")) t=
                                  t.substring(1);
                                if (t.endsWith("\"")) t=
                                  t.substring(0, t.length() - 1);
                                this.type = t.getBytes();
                                this.dtnf = DimeTypeNameFormat.URI;
                                return;
                            }
                        } else if (t.toLowerCase().startsWith("uri=")) {
                            if (-1 != t.indexOf('=')) {
                                t = t.substring(t.indexOf('=')).trim();
                                if (t.length() != 0) {
                                    t = t.trim();
                                    if (t.startsWith("\"")) t =
                                      t.substring(1);

                                    if (t.endsWith("\""))
                                     t = t.substring(0, t.length() - 1);
                                    this.type = t.getBytes();
                                    this.dtnf = DimeTypeNameFormat.URI;
                                    return;

                                }
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * Write to stream the data using maxchunk for the largest junk.
     *
     * @param os        the <code>OutputStream</code> to write to
     * @param position  the position to write
     * @param maxchunk  the maximum length of any one chunk
     * @throws IOException if there was a problem writing data to the stream
     */
    void write(java.io.OutputStream os, byte position, long maxchunk)
      throws java.io.IOException {
        if (maxchunk < 1) throw new IllegalArgumentException(
                    Messages.getMessage("attach.dimeMaxChunkSize0", "" + maxchunk));
        if (maxchunk > MAX_DWORD) throw new IllegalArgumentException(
                    Messages.getMessage("attach.dimeMaxChunkSize1", "" + maxchunk));
        if (data instanceof byte[]) {
            send(os, position, (byte[]) data, maxchunk);
        } else if (data instanceof DynamicContentDataHandler) {
            send(os, position, (DynamicContentDataHandler) data, maxchunk);
        } else if (data instanceof DataHandler) {
            DataSource source = ((DataHandler)data).getDataSource();
            DynamicContentDataHandler dh2 = new DynamicContentDataHandler(source);
            send(os, position, dh2, maxchunk);
        }
    }

    /**
     * Write to stream the data using the default largest chunk size.
     *
     * @param os  the <code>OutputStream</code> to write to
     * @param position  the position to write
     * @throws IOException if there was a problem writing data to the stream
     */
    void write(java.io.OutputStream os, byte position)
      throws java.io.IOException {
        write(os, position, MAX_DWORD);
    }

    private static final byte[] pad = new byte[4];

    void send(java.io.OutputStream os, byte position, byte[] data,
        final long maxchunk)throws java.io.IOException {
        send(os, position, data, 0, data.length, maxchunk);
    }

    void send(java.io.OutputStream os, byte position, byte[] data,
        int offset, final int length, final long maxchunk)
        throws java.io.IOException {

        byte chunknext = 0;

        do {
            int sendlength = (int) Math.min(maxchunk, length - offset);

            sendChunk(os, position, data, offset, sendlength, (byte)
                ((sendlength < (length - offset) ? CHUNK : 0)
                 | chunknext));
            offset += sendlength;
            chunknext = CHUNK_NEXT;
        }
        while (offset < length);
    }

    void send(java.io.OutputStream os, byte position, DataHandler dh,
        final long maxchunk) throws java.io.IOException {
        java.io.InputStream in = null;
        try {
            long dataSize = getDataSize();
            in = dh.getInputStream();
            byte[] readbuf = new byte[64 * 1024];
            int bytesread;

            sendHeader(os, position, dataSize, (byte) 0);
            long totalsent = 0;

            do {
                bytesread = in.read(readbuf);
                if (bytesread > 0) {
                    os.write(readbuf, 0, bytesread);
                    totalsent += bytesread;
                }
            }
            while (bytesread > -1);
            os.write(pad, 0, dimePadding(totalsent));
        }
        finally {
            if (in != null) {
                try {
                  in.close();
                }
                catch (IOException e) {
                  // ignore
                }
            }
        }
    }
    
    /**
     * Special case for dynamically generated content. 
     * maxchunk is currently ignored since the default is 2GB.
     * The chunk size is retrieved from the DynamicContentDataHandler
     * 
     * @param os
     * @param position
     * @param dh
     * @param maxchunk
     * @throws java.io.IOException
     */
    void send(java.io.OutputStream os, byte position, DynamicContentDataHandler dh,
            final long maxchunk)
            throws java.io.IOException {
    	
    		BufferedInputStream in = new BufferedInputStream(dh.getInputStream());
    		
    		final int myChunkSize = dh.getChunkSize();
    		
    		byte[] buffer1 = new byte[myChunkSize]; 
    		byte[] buffer2 = new byte[myChunkSize]; 
    		
    		int bytesRead1 = 0 , bytesRead2 = 0;

    		bytesRead1 = in.read(buffer1);
    		
    		if(bytesRead1 < 0) {
                sendHeader(os, position, 0, ONLY_CHUNK);
                os.write(pad, 0, dimePadding(0));
                return;
    		}
    		byte chunkbyte = CHUNK;
    		do {
    			bytesRead2 = in.read(buffer2);
    			
    			if(bytesRead2 < 0) {
    				//last record...do not set the chunk bit.
    				//buffer1 contains the last chunked record!!
    			   
    			    //Need to distinguish if this is the first 
    			    //chunk to ensure the TYPE and ID are sent
    			    if ( chunkbyte == CHUNK ){
    			        chunkbyte = ONLY_CHUNK;
    			    } else {
    			        chunkbyte = LAST_CHUNK;
    			    }
    				sendChunk(os, position, buffer1, 0, bytesRead1, chunkbyte);
    				break;
    			}
    			
    			sendChunk(os, position, buffer1, 0, bytesRead1, chunkbyte);
    			//set chunk byte to next chunk flag to avoid
    			//sending TYPE and ID on subsequent chunks
    			chunkbyte = CHUNK_NEXT;
    			//now that we have written out buffer1, copy buffer2 into to buffer1
    			System.arraycopy(buffer2,0,buffer1,0,myChunkSize);
    			bytesRead1 = bytesRead2;
    			
    		}while(bytesRead2 > 0);
    }

    protected void sendChunk(java.io.OutputStream os,
    final byte position,
        byte[] data, byte chunk) throws java.io.IOException {

        sendChunk(os, position, data, 0, data.length, chunk);
    }

    protected void sendChunk(java.io.OutputStream os,
        final byte position, byte[] data, int offset, int length,
        byte chunk) throws java.io.IOException {

        sendHeader(os, position, length, chunk);
        os.write(data, offset, length);
        os.write(pad, 0, dimePadding(length));
    }

    static final  byte CURRENT_OPT_T = (byte) 0;

    protected void sendHeader(java.io.OutputStream os,
    final byte position,
        long length, byte chunk) throws java.io.IOException {
        byte[] fixedHeader = new byte[12];
        
        //If first chunk then send TYPE and ID 
        boolean isFirstChunk = ((chunk == CHUNK) || (chunk == ONLY_CHUNK));
        //If chunk is ONLY_NEXT then 
        //reset to CHUNK so CF is set.
        //If chunk is ONLY_CHUNK (first and last chunk)  
        //then do not set CF since this is the only chunk
        if ( chunk == CHUNK_NEXT ){
            chunk = CHUNK;
        } else if ( chunk  == ONLY_CHUNK ){
            chunk = LAST_CHUNK;
        }
                
        //VERSION
        fixedHeader[0] = (byte)((DimeMultiPart.CURRENT_VERSION << 3) & 0xf8);

        // B, E
        fixedHeader[0] |= (byte) ((position & (byte) 0x6)
         & ((chunk & CHUNK) != 0 ? ~POSITION_LAST : ~0) &
                    ((chunk & CHUNK_NEXT) != 0 ? ~POSITION_FIRST : ~0));
        fixedHeader[0] |= (chunk & CHUNK);
        
        boolean MB = 0 != (0x4 & fixedHeader[0]);
        //TYPE_T
        if ( MB || isFirstChunk ){ //If this is a follow on chunk dont send id again.
            fixedHeader[1] = (byte) ((dtnf.toByte() << 4) & 0xf0);
        } else {
        	fixedHeader[1] = (byte) 0x00;
        }
        
        //OPT_T
        fixedHeader[1] |= (byte) (CURRENT_OPT_T & 0xf);

        //OPTION_LENGTH
        fixedHeader[2] = (byte) 0;
        fixedHeader[3] = (byte) 0;

        //ID_LENGTH
        if  ( (MB || isFirstChunk) && (id != null && id.length > 0))   { //If this is a follow on chunk dont send id again.
            fixedHeader[4] = (byte) ((id.length >>> 8) & 0xff);
            fixedHeader[5] = (byte) ((id.length) & 0xff);
        } else {
        	fixedHeader[4] = (byte) 0;
            fixedHeader[5] = (byte) 0;
        }

        //TYPE_LENGTH
        if (  MB || isFirstChunk )  {
            fixedHeader[6] = (byte) ((type.length >>> 8) & 0xff);
            fixedHeader[7] = (byte) ((type.length) & 0xff);
        } else {
        	fixedHeader[6] = (byte) 0;
            fixedHeader[7] = (byte) 0;
        }

        //DATA_LENGTH
        fixedHeader[8] = (byte) ((length >>> 24) & 0xff);
        fixedHeader[9] = (byte) ((length >>> 16) & 0xff);
        fixedHeader[10] = (byte) ((length >>> 8) & 0xff);
        fixedHeader[11] = (byte) (length & 0xff);

        os.write(fixedHeader);

        //OPTIONS + PADDING
        // (NONE)

        //ID + PADDING
        if ( (MB || isFirstChunk) && (id != null && id.length > 0))  {
            os.write(id);
            os.write(pad, 0, dimePadding(id.length));
        }

        //TYPE + PADDING
        if ( MB || isFirstChunk )  {
            os.write(type);
            os.write(pad, 0, dimePadding(type.length));
        }
    }

    static final int dimePadding(long l) {
        return (int) ((4L - (l & 0x3L)) & 0x03L);
    }

    long getTransmissionSize(long chunkSize) {
        long size = 0;
        size += id.length;
        size += dimePadding(id.length);
        size += type.length;
        size += dimePadding(type.length);
        //no options.
        long dataSize = getDataSize();

        if(0 == dataSize){
            size+=12; //header size.
        }else{

            long fullChunks = dataSize / chunkSize;
            long lastChunkSize = dataSize % chunkSize;

            if (0 != lastChunkSize) size += 12; //12 bytes for fixed header
            size += 12 * fullChunks; //add additional header size for each chunk.
            size += fullChunks * dimePadding(chunkSize);
            size += dimePadding(lastChunkSize);
            size += dataSize;
        }
        return size;
    }

    long getTransmissionSize() {
        return getTransmissionSize(MAX_DWORD);
    }

    protected long getDataSize() {
        if (data instanceof byte[]) return ((byte[]) (data)).length;
        if (data instanceof DataHandler)
          return getDataSize((DataHandler) data);
        return -1;
    }

    protected long getDataSize(DataHandler dh) {
        long dataSize = -1L;

        try {
            DataSource ds = dh.getDataSource();

            //Do files our selfs since this is costly to read in. Ask the file system.
            // This is 90% of the use of attachments.
            if (ds instanceof javax.activation.FileDataSource) {
                javax.activation.FileDataSource fdh =
                    (javax.activation.FileDataSource) ds;
                java.io.File df = fdh.getFile();

                if (!df.exists()) {
                    throw new RuntimeException(
                            Messages.getMessage("noFile",
                                df.getAbsolutePath()));
                }
                dataSize = df.length();
            } else {
                dataSize = 0;
                java.io.InputStream in = ds.getInputStream();
                byte[] readbuf = new byte[64 * 1024];
                int bytesread;

                do {
                    bytesread = in.read(readbuf);
                    if (bytesread > 0) dataSize += bytesread;
                }
                while (bytesread > -1);

                if (in.markSupported()) {
                    //Leave the stream open for future reading
                    // and reset the stream pointer to the first byte
                    in.reset();
                } else {
                    //FIXME: bug http://nagoya.apache.org/jira/secure/ViewIssue.jspa?key=AXIS-1126
                    //if we close this then how can we read the file? eh?
                    in.close();
                }
            }
        } catch (Exception e) {
            //TODO: why are exceptions swallowed here?
            log.error(Messages.getMessage("exception00"), e);
        }
        return dataSize;
    }
}
