/* Distributed under the Apache License, Version 2.0.
   See accompanying NOTICE file for details.*/

/**
 * 
 */
package com.kitware.physiology.utilities;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;

import org.xml.sax.SAXException;


/**
 * @author abray
 *
 */
public class JAXBSerializer
{
  protected JAXBContext    jbctxt         = null;
  protected Marshaller     marshaller     = null;
  protected Unmarshaller   unmarshaller   = null;
  protected final String   FILE_EXTENSION = "xml";
  protected SchemaFactory  sf             = null;
  protected String         bindPackage;
  protected Object         objFac;

  protected JAXBElement<?> out;
  protected Class<?>       spec[] = new Class[1];
  protected Object         args[] = new Object[1];
  
  public JAXBSerializer(String xsdDirectory, String xsdFile, String bindPackage, Object objectFactory) throws JAXBException, SAXException
  {
    File schemaFile = new File(xsdDirectory+"/"+xsdFile).getAbsoluteFile();
    if(!schemaFile.exists())
    {
      Log.error("Unable to find schema at "+xsdDirectory+"/"+xsdFile);
      return;
    }
    this.bindPackage = bindPackage;
    this.objFac   = objectFactory;
    
    this.jbctxt = JAXBContext.newInstance(this.bindPackage,this.objFac.getClass().getClassLoader());
    this.sf=SchemaFactory.newInstance(
      javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI);
    this.marshaller = this.jbctxt.createMarshaller();
    this.marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
    this.marshaller.setProperty(Marshaller.JAXB_SCHEMA_LOCATION, "");
    this.unmarshaller = this.jbctxt.createUnmarshaller();
    // Set up validation
    Schema schema = sf.newSchema(schemaFile);
    this.unmarshaller.setSchema(schema);
    this.marshaller.setSchema(schema);
  }
  
  public synchronized String getBindPackage()
  {
    return this.bindPackage;
  }

  /**
   * Serializes a JAXB object into an XML string
   *
   * @param data JAXB object to serialize
   *
   * @return XML string representing JAXB object
   */
  public synchronized String serialize(Object data)
  {
    if(data==null)
      throw new RuntimeException("JAXBSerializer recieved null jaxb data object for serialization.");

    String str;
    ByteArrayOutputStream stream = new ByteArrayOutputStream();
    try
    {
      String rootName="";
      try
      {
        args[0] = data;
        spec[0] = data.getClass();
        rootName = data.getClass().getName();
        rootName = rootName.substring(rootName.lastIndexOf('.')+1,rootName.length()-4);
        Method method = objFac.getClass().getMethod("create"+rootName, spec);
        out = (JAXBElement<?>)method.invoke(objFac, args);
        marshaller.marshal(out, stream);
        str = stream.toString();
        return str;
      }
      catch(JAXBException e)
      {
        Log.error("Serialization error: ObjectFactory failed to find and/or call method create"+rootName+"Data to serialize to",e);
        return null;
      } 
    }
    catch (Exception e)
    {
      Log.error("Error Writing XML: ",e);
      if(stream.size()>0)
        Log.error("Error Writing XML: " + stream);
      e.printStackTrace();

      return null;
    }
  }

  /**
   * Deserializes an XML string into a JAXB object
   *
   * @param xml XML string to deserialize
   *
   * @return JAXB object representing string
   */
  public synchronized Object serialize(String xml)
  {
    try
    {
      ByteArrayInputStream stream = new ByteArrayInputStream(xml.getBytes());      
      JAXBElement<?> data = null;
      synchronized(unmarshaller)
      {
        data = (JAXBElement<?>)unmarshaller.unmarshal(stream);
      }
      return data.getValue();
    }
    catch (Exception e)
    {
      Log.error("Error Reading XML ",e);
      Log.error("XML"+xml);
      e.printStackTrace();

      return null;
    }
  }

  /**
   * Serialized a jaxb object to a file
   * @param fileName
   * @param data
   * @return
   */
  public synchronized void writeFile(String fileName, Object data)
  {
    if(data==null)
      throw new RuntimeException("JAXBSerializer recieved null jaxb data object for serialization.");

    if(data instanceof String)
    {
      FileUtils.writeFile(fileName, (String)data);
      return;
    }

    try
    {
      String rootName="";
      try
      {
        args[0] = data;
        spec[0] = data.getClass();
        rootName = data.getClass().getName();
        rootName = rootName.substring(rootName.lastIndexOf('.')+1,rootName.indexOf("Data"));
        Method method = objFac.getClass().getMethod("create"+rootName, spec);
        out = (JAXBElement<?>)method.invoke(objFac, args);
        FileOutputStream stream = new FileOutputStream(fileName);
        marshaller.marshal(out, stream);
        stream.close();
      }
      catch(JAXBException e)
      {
        Log.error("Serialization error: ObjectFactory failed to find and/or call method create"+rootName+"Data to serialize to",e);
      } 
    }
    catch (Exception e)
    {
      Log.error("Error Writing JAXB File: ",e);
    }
  }

  /**
   * Read a file name into a JAXB class object
   * @param fileName
   * @return
   */
  public synchronized Object readFile(String fileName)
  {
    File f = new File(fileName);
    if(f.exists())
      return readFile(f);    
    Log.error(fileName+" does not exist");
    return null;
  }

  /**
   * Read a file name into a JAXB class object
   * @param fileName
   * @return
   */
  public synchronized Object readFile(File file)
  {
    try
    {
      FileInputStream fin = new FileInputStream(file);
      JAXBElement<?> data = null;
      synchronized(unmarshaller)
      {
        data = (JAXBElement<?>)unmarshaller.unmarshal(fin);
      }
      fin.close();
      return data.getValue();
    }
    catch (Exception e)
    {
      Log.error("Error Reading XML File "+file,e);
      return null;
    }
  }

  public synchronized Object binaryDecode(String data)
  {
    try
    {
      if(data==null||data.isEmpty())
        return null;
      byte[] bytes = Base64.decode(data);
      return binaryDecode(bytes);
    }
    catch(Exception ex)
    {
      Log.error("Unable to read binary encoding ",ex);
      return null;
    }
  }

  public synchronized Object binaryDecode(byte[] bytes)
  {
    try
    {
      if(bytes==null)
        return null;
      ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
      Object o = ois.readObject();
      ois.close();
      return o;
    }
    catch(Exception ex)
    {
      Log.error("Unable to read binary encoding ",ex);
      return null;
    }
  }

  public synchronized Object binaryDecodeDecompress(byte[] bytes)
  {
    try
    {
      if(bytes==null)
        return null;
      GZIPInputStream gzipin = new GZIPInputStream(new ByteArrayInputStream(bytes));
      ObjectInputStream ois = new ObjectInputStream(gzipin);
      Object o = ois.readObject();
      ois.close();
      return o;
    }
    catch(Exception ex)
    {
      Log.error("Unable to read binary encoding ",ex);
      return null;
    }
  }

  public synchronized String binaryEncodeToString(Object o)
  {
    try
    {
      return Base64.encode(binaryEncodeToBytes(o));
    }
    catch(Exception ex)
    {
      Log.error("Unable to binary encode object: "+o.getClass().getName(),ex);
      return null;
    }
  }

  public synchronized byte[] binaryEncodeToBytes(Object o)
  {
    try
    {
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      ObjectOutputStream oos = new ObjectOutputStream(baos);
      oos.writeObject(o);
      oos.close();
      return baos.toByteArray();
    }
    catch(Exception ex)
    {
      Log.error("Unable to binary encode object: "+o.getClass().getName(),ex);
      return null;
    }
  }

  public synchronized byte[] binaryEncodeCompressToBytes(Object o)
  {
    try
    {
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      GZIPOutputStream gzipout = new GZIPOutputStream(baos);
      ObjectOutputStream oos = new ObjectOutputStream(gzipout);
      oos.writeObject(o);
      oos.close();
      gzipout.finish();
      return baos.toByteArray();
    }
    catch(Exception ex)
    {
      Log.error("Unable to binary encode object: "+o.getClass().getName(),ex);
      return null;
    }
  }

  /**
   * Verifies that filename contains right extension
   *
   * @param fileName File name to check
   *
   * @return if it does or not
   */
  public synchronized boolean hasVaildFileExtension(String fileName)
  {
    if (!fileName.toLowerCase().endsWith(FILE_EXTENSION.toLowerCase()))
    {
      return false;
    }
    return true;
  }
}
