/*
 * Copyright (C) 2007 Helge Hess <helge.hess@opengroupware.org>
 * 
 * This file is part of JOPE.
 * 
 * JOPE is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2, or (at your option) any later version.
 * 
 * JOPE is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with JOPE; see the file COPYING. If not, write to the Free Software
 * Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */
package org.opengroupware.jope.foundation;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.net.URL;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * UData
 * <p>
 * Byte array related utility functions.
 */
public class UData extends NSObject {
  protected static Log log = LogFactory.getLog("UData");

  private UData() { } /* do not allow construction */
  
  /**
   * This method reads the whole contents of the given stream into a byte array.
   * After the content is read, the stream is closed.
   * 
   * @param _in - an InputStream to read the content from
   * @return a byte[] array containing the stream contents or null on error
   */
  public static byte[] loadContentFromStream(InputStream _in) {
    if (_in == null) return null;
    
    // TBD: is this necessary? Probably not, we already copy with a bufer
    BufferedInputStream in;
    in = (_in instanceof BufferedInputStream)
      ? (BufferedInputStream)_in
      : new BufferedInputStream(_in);
    
    // TODO: there must be a smarter way to do this ;-)
    byte[] results = new byte[0];
    try {
      int    didRead;
      byte[] buf = new byte[4096];
      
      while ((didRead = in.read(buf)) > 0) {
        byte[] nre = new byte[results.length + didRead];
        System.arraycopy(results, 0 /* start of source */,
                         nre,     0 /* start in dest   */,
                         results.length);
        System.arraycopy(buf, 0              /* start of source */,
                         nre, results.length /* start in dest   */,
                         didRead);
        
        results = nre;
      }
    }
    catch (IOException e) {
      log.warn("failed to read data from stream: " + _in, e);
      return null;
    }
    finally {
      try {
        _in.close();
      }
      catch (IOException e) {
        log.warn("could not close input stream", e);
      }
    }
    
    return results;
  }

  /**
   * This method reads the whole contents of the given object into a byte array.
   * After the content is read, the stream is closed.
   * <p>
   * The object can be:
   * <ul>
   *   <li>an InputStream
   *   <li>a File object
   *   <li>a String treated as a path name
   *   <li>a URL object
   * </ul>
   * 
   * @param _o - an InputStream/File/URL/path to read the content from
   * @return a byte[] array containing the stream contents or null on error
   */
  public static byte[] loadContentFromSource(Object _o) {
    if (_o == null)
      return null;
    
    if (_o instanceof InputStream)
      return loadContentFromStream((InputStream)_o);
    
    if (_o instanceof File) {
      try {
        /* Note: stream is closed by load function */
        FileInputStream is = new FileInputStream((File)_o);
        return loadContentFromStream(is);
      }
      catch (FileNotFoundException e) {
        log.info("could not open file: " + _o);
        return null;
      }
    }
    
    if (_o instanceof String) {
      try {
        /* Note: stream is closed by load function */
        FileInputStream is = new FileInputStream((String)_o);
        return loadContentFromStream(is);
      }
      catch (FileNotFoundException e) {
        log.info("could not open file: " + _o);
        return null;
      }
    }
    
    if (_o instanceof URL) {
      try {
        /* Note: stream is closed by load function */
        return loadContentFromStream(((URL)_o).openStream());
      }
      catch (IOException e) {
        log.info("could not open URL: " + _o, e);
        return null;
      }
    }
    
    log.error("don't know how to load data from object: " + _o.getClass());
    return null;
  }
  
  
  /* writing files */
  
  public static Exception writeToStream(byte[] _data, OutputStream _out) {
    if (_data == null)
      return new NSException("got no data to write ...");
    if (_out == null)
      return new NSException("got no stream to write data to ...");
    
    try {
      _out.write(_data, 0, _data.length);
      _out.flush();
    }
    catch (IOException e) {
      return e;
    }
    
    return null /* everything went fine */;
  }

  public static Exception writeToFile
    (byte[] _data, File _file, boolean _atomically)
  {
    if (_file == null)
      return new NSException("got no File to write data to ...");
    if (_file.isDirectory())
      return new NSException("target File is a directory ...");
    
    OutputStream os;
    File tmpFile = null;
    
    /* create tmpfile and open output stream */
    
    if (_atomically) {
      try {
        // TBD: this gives Prefix string too short
        tmpFile = File.createTempFile(".", ".atomicwrite", _file.getParentFile());
      }
      catch (IOException e) {
        return e;
      }
      
      try {
        os = new FileOutputStream(_file);
      }
      catch (FileNotFoundException e) {
        tmpFile.delete();
        return e;
      }
    }
    else {
      try {
        os = new FileOutputStream(_file);
      }
      catch (FileNotFoundException e) {
        return e;
      }
    }
    
    /* write data to stream */
    
    Exception error = writeToStream(_data, os);
    
    try {
      if (os != null) os.close();
    }
    catch (IOException e) {
      log.warn("could not close output stream: " + os, e);
      os = null;
    }
    
    if (error != null) {
      if (tmpFile != null) tmpFile.delete();
      return error;
    }
    
    if (tmpFile == null) {
      /* OK, we did a non atomic write and everything went fine. Great! */
      return null;
    }
    
    /* move file on atomic writes, depending on the host OS this is not really
     * atomic
     */
      
    /* first attempt to move the file "over" the old one */

    if (tmpFile.renameTo(_file))
      return null; /* rename went fine */

    /* If this didn't work out, check whether the target exists and
     * delete it prior moving the tmpfile over it */
    
    if (_file.exists()) {
      if (!_file.delete()) {
        // TBD: should we attempt to copy in this situation?
        tmpFile.delete();
        return new NSException("Could not delete target file");
      }

      if (tmpFile.renameTo(_file))
        return null; /* rename went fine */

      // TBD: should we attempt to copy in this situation?
      return new NSException("Could not move temporary data file!");
    }
    
    return new NSException("Could not move temporary data file!");
  }
  
  public static Exception writeToFile
    (byte[] _data, String _path, boolean _atomically)
  {
    if (_data == null)
      return new NSException("got no data to write ...");
    if (_path == null)
      return new NSException("got no path to write data to ...");
    
    return writeToFile(_data, new File(_path), _atomically);
  }

  
  /* hashing */
  
  /**
   * Calculates an MD5 hash over the given byte array and returns it as a
   * String containing hex digits (eg 0FAADE...).
   * 
   * @param _p - a byte array
   * @return a String containing the MD5 hash, or null on error
   */
  public static String md5HashForData(byte[] _p) {
    if (_p == null) return null;
    
    String pwdhash = null;
    try {
      // TODO: cache digest in thread local variable?
      MessageDigest md5 = MessageDigest.getInstance("MD5");
      md5.update(_p);
      
      byte[]        bytehash  = md5.digest();
      StringBuilder hexString = new StringBuilder();
      for (int i = 0; i < bytehash.length; i++) {
        String s = Integer.toHexString(0xFF & bytehash[i]);
        if (s.length() == 1)
          hexString.append('0');
        hexString.append(s);
      }
      md5.reset();

      pwdhash = hexString.toString();
    }
    catch (NoSuchAlgorithmException e) {
      System.err.println("Did not find MD5 hash generator!");
      return null;
    }
    
    if (pwdhash == null || pwdhash.length() == 0) {
      log.error("could not compute the MD5 hash of a given byte array.");
      return null;
    }
    return pwdhash;
  }
}
