/*******************************************************************************
 * This file is part of GECAMed.
 * 
 * GECAMed is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License (L-GPL) as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * GECAMed 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 (L-GPL)
 * along with GECAMed.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * GECAMed is Copyrighted by the Centre de Recherche Public Henri Tudor (http://www.tudor.lu)
 * (c) CRP Henri Tudor, Luxembourg, 2008
 *******************************************************************************/
package lu.tudor.santec.gecamed.core.utils;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
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.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import lu.tudor.santec.gecamed.patient.utils.PatientNameFormatter;

import org.apache.commons.io.FilenameUtils;

/**
 * general helper classes for all kind of file operations
 * 
 * 
 * @author Johannes Hermen johannes.hermen(at)tudor.lu
 * 
 * @version <br>
 *          $Log: FileUtils.java,v $
 *          Revision 1.27  2013-11-25 13:15:22  ferring
 *          File reading and writing improved
 *
 *          Revision 1.26  2013-11-22 14:43:45  ferring
 *          closing stream in time
 *
 *          Revision 1.25  2013-11-14 18:23:07  donak
 *          belongs to the last commit (resolved conflicts)
 *
 *          Revision 1.23  2013-11-05 07:23:12  ferring
 *          method added to delete CDA folder of patient
 *
 *          Revision 1.22  2013-10-23 13:19:31  ferring
 *          FileOpening fixed
 *
 *          Revision 1.21  2013-10-22 17:09:49  donak
 *          User inactivity Watchdog (currently not enabled)
 *          Fix for pdf --> pdf/a conversion
 *
 *          Revision 1.20  2013-10-21 10:58:28  donak
 *          Fixed receive file content bug
 *          Fixed bug about not displaying progress indicator (animated gif) when context is not on JTable
 *          Fixed upload menu item for letters where item text was not language dependent (also removed ProvideDocumentAction as it was redundant)
 *
 *          Revision 1.19  2013-05-08 13:45:24  ferring
 *          comment on patient folders
 *
 *          Revision 1.18  2008-09-25 09:43:06  heinemann
 *          fixed copyrights
 *
 *          Revision 1.17  2008-09-17 14:07:13  heinemann
 *          *** empty log message ***
 *
 *          Revision 1.16  2008-09-15 09:18:35  heinemann
 *          included deletion of patient files in patient delete process.
 *
 *          Revision 1.15  2008-07-25 14:42:42  heinemann
 *          *** empty log message ***
 *
 *          Revision 1.14  2008-07-14 12:34:51  hermen
 *          added writeStreamToFile
 *
 *          Revision 1.13  2008-04-14 12:39:02  hermen
 *          fixed small path bug
 *
 *          Revision 1.12  2008-04-14 08:10:49  hermen
 *          added
 *          ArrayList<File> getFilesRecursive(File dir)
 *
 *          Revision 1.11  2008-04-10 12:25:42  heinemann
 *          *** empty log message ***
 *
 *          Revision 1.10  2008-04-09 09:39:43  heinemann
 *          *** empty log message ***
 *
 *          Revision 1.9  2008-04-08 09:25:52  hermen
 *          *** empty log message ***
 * <br>
 *          Revision 1.8 2008-03-28 14:08:30 hermen <br>
 *          changed patient dir numberformat <br>
 *          <br>
 *          Revision 1.7 2008-03-28 14:03:54 hermen <br>
 *          *** empty log message *** <br>
 *          <br>
 *          Revision 1.6 2008-03-28 14:03:08 hermen <br>
 *          *** empty log message *** <br>
 *          <br>
 *          Revision 1.5 2008-03-28 14:02:45 hermen <br>
 *          changed patient dir numberformat <br>
 *          <br>
 *          Revision 1.4 2008-03-28 13:57:35 hermen <br>
 *          made getpatientdir public <br>
 *          <br>
 *          Revision 1.3 2008-03-28 13:40:45 heinemann <br>
 *          *** empty log message *** <br>
 *          <br>
 *          Revision 1.2 2008-03-05 07:27:39 hermen <br>
 *          a lot of improvements and bugfixes <br>
 * 
 */
public class FileUtils { 

	/**
	 * Number of patient folders that are grouped in one folder
	 */
	private static final int STEP = 500;
	
	private static final int _128KBIT = 128000;
	
	/**
	 * The length of the file date format (yyyyMMdd_HHmmss_SSS) + '-' and '.'
	 */
	private static int UNIQUE_NAME_LENGTH = 21;
	
	private static int MAX_FILENAME_LENGTH = 50;
	
	/** the logger Object for this class */
	private static org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger(FileUtils.class.getName());

	private static NumberFormat nf = NumberFormat.getInstance();

	static {
		nf.setMinimumIntegerDigits(6);
		nf.setGroupingUsed(false);
	}
	
	
	/**
	 * reads the provided file into a byte[]
	 * 
	 * @param file
	 * @return
	 * @throws IOException
	 */
	public static byte[] readFile (File file) throws IOException
	{
		return readFile(new FileInputStream(file));
	}
	
	
	/**
	 * reads the provided FileInputtream into a byte[]
	 * 
	 * @param fis
	 * @return
	 * @throws IOException
	 */
	public static byte[] readFile (FileInputStream fis) throws IOException 
	{
		int				fileSize;
		byte[]			data;
		int				dataIndex	= 0;
		ByteBuffer		buffer;
		int				read;
		FileChannel		channel;
		
		
		try
		{
			fileSize	= fis.available();
			data		= new byte[fileSize];
			channel		= fis.getChannel();
			
			if (fileSize > _128KBIT)
			{
				buffer = ByteBuffer.allocateDirect(_128KBIT);
				while ((read = channel.read(buffer)) > 0)
				{
					buffer.position(0);
					buffer.limit(read);
					while (buffer.hasRemaining())
						data[dataIndex++] = buffer.get();
					buffer.clear();
				}
			}
			else
			{
				buffer = ByteBuffer.wrap(data);
				channel.read(buffer);
			}
		}
		finally
		{
			if (fis != null)
				fis.close();
		}
		
		return data;
		
		// old way
//		InputStream is = new FileInputStream(file);
//
//		long length = file.length();
//		byte[] bytes = new byte[(int) length];
//
//		int offset = 0;
//		int numRead = 0;
//		while (offset < bytes.length
//				&& (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
//			offset += numRead;
//		}
//		is.close();
//
//		if (offset < bytes.length) {
//			throw new IOException("Could not completely read file "
//					+ file.getName());
//		}
//
//		return bytes;
	}

	/**
	 * writes the provided byte[] to the provided file.
	 * 
	 * @param data
	 * @param target
	 * @return
	 * @throws IOException
	 */
	public static boolean writeFile (byte[] data, File target)
			throws IOException
	{
		ByteBuffer			buffer		= ByteBuffer.wrap(data);
		FileOutputStream	fos			= null;
		FileChannel			channel;
		
		
		try
		{
			fos		= new FileOutputStream(target);
			channel	= fos.getChannel();
			channel.write(buffer);
		}
		finally
		{
			if (fos != null)
				fos.close();
		}
		
		return true;
		
//		BufferedOutputStream fOut = new BufferedOutputStream(
//				new FileOutputStream(toFile, false));
//		fOut.write(data);
//		fOut.close();
//		return true;
	}

	/**
	 * deletes the provided file
	 * 
	 * @param file
	 * @return
	 */
	public static boolean deleteFile(File file) {
		return file.delete();
	}

	/**
	 * 
	 * Format of the folder is 001-500/patientid/
	 * 
	 * @param patientId
	 * @return
	 */
	public static String getPatientDir(Integer patientId) {
		/* ================================================== */
		/*
		 * Yes, it's a bug! Should be: 
		 * 		start = (patientId / STEP) * STEP + 1
		 * Could be fixed by renaming the folders, but this is quite sensible, 
		 * because this method is called from several places.
		 * Renaming it once in the updater or installer might cause problems,
		 * when reloading files from a file backup.
		 */
		int start = (patientId / STEP) + 1;
		int end = start + STEP - 1;

		return "" + nf.format(start) + "-" + nf.format(end) + File.separator
				+ nf.format(patientId);
		/* ================================================== */
	}
	
	
	private static String createFileName (String filename)
	{
		return createFileName(filename, true, true);
	}
	
	
	/**
	 * Format is
	 * 
	 * filename_TIMESTAMP.Extension
	 * 
	 * example myword_12897324862387461.doc
	 * 
	 * @param filename
	 * @return
	 */
	private static String createFileName (String filename, boolean normalize, boolean makeUnique) 
	{
		try
		{
			if (makeUnique)
			{
				String basename = FilenameUtils.getBaseName(filename);
				String extension = FilenameUtils.getExtension(filename);
				
				if ((basename.length() + extension.length() + UNIQUE_NAME_LENGTH) > MAX_FILENAME_LENGTH)
				{
					// shorten the basename
					basename = basename.substring(0, MAX_FILENAME_LENGTH - UNIQUE_NAME_LENGTH);
				}
				
				// add the date in millis to the file to make it unique
				filename = new StringBuilder()
						.append(basename)
						.append("_")
						.append(GECAMedUtils.getDateFormatter(GECAMedUtils.DATE_FORMAT_FILE_TIME).format(new Date()))
						.append(".")
						.append(extension)
						.toString();
			}
			
			if (normalize)
			{
				// remove the accents from the filename
				filename = PatientNameFormatter.unaccent(filename);
				// make the file lower case
				filename = filename.toLowerCase();
				// replace the spaces with '_'
				filename = filename.replace(" ", "_");
				
				// remove special characters
				Pattern p = Pattern.compile("[^a-z0-9_.]");
				filename = p.matcher(filename).replaceAll("");
			}
		} 
		catch (Exception e)
		{
			logger.warn("Error while creating unique filename", e);
		}

		return filename;
		/* ================================================== */
	}
	
	
	/**
	 * Save a file to the file system
	 * Overrides an existing file.
	 * @param data
	 *                the file data
	 * @param filename
	 *                the filename
	 * @param dir
	 *                the folder to store. See ServerConfig for selection
	 * @param patientId
	 *                the id of the patient
	 * @return the generated filename without path.
	 * 
	 * @throws Exception
	 */
	public static String saveGECAMedFile(byte[] data, String filename,
			String dir) throws Exception {
		/* ================================================== */
		if (data == null 
				|| filename == null
				|| "".equals(filename) 
				|| dir == null 
				|| "".equals(dir))
			throw new Exception("Missing input data to store a file");
		/* ------------------------------------------------------- */
		// check if file exists
		/* ------------------------------------------------------- */
		filename = createFileName(filename, true, false);
		File file = new File(dir, filename);
		
		if (file.exists()) {
			/* ------------------------------------------------------- */
			String generatedFileName = createFileName(filename, false, true);
			file = new File(dir, generatedFileName);
			/* ------------------------------------------------------- */
		}
		file.getParentFile().mkdirs();
		
		if (!file.createNewFile())
			// TODO: create specific Exception
			throw new Exception("Couldn't create file "+file.getAbsolutePath());
		
		writeFile(data, file);

		return file.getName();
		/* ================================================== */
	}

	/**
	 * Read a file from the filesystem
	 * 
	 * @param filename
	 *                the generated filename
	 * @param dir
	 *                the folder where the file was stored. See ServerConfig
	 *                for selection
	 * @param patientId
	 *                the patient id
	 * @return the file's data
	 * 
	 * @throws Exception
	 */
	public static byte[] getGECAMedFile(String filename, String dir)
			throws Exception {
		/* ================================================== */
		if (filename == null || "".equals(filename) || dir == null
				|| "".equals(dir))
			throw new Exception("Missing input data to read a file");
		/* ------------------------------------------------------- */
		File file = new File(dir, filename);

		return readFile(file);
		/* ================================================== */
	}

	/**
	 * Delete a file
	 * 
	 * @param filename
	 * @param dir
	 * @param patientId
	 * @return
	 * @throws Exception
	 */
	public static boolean deleteGECAMedFile(String filename, String dir)
			throws Exception {
		/* ================================================== */
		if (filename == null || "".equals(filename) || dir == null
				|| "".equals(dir))
			throw new Exception("Missing input data to delete a file");
		/* ------------------------------------------------------- */
		File file = new File(dir, filename);

		return deleteFile(file);
		/* ================================================== */
	}
	
	
	
	/**
	 * Creats a File object for a patient's file
	 * The file is not created yet and not check for existance!!
	 * 
	 * @param patientId
	 * @param filename
	 * @return
	 */
	public static File createPatientFile(Integer patientId, String filename) {
	    /* ================================================== */
	    // get the folder for this patient id
	    /* ------------------------------------------------------- */
	    String patientDir = getPatientDir(patientId);
        /* ------------------------------------------------------- */
        // create the file's folder
	    /* ------------------------------------------------------- */
        File fileFolder = new File(ServerConfig.getProperty(ServerConfig.PATIENT_FILES_DIR), patientDir);
        
        File file = new File(fileFolder.getAbsolutePath(), createFileName(filename));
        
        return file;
	    /* ================================================== */
	}
	
	/**
	 * creates an empty, temporary file with a unique file name
	 * 
	 * @param filename
	 *            The name of the file. The function assures that the temp-file name remains unique
	 * @return The temporary file
	 * @throws IOException
	 */
	public static File createTempFile(String filename) throws IOException {
		/* ================================================== */
		// create a tempfile
		/* ------------------------------------------------------- */
		// create a file object for the temp dir in the gecamed server base dir
		/* ------------------------------------------------------- */
		File tmpFile = new File(ServerConfig.getProperty(ServerConfig.FILE_BASE_DIR) + File.separator + "tmp");
		if (!tmpFile.exists())
			tmpFile.mkdirs();
		return File.createTempFile(FilenameUtils.getBaseName(filename), "." + FilenameUtils.getExtension(filename), tmpFile);
		/* ================================================== */
	}
	
	public static File moveTempFileToPatientDir(String filepath, String originalFilename, Integer patientId) throws Exception {
	    /* ================================================== */
	    // get the folder for this patient id
        /* ------------------------------------------------------- */
        String patientDir = getPatientDir(patientId);
        /* ------------------------------------------------------- */
        // create the file's folder
        /* ------------------------------------------------------- */
        File fileFolder = new File(ServerConfig.getProperty(ServerConfig.PATIENT_FILES_DIR), patientDir);
        /* ------------------------------------------------------- */
        // move the file
        /* ------------------------------------------------------- */
        File file = new File(fileFolder.getAbsolutePath(), createFileName(originalFilename));
        if (!file.exists())
            file.getParentFile().mkdirs();
        /* ------------------------------------------------------- */
        File tmpFile = new File(filepath);
        
        if (tmpFile.renameTo(file))
            return file;
        else {
            
            throw new Exception("unable to move temp file to patient dir " + tmpFile.getAbsolutePath() + " -> " + file.getAbsolutePath());
        }
        /* ------------------------------------------------------- */
	    /* ================================================== */
	}
	
	/**
	 * Save a file of a patient
	 * Overrides an existing file.
	 * @param data  the file data
	 * @param filename the filename
	 * @param dir     the folder to store. See ServerConfig for selection. A subfolder for the patient
	 *                will be created.
	 * @param patientId the id of the patient
	 * @return the generated filename without path.
	 * @throws Exception
	 */
	public static String saveGECAMedPatientFile(byte[] data, String filename,
			String dir, Integer patientId) throws Exception {
		/* ================================================== */
		if (data == null 
				|| filename == null
				|| "".equals(filename) 
				|| dir == null 
				|| "".equals(dir)
				|| patientId == null)
			throw new Exception("Missing input data to store a file");
		/* ------------------------------------------------------- */
		String	patientDir	= getPatientDir(patientId);
		File	fileFolder	= new File(dir, patientDir);
		
		return saveGECAMedFile(data, filename, fileFolder.getAbsolutePath());
		/* ------------------------------------------------------- */
//		// check if file exists
//		/* ------------------------------------------------------- */
//		File file = new File(fileFolder, filename);
//		
//		if (!file.exists()) {
//			/* ------------------------------------------------------- */
//			String generatedFileName = createFileName(filename);
//			file = new File(fileFolder, generatedFileName);
//			file.getParentFile().mkdirs();
//			/* ------------------------------------------------------- */
//		}
//		writeFile(data, file);
//
//		return file.getName();
		/* ================================================== */
	}

	/**
	 * Read a file from the filesystem
	 * @param filename
	 *                the generated filename
	 * @param dir
	 *                the folder where the file was stored. See ServerConfig
	 *                for selection
	 * @param patientId
	 *                the patient id
	 * @return the file's data
	 * 
	 * @throws Exception
	 */
	public static byte[] getGECAMedPatientFile(String filename, String dir,
			Integer patientId) throws FileNotFoundException, Exception {
		/* ================================================== */
		if (filename == null || "".equals(filename) || dir == null
				|| "".equals(dir) || patientId == null)
			throw new Exception("Missing input data to read a file");
		/* ------------------------------------------------------- */
		String patientDir = getPatientDir(patientId);
		File fileFolder = new File(dir, patientDir);
		/* ------------------------------------------------------- */
		File file = new File(fileFolder, filename);
		
		if (!file.exists())
			throw new FileNotFoundException("The file \""+file.getPath()+"\" does not exist!");
		
		return readFile(file);
		/* ================================================== */
	}

	/**
	 * Delete a patient file
	 * 
	 * 
	 * @param filename
	 * @param dir
	 * @param patientId
	 * @return
	 * @throws Exception
	 */
	public static boolean deleteGECAMedPatientFile(String filename, String dir,
			Integer patientId) throws Exception {
		/* ================================================== */
		if (filename == null || "".equals(filename) || dir == null
				|| "".equals(dir) || patientId == null)
			throw new Exception("Missing input data to delete a file");
		/* ------------------------------------------------------- */
		String patientDir = getPatientDir(patientId);
		File fileFolder = new File(dir, patientDir);
		/* ------------------------------------------------------- */
		File file = new File(fileFolder, filename);

		boolean result =  deleteFile(file);
		/* ------------------------------------------------------- */
		// check if folder is empty
		/* ------------------------------------------------------- */
//		if (fileFolder.list().length < 1)
//			fileFolder.delete();
//		
		return result;
		/* ================================================== */
	}
	
	
	/**
	 * Deletes a patient folder and all it's files.
	 * 
	 * @param dir the base dir to delete in. e.g. patient_dir or dicom_dir
	 * @param patientId the patientid specifies the folder to delete
	 * @return
	 * @throws Exception
	 */
	public static boolean deleteGECAMedPatientFolder(String dir,
			Integer patientId) throws Exception {
		/* ================================================== */
		String patientFolder = getPatientDir(patientId);
		
		File folder = new File(dir, patientFolder);
		/* ------------------------------------------------------- */
		if (!folder.isDirectory())
			return false;
		/* ------------------------------------------------------- */
		org.apache.commons.io.FileUtils.deleteDirectory(folder);
		
		return true;
		/* ================================================== */
	}
	
	
	public static boolean deleteCdaPatientFolder (Integer patientId)
	{
		String	baseDir		= ServerConfig.getProperty(ServerConfig.PATIENT_FILES_DIR);
		String	cdaDir		= getPatientDir(patientId) + File.separator + "CDA";
		File	cdaFolder	= new File(baseDir, cdaDir);
		
		if (!cdaFolder.exists() || !cdaFolder.isDirectory())
			return false;
		
		logger.info("Deleting CDA folder \""+cdaFolder.getAbsolutePath()+"\"");
		return org.apache.commons.io.FileUtils.deleteQuietly(cdaFolder);
	}
	
	
	/**
	 * returns an array of all files in the specified directory.
	 * @param dir start dir
	 * @return list of all recursive files 
	 */
	public static ArrayList<File> getFilesRecursive(File dir) {
		/* ================================================== */

		ArrayList<File> al = new ArrayList<File>();
		visitAllFiles(dir, al);
		return al;
		/* ================================================== */
	}
	
	/**
	 * Process only files under dir
	 * @param dir
	 * @param al
	 */
	private static void visitAllFiles(File dir, ArrayList<File> al) {
		/* ================================================== */
        if (dir.isDirectory()) {
            String[] children = dir.list();
            for (int i=0; i<children.length; i++) {
                visitAllFiles(new File(dir, children[i]), al);
            }
        } else {
            al.add(dir);
        }
        /* ================================================== */
    }

	/**
	 * @param resourceAsStream
	 * @param outFile
	 * @throws Exception
	 */
	public static void writeStreamToFile(InputStream resourceAsStream, File outFile) throws Exception {
		/* ================================================== */
	    FileOutputStream fos = new FileOutputStream(outFile);
	    try {
	        byte[] buf = new byte[1024];
	        int i = 0;
	        while ((i = resourceAsStream.read(buf)) != -1) {
	            fos.write(buf, 0, i);
	        }
	    } 
	    catch (Exception e) {
	        throw e;
	    }
	    finally {
	        fos.close();
	    }
		/* ================================================== */
	}
	
	  /**
	   * Unpack an archive from a URL
	   * 
	   * @param url
	   * @param targetDir
	   * @return the file to the url
	   * @throws IOException
	   */
	  public static File unpackArchive(URL url, File targetDir, boolean deleteAfter) throws IOException {
	      if (!targetDir.exists()) {
	          targetDir.mkdirs();
	      }
	      InputStream in = new BufferedInputStream(url.openStream(), 1024);
	      // make sure we get the actual file
	      File zip = File.createTempFile("gecamed_temp_", ".zip", targetDir);
	      OutputStream out = new BufferedOutputStream(new FileOutputStream(zip));
	      copyInputStream(in, out);
	      out.close();
	      File temp = unpackArchive(zip, targetDir);
	      if (deleteAfter) {
	    	  temp.delete();
	    	  if (temp.exists()) 
	    		  temp.deleteOnExit();
	      }
	      return temp;
	  }
	  /**
	   * Unpack a zip file
	   * 
	   * @param theFile
	   * @param targetDir
	   * @return the file
	   * @throws IOException
	   */
	  public static File unpackArchive(File theFile, File targetDir) throws IOException {
	      if (!theFile.exists()) {
	          throw new IOException(theFile.getAbsolutePath() + " does not exist");
	      }
	      if (!buildDirectory(targetDir)) {
	          throw new IOException("Could not create directory: " + targetDir);
	      }
	      ZipFile zipFile = new ZipFile(theFile);
	      try {
	    	  for (Enumeration entries = zipFile.entries(); entries.hasMoreElements();) {
	    		  ZipEntry entry = (ZipEntry) entries.nextElement();
	    		  File file = new File(targetDir, File.separator + entry.getName());
	    		  if (!buildDirectory(file.getParentFile())) {
	    			  throw new IOException("Could not create directory: " + file.getParentFile());
	    		  }
	    		  if (!entry.isDirectory()) {
	    			  copyInputStream(zipFile.getInputStream(entry), new BufferedOutputStream(new FileOutputStream(file)));
	    		  } else {
	    			  if (!buildDirectory(file)) {
	    				  throw new IOException("Could not create directory: " + file);
	    			  }
	    		  }
	    	  }
		} catch (Exception e) {
			throw new IOException("Error extracting Zip");
		} finally {
			zipFile.close();			
		}
	      return theFile;
	  }

	  public static void copyInputStream(InputStream in, OutputStream out) throws IOException {
	      byte[] buffer = new byte[1024];
	      int len = in.read(buffer);
	      while (len >= 0) {
	          out.write(buffer, 0, len);
	          len = in.read(buffer);
	      }
	      in.close();
	      out.close();
	  }

	  public static boolean buildDirectory(File file) {
	      return file.exists() || file.mkdirs();
	  }
	
}
