/*
 * This software is distributed under following license based on modified BSD
 * style license.
 * ----------------------------------------------------------------------
 * 
 * Copyright 2009 The Nimbus2 Project. All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer. 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE NIMBUS PROJECT ``AS IS'' AND ANY EXPRESS
 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
 * NO EVENT SHALL THE NIMBUS PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * The views and conclusions contained in the software and documentation are
 * those of the authors and should not be interpreted as representing official
 * policies, either expressed or implied, of the Nimbus2 Project.
 */
package jp.ossc.nimbus.service.io;

import java.io.*;
import java.lang.reflect.*;
import java.util.zip.*;

import jp.ossc.nimbus.core.*;

/**
 * 直列化可能オブジェクト直列化サービス。<p>
 * 
 * @author M.Takata
 */
public class SerializableExternalizerService<T> extends ServiceBase
 implements SerializableExternalizerServiceMBean, Externalizer<T>, Serializable{
    
    private static final long serialVersionUID = -2894857230847782064L;
    
    protected int compressMode = COMPRESS_MODE_NONE;
    protected int compressLevel = Deflater.DEFAULT_COMPRESSION;
    protected int compressMethod = ZipOutputStream.DEFLATED;
    protected int compressThreshold = -1;
    protected int bufferSize;
    protected Class<? extends ObjectOutput> objectOutputClass;
    protected Constructor<? extends ObjectOutput> objectOutputConstructor;
    protected Class<? extends ObjectInput> objectInputClass;
    protected Constructor<? extends ObjectInput> objectInputConstructor;
    
    public void setCompressMode(int mode){
        compressMode = mode;
    }
    public int getCompressMode(){
        return compressMode;
    }
    
    public void setCompressLevel(int level){
        compressLevel = level;
    }
    public int getCompressLevel(){
        return compressLevel;
    }
    
    public void setCompressMethod(int method){
        compressMethod = method;
    }
    public int getCompressMethod(){
        return compressMethod;
    }
    
    public void setCompressThreshold(int threshold){
        compressThreshold = threshold;
    }
    public int getCompressThreshold(){
        return compressThreshold;
    }
    
    public void setBufferSize(int size){
        bufferSize = size;
    }
    public int getBufferSize(){
        return bufferSize;
    }
    
    public void setObjectOutputClass(Class<? extends ObjectOutput> clazz){
        if(clazz == null){
            objectOutputConstructor = null;
            objectOutputClass = null;
        }else{
            try{
                objectOutputConstructor = (Constructor<? extends ObjectOutput>)clazz.getConstructor(new Class[]{OutputStream.class});
            }catch(NoSuchMethodException e){
                throw new IllegalArgumentException("No support ObjectOutputClass." + clazz);
            }
            objectOutputClass = clazz;
        }
    }
    public Class<? extends ObjectOutput> getObjectOutputClass(){
        return objectOutputClass;
    }
    
    public void setObjectInputClass(Class<? extends ObjectInput> clazz){
        if(clazz == null){
            objectInputConstructor = null;
            objectInputClass = null;
        }else{
            try{
                objectInputConstructor = (Constructor<? extends ObjectInput>)clazz.getConstructor(new Class[]{InputStream.class});
            }catch(NoSuchMethodException e){
                throw new IllegalArgumentException("No support ObjectInputClass." + clazz);
            }
            objectInputClass = clazz;
        }
    }
    public Class<? extends ObjectInput> getObjectInputClass(){
        return objectInputClass;
    }
    
    public void writeExternal(T obj, OutputStream out) throws IOException{
        ObjectOutput output = null;
        if(objectOutputClass == null){
            output = new ObjectOutputStream(out);
        }else{
            try{
                output = objectOutputConstructor.newInstance(new Object[]{out});
            }catch(InstantiationException e){
                throw new IOException("ObjectOutput can not instanciate." + objectOutputClass.getName() + " cause " + e.toString());
            }catch(IllegalAccessException e){
                throw new IOException("ObjectOutput can not instanciate." + objectOutputClass.getName() + " cause " + e.toString());
            }catch(InvocationTargetException e){
                throw new IOException("ObjectOutput can not instanciate." + objectOutputClass.getName() + " cause " + e.getTargetException().toString());
            }
        }
        writeExternal(obj, output);
    }
    
    public void writeExternal(T obj, ObjectOutput out) throws IOException{
        try{
            if(compressMode == COMPRESS_MODE_NONE){
                writeInternal(obj, out);
                return;
            }
            if(obj == null){
                out.writeBoolean(false);
                writeInternal(obj, out);
                return;
            }
            final ByteArrayOutputStream baos
                = bufferSize > 0 ? new ByteArrayOutputStream(bufferSize)
                    : new ByteArrayOutputStream();
            final ObjectOutputStream oos = new ObjectOutputStream(baos);
            writeInternal(obj, oos);
            oos.flush();
            byte[] bytes = baos.toByteArray();
            if(compressThreshold >= bytes.length){
                out.writeBoolean(false);
                writeInternal(obj, out);
                return;
            }
            baos.reset();
            DeflaterOutputStream dos = null;
            Deflater deflater = null;
            switch(compressMode){
            case COMPRESS_MODE_ZLIB:
                deflater = new Deflater(compressLevel);
                dos = bufferSize > 0 ? new DeflaterOutputStream(baos, deflater, bufferSize) : new DeflaterOutputStream(baos, deflater);
                break;
            case COMPRESS_MODE_ZIP:
                ZipOutputStream zos = new ZipOutputStream(baos);
                zos.setLevel(compressLevel);
                zos.setMethod(compressMethod);
                zos.putNextEntry(new ZipEntry("a"));
                dos = zos;
                break;
            case COMPRESS_MODE_GZIP:
                dos = bufferSize > 0 ? new GZIPOutputStream(baos, bufferSize) : new GZIPOutputStream(baos);
                break;
            default:
                throw new IOException("Unknown compress mode : " + compressMode);
            }
            try{
                dos.write(bytes, 0, bytes.length);
                if(compressMode == COMPRESS_MODE_ZIP){
                    ((ZipOutputStream)dos).closeEntry();
                }
                dos.finish();
            }finally{
                if(deflater != null){
                    deflater.end();
                }
                try{
                    dos.close();
                }catch(IOException e){}
                baos.close();
            }
            byte[] compressedBytes = baos.toByteArray();
            if(bytes.length <= compressedBytes.length){
                out.writeBoolean(false);
                writeInternal(obj, out);
            }else{
                out.writeBoolean(true);
                out.writeInt(compressedBytes.length);
                out.write(compressedBytes);
            }
        }finally{
            out.flush();
        }
    }
    
    protected void writeInternal(Object obj, ObjectOutput out) throws IOException{
        out.writeObject(obj);
    }
    
    public T readExternal(InputStream in) throws IOException, ClassNotFoundException{
        ObjectInput input = null;
        if(objectInputClass == null){
            input = new ObjectInputStream(in);
        }else{
            try{
                input = objectInputConstructor.newInstance(new Object[]{in});
            }catch(InstantiationException e){
                throw new IOException("ObjectInput can not instanciate." + objectInputClass.getName() + " cause " + e.toString());
            }catch(IllegalAccessException e){
                throw new IOException("ObjectInput can not instanciate." + objectInputClass.getName() + " cause " + e.toString());
            }catch(InvocationTargetException e){
                throw new IOException("ObjectInput can not instanciate." + objectInputClass.getName() + " cause " + e.getTargetException().toString());
            }
        }
        return readExternal(input);
    }
    
    @SuppressWarnings("unchecked")
    public T readExternal(ObjectInput in) throws IOException, ClassNotFoundException{
        if(compressMode == COMPRESS_MODE_NONE){
            return (T)readInternal(in);
        }
        final boolean isCompressed = in.readBoolean();
        if(!isCompressed){
            return (T)readInternal(in);
        }
        final int length = in.readInt();
        final byte[] bytes = new byte[length];
        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int read = 0;
        while((read = in.read(bytes)) != -1){
            baos.write(bytes, 0, read);
        }
        final ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        InputStream is = null;
        Inflater inflater = null;
        switch(compressMode){
        case COMPRESS_MODE_ZLIB:
            inflater = new Inflater();
            is = bufferSize > 0 ? new InflaterInputStream(bais, inflater, bufferSize) : new InflaterInputStream(bais);
            break;
        case COMPRESS_MODE_ZIP:
            is = new ZipInputStream(bais);
            ((ZipInputStream)is).getNextEntry();
            break;
        case COMPRESS_MODE_GZIP:
            is = bufferSize > 0 ? new GZIPInputStream(bais, bufferSize) : new GZIPInputStream(bais);
            break;
        default:
            throw new IOException("Unknown compress mode : " + compressMode);
        }
        final ObjectInputStream ois = new ObjectInputStream(is);
        try{
            return (T)readInternal(ois);
        }finally{
            if(inflater != null){
                inflater.end();
            }
            try{
                is.close();
            }catch(IOException e){}
            bais.close();
        }
    }
    
    protected Object readInternal(ObjectInput in) throws IOException, ClassNotFoundException{
        return in.readObject();
    }
}