/*
* Copyright 2010 Srikanth Reddy Lingala  
* 
* 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. 
*/

package com.zip4j.util;

import com.zip4j.exception.ZipException;
import com.zip4j.model.FileHeader;
import com.zip4j.model.ZipModel;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
import java.util.TimeZone;

public class Zip4jUtil {
	
	public static boolean isStringNotNullAndNotEmpty(String str) {
		if (str == null || str.trim().length() <= 0) {
			return false;
		}
		
		return true;
	}
	
	public static boolean checkOutputFolder(String path) throws ZipException {
		if (!isStringNotNullAndNotEmpty(path)) {
			throw new ZipException(new NullPointerException("output path is null"));
		}
		
		File file = new File(path);
		
		if (file.exists()) {
		
			if (!file.isDirectory()) {
				throw new ZipException("output folder is not valid");
			}
			
			if (!file.canWrite()) {
				throw new ZipException("no write access to output folder");
			}
		} else {
			try {
				file.mkdirs();
				if (!file.isDirectory()) {
					throw new ZipException("output folder is not valid");
				}
				
				if (!file.canWrite()) {
					throw new ZipException("no write access to destination folder");
				}
				
//				SecurityManager manager = new SecurityManager();
//				try {
//					manager.checkWrite(file.getAbsolutePath());
//				} catch (Exception e) {
//					e.printStackTrace();
//					throw new ZipException("no write access to destination folder");
//				}
			} catch (Exception e) {
				throw new ZipException("Cannot create destination folder");
			}
		}
		
		return true;
	}
	
	public static boolean checkFileReadAccess(String path) throws ZipException {
		if (!isStringNotNullAndNotEmpty(path)) {
			throw new ZipException("path is null");
		}
		
		if (!checkFileExists(path)) {
			throw new ZipException("file does not exist: " + path);
		}
		
		try {
			File file = new File(path);
			return file.canRead();
		} catch (Exception e) {
			throw new ZipException("cannot read zip file");
		}
	}
	
	public static boolean checkFileWriteAccess(String path) throws ZipException {
		if (!isStringNotNullAndNotEmpty(path)) {
			throw new ZipException("path is null");
		}
		
		if (!checkFileExists(path)) {
			throw new ZipException("file does not exist: " + path);
		}
		
		try {
			File file = new File(path);
			return file.canWrite();
		} catch (Exception e) {
			throw new ZipException("cannot read zip file");
		}
	}
	
	public static boolean checkFileExists(String path) throws ZipException {
		if (!isStringNotNullAndNotEmpty(path)) {
			throw new ZipException("path is null");
		}
		
		File file = new File(path);
		return checkFileExists(file);
	}
	
	public static boolean checkFileExists(File file) throws ZipException {
		if (file == null) {
			throw new ZipException("cannot check if file exists: input file is null");
		}
		return file.exists();
	}
	
	public static boolean isWindows(){
		String os = System.getProperty("os.name").toLowerCase();
	    return (os.indexOf( "win" ) >= 0); 
	}
	
	public static void setFileReadOnly(File file) throws ZipException {
		if (file == null) {
			throw new ZipException("input file is null. cannot set read only file attribute");
		}
		
		if (file.exists()) {
			file.setReadOnly();
		}
	}
	
	public static void setFileHidden(File file) throws ZipException {		
		if (file == null) {
			throw new ZipException("input file is null. cannot set hidden file attribute");
		}
		
		if (!isWindows()) {
			return;
		}
		
		if (file.exists()) {
			try {
				Runtime.getRuntime().exec("attrib +H \"" + file.getAbsolutePath() + "\"");
			} catch (IOException e) {
				// do nothing as this is not of a higher priority
				// add log statements here when logging is done
			}
		}
	}
	
	public static void setFileArchive(File file) throws ZipException {		
		if (file == null) {
			throw new ZipException("input file is null. cannot set archive file attribute");
		}
		
		if (!isWindows()) {
			return;
		}
		
		if (file.exists()) {
			try {
				if (file.isDirectory()) {
					Runtime.getRuntime().exec("attrib +A \"" + file.getAbsolutePath() + "\"");
				} else {
					Runtime.getRuntime().exec("attrib +A \"" + file.getAbsolutePath() + "\"");
				}
				
			} catch (IOException e) {
				// do nothing as this is not of a higher priority
				// add log statements here when logging is done
			}
		}
	}
	
	public static void setFileSystemMode(File file) throws ZipException {
		if (file == null) {
			throw new ZipException("input file is null. cannot set archive file attribute");
		}
		
		if (!isWindows()) {
			return;
		}
		
		if (file.exists()) {
			try {
				Runtime.getRuntime().exec("attrib +S \"" + file.getAbsolutePath() + "\"");
			} catch (IOException e) {
				// do nothing as this is not of a higher priority
				// add log statements here when logging is done
			}
		}
	}
	
	public static long getLastModifiedFileTime(File file, TimeZone timeZone) throws ZipException {
		if (file == null) {
			throw new ZipException("input file is null, cannot read last modified file time");
		}
		
		if (!file.exists()) {
			throw new ZipException("input file does not exist, cannot read last modified file time");
		}
		
		return file.lastModified();
	}
	
	public static String getFileNameFromFilePath(File file) throws ZipException {
		if (file == null) {
			throw new ZipException("input file is null, cannot get file name");
		}
		
		if (file.isDirectory()) {
			return null;
		}
		
		return file.getName();
	}
	
	public static long getFileLengh(String file) throws ZipException {
		if (!isStringNotNullAndNotEmpty(file)) {
			throw new ZipException("invalid file name");
		}
		
		return getFileLengh(new File(file));
	}
	
	public static long getFileLengh(File file) throws ZipException {
		if (file == null) {
			throw new ZipException("input file is null, cannot calculate file length");
		}
		
		if (file.isDirectory()) {
			return -1;
		}
		
		return file.length();
	}
	
	/**
	 * Converts input time from Java to DOS format
	 * @param time
	 * @return time in DOS format 
	 */
	public static long javaToDosTime(long time) {
		
		Calendar cal = Calendar.getInstance();
		cal.setTimeInMillis(time);
		
		int year = cal.get(Calendar.YEAR);
		if (year < 1980) {
		    return (1 << 21) | (1 << 16);
		}
		return (year - 1980) << 25 | (cal.get(Calendar.MONTH) + 1) << 21 |
	               cal.get(Calendar.DATE) << 16 | cal.get(Calendar.HOUR_OF_DAY) << 11 | cal.get(Calendar.MINUTE) << 5 |
	               cal.get(Calendar.SECOND) >> 1;
	}
	
	/**
	 * Converts time in dos format to Java format
	 * @param dosTime
	 * @return time in java format
	 */
	public static long dosToJavaTme(int dosTime) {
		int sec = 2 * (dosTime & 0x1f);
	    int min = (dosTime >> 5) & 0x3f;
	    int hrs = (dosTime >> 11) & 0x1f;
	    int day = (dosTime >> 16) & 0x1f;
	    int mon = ((dosTime >> 21) & 0xf) - 1;
	    int year = ((dosTime >> 25) & 0x7f) + 1980;
	    
	    Calendar cal = Calendar.getInstance();
		cal.set(year, mon, day, hrs, min, sec);
		cal.set(Calendar.MILLISECOND, 0);
		return cal.getTime().getTime();
	}
	
	public static FileHeader getFileHeader(ZipModel zipModel, String fileName) throws ZipException {
		if (zipModel == null) {
			throw new ZipException("zip model is null, cannot determine file header for fileName: " + fileName);
		}
		
		if (!isStringNotNullAndNotEmpty(fileName)) {
			throw new ZipException("file name is null, cannot determine file header for fileName: " + fileName);
		}
		
		FileHeader fileHeader = null;
		fileHeader = getFileHeaderWithExactMatch(zipModel, fileName);
		
		if (fileHeader == null) {
			fileName = fileName.replaceAll("\\\\", "/");
			fileHeader = getFileHeaderWithExactMatch(zipModel, fileName);
			
			if (fileHeader == null) {
				fileName = fileName.replaceAll("/", "\\\\");
				fileHeader = getFileHeaderWithExactMatch(zipModel, fileName);
			}
		}
		
		return fileHeader;
	}
	
	public static FileHeader getFileHeaderWithExactMatch(ZipModel zipModel, String fileName) throws ZipException {
		if (zipModel == null) {
			throw new ZipException("zip model is null, cannot determine file header with exact match for fileName: " + fileName);
		}
		
		if (!isStringNotNullAndNotEmpty(fileName)) {
			throw new ZipException("file name is null, cannot determine file header with exact match for fileName: " + fileName);
		}
		
		if (zipModel.getCentralDirectory() == null) {
			throw new ZipException("central directory is null, cannot determine file header with exact match for fileName: " + fileName);
		}
		
		if (zipModel.getCentralDirectory().getFileHeaders() == null) {
			throw new ZipException("file Headers are null, cannot determine file header with exact match for fileName: " + fileName);
		}
		
		if (zipModel.getCentralDirectory().getFileHeaders().size() <= 0) {
			return null;
		}
		ArrayList fileHeaders = zipModel.getCentralDirectory().getFileHeaders();
		for (int i = 0; i < fileHeaders.size(); i++) {
			FileHeader fileHeader = (FileHeader)fileHeaders.get(i);
			String fileNameForHdr = fileHeader.getFileName();
			if (!isStringNotNullAndNotEmpty(fileNameForHdr)) {
				continue;
			}
			
			if (fileName.equalsIgnoreCase(fileNameForHdr)) {
				return fileHeader;
			}
		}
		
		return null;
	}
	
	public static int getIndexOfFileHeader(ZipModel zipModel, 
			FileHeader fileHeader) throws ZipException {
		
		if (zipModel == null || fileHeader == null) {
			throw new ZipException("input parameters is null, cannot determine index of file header");
		}
		
		if (zipModel.getCentralDirectory() == null) {
			throw new ZipException("central directory is null, ccannot determine index of file header");
		}
		
		if (zipModel.getCentralDirectory().getFileHeaders() == null) {
			throw new ZipException("file Headers are null, cannot determine index of file header");
		}
		
		if (zipModel.getCentralDirectory().getFileHeaders().size() <= 0) {
			return -1;
		}
		String fileName = fileHeader.getFileName();
		
		if (!isStringNotNullAndNotEmpty(fileName)) {
			throw new ZipException("file name in file header is empty or null, cannot determine index of file header");
		}
		
		ArrayList fileHeaders = zipModel.getCentralDirectory().getFileHeaders();
		for (int i = 0; i < fileHeaders.size(); i++) {
			FileHeader fileHeaderTmp = (FileHeader)fileHeaders.get(i);
			String fileNameForHdr = fileHeaderTmp.getFileName();
			if (!isStringNotNullAndNotEmpty(fileNameForHdr)) {
				continue;
			}
			
			if (fileName.equalsIgnoreCase(fileNameForHdr)) {
				return i;
			}
		}
		return -1;
	}
	
	public static ArrayList getFilesInDirectoryRec(File path, 
			boolean readHiddenFiles) throws ZipException {
		
		if (path == null) {
			throw new ZipException("input path is null, cannot read files in the directory");
		}
		
		ArrayList result = new ArrayList();
		File[] filesAndDirs = path.listFiles();
		List filesDirs = Arrays.asList(filesAndDirs);
		
		if (!path.canRead()) {
			return result; 
		}
		
		for(int i = 0; i < filesDirs.size(); i++) {
			File file = (File)filesDirs.get(i);
			if (file.isHidden() && !readHiddenFiles) {
				return result;
			}
			result.add(file);
			if (file.isDirectory()) {
				List deeperList = getFilesInDirectoryRec(file, readHiddenFiles);
				result.addAll(deeperList);
			}
	    }
		return result;
	}
	
	public static String getZipFileNameWithoutExt(String zipFile) throws ZipException {
		if (!isStringNotNullAndNotEmpty(zipFile)) {
			throw new ZipException("zip file name is empty or null, cannot determine zip file name");
		}
		String tmpFileName = zipFile;
		if (zipFile.indexOf(System.getProperty("file.separator")) >= 0) {
			tmpFileName = zipFile.substring(zipFile.lastIndexOf(System.getProperty("file.separator")));
		}
		
		if (tmpFileName.indexOf(".") > 0) {
			tmpFileName = tmpFileName.substring(0, tmpFileName.lastIndexOf("."));
		}
		return tmpFileName;
	}
	
	public static byte[] convertCharset(String str) throws ZipException {
		try {
			byte[] converted = null;
			String charSet = detectCharSet(str);
			if (charSet.equals(InternalZipConstants.CHARSET_CP850)) {
				converted = str.getBytes(InternalZipConstants.CHARSET_CP850);
			} else if (charSet.equals(InternalZipConstants.CHARSET_UTF8)) {
				converted = str.getBytes(InternalZipConstants.CHARSET_UTF8);
			} else {
				converted = str.getBytes();
			}
			return converted;
		}
		catch (UnsupportedEncodingException err) {
			return str.getBytes();
		} catch (Exception e) {
			throw new ZipException(e);
		}
	}
	
	/**
	 * Decodes file name based on encoding. If file name is UTF 8 encoded
	 * returns an UTF8 encoded string, else return Cp850 encoded String. If 
	 * appropriate charset is not supported, then returns a System default 
	 * charset encoded String
	 * @param data
	 * @param isUTF8
	 * @return String
	 */
	public static String decodeFileName(byte[] data, boolean isUTF8) {
		if (isUTF8) {
			try {
				return new String(data, InternalZipConstants.CHARSET_UTF8);
			} catch (UnsupportedEncodingException e) {
				return new String(data);
			}
		} else {
			return getCp850EncodedString(data);
		}
	}
	
	/**
	 * Returns a string in Cp850 encoding from the input bytes.
	 * If this encoding is not supported, then String with the default encoding is returned.
	 * @param data
	 * @return String
	 */
	public static String getCp850EncodedString(byte[] data) {
		try {
			String retString = new String(data, InternalZipConstants.CHARSET_CP850);
			return retString;
		} catch (UnsupportedEncodingException e) {
			return new String(data);
		}
	}
	
	/**
	 * Returns an absoulte path for the given file path 
	 * @param filePath
	 * @return String
	 */
	public static String getAbsoluteFilePath(String filePath) throws ZipException {
		if (!isStringNotNullAndNotEmpty(filePath)) {
			throw new ZipException("filePath is null or empty, cannot get absolute file path");
		}
		
		File file = new File(filePath);
		return file.getAbsolutePath();
	}
	
	/**
	 * Checks to see if all the elements in the arraylist match the given type
	 * @param sourceList - list to be checked
	 * @param type - type of elements to be present in the list (ex: File, String, etc)
	 * @return true if all elements match the given type, if not returns false
	 */
	public static boolean checkArrayListTypes(ArrayList sourceList, int type) throws ZipException {
		
		if (sourceList == null) {
			throw new ZipException("input arraylist is null, cannot check types");
		}
		
		if (sourceList.size() <= 0) {
			return true;
		}
		
		boolean invalidFound = false;
		
		switch (type) {
		case InternalZipConstants.LIST_TYPE_FILE:
			for (int i = 0; i < sourceList.size(); i++) {
				if (!(sourceList.get(i) instanceof File)) {
					invalidFound = true;
					break;
				}
			}
			break;
		case InternalZipConstants.LIST_TYPE_STRING:
			for (int i = 0; i < sourceList.size(); i++) {
				if (!(sourceList.get(i) instanceof String)) {
					invalidFound = true;
					break;
				}
			}
			break;
		default:
			break;
		}
		return !invalidFound;
	}
	
	/**
	 * Detects the encoding charset for the input string
	 * @param str
	 * @return String - charset for the String
	 * @throws ZipException - if input string is null. In case of any other exception
	 * this method returns default System charset
	 */
	public static String detectCharSet(String str) throws ZipException {
		if (str == null) {
			throw new ZipException("input string is null, cannot detect charset");
		}
		
		try {
			byte[] byteString = str.getBytes(InternalZipConstants.CHARSET_CP850);
			String tempString = new String(byteString, InternalZipConstants.CHARSET_CP850);
			
			if (str.equals(tempString)) {
				return InternalZipConstants.CHARSET_CP850;
			}
			
			byteString = str.getBytes(InternalZipConstants.CHARSET_UTF8);
			tempString = new String(byteString, InternalZipConstants.CHARSET_UTF8);
			
			if (str.equals(tempString)) {
				return InternalZipConstants.CHARSET_UTF8;
			}
			
			return InternalZipConstants.CHARSET_DEFAULT;
		} catch (UnsupportedEncodingException e) {
			return InternalZipConstants.CHARSET_DEFAULT;
		} catch (Exception e) {
			return InternalZipConstants.CHARSET_DEFAULT;
		}
	}
	
	/**
	 * returns the length of the string by wrapping it in a byte buffer with
	 * the appropriate charset of the input string and returns the limit of the 
	 * byte buffer
	 * @param str
	 * @return length of the string
	 * @throws ZipException
	 */
	public static int getEncodedStringLength(String str) throws ZipException {
		if (!isStringNotNullAndNotEmpty(str)) {
			throw new ZipException("input string is null, cannot calculate encoded String length");
		}
		
		String charset = detectCharSet(str);
		return getEncodedStringLength(str, charset);
	}
	
	/**
	 * returns the length of the string in the input encoding
	 * @param str
	 * @param charset
	 * @return int
	 * @throws ZipException
	 */
	public static int getEncodedStringLength(String str, String charset) throws ZipException {
		if (!isStringNotNullAndNotEmpty(str)) {
			throw new ZipException("input string is null, cannot calculate encoded String length");
		}
		
		if (!isStringNotNullAndNotEmpty(charset)) {
			throw new ZipException("encoding is not defined, cannot calculate string length");
		}
		
		ByteBuffer byteBuffer = null;
		
		try {
			if (charset.equals(InternalZipConstants.CHARSET_CP850)) {
				byteBuffer = ByteBuffer.wrap(str.getBytes(InternalZipConstants.CHARSET_CP850));
			} else if (charset.equals(InternalZipConstants.CHARSET_UTF8)) {
				byteBuffer = ByteBuffer.wrap(str.getBytes(InternalZipConstants.CHARSET_UTF8));
			} else {
				byteBuffer = ByteBuffer.wrap(str.getBytes(charset));
			}
		} catch (UnsupportedEncodingException e) {
			byteBuffer = ByteBuffer.wrap(str.getBytes());
		} catch (Exception e) {
			throw new ZipException(e);
		}
		
		return byteBuffer.limit();
	}
	
	/**
	 * Checks if the input charset is supported
	 * @param charset
	 * @return boolean
	 * @throws ZipException
	 */
	public static boolean isSupportedCharset(String charset) throws ZipException {
		if (!isStringNotNullAndNotEmpty(charset)) {
			throw new ZipException("charset is null or empty, cannot check if it is supported");
		}
		
		try {
			new String("a".getBytes(), charset);
			return true;
		} catch (UnsupportedEncodingException e) {
			return false;
		} catch (Exception e) {
			throw new ZipException(e);
		}
	}
	
	public static ArrayList getSplitZipFiles(ZipModel zipModel) throws ZipException {
		if (zipModel == null) {
			throw new ZipException("cannot get split zip files: zipmodel is null");
		}
		
		if (zipModel.getEndCentralDirRecord() == null) {
			return null;
		}
		
		ArrayList retList = new ArrayList();
		String currZipFile = zipModel.getZipFile();
		String zipFileName = (new File(currZipFile)).getName();
		String partFile = null;
		
		if (!isStringNotNullAndNotEmpty(currZipFile)) {
			throw new ZipException("cannot get split zip files: zipfile is null");
		}
		
		if (!zipModel.isSplitArchive()) {
			retList.add(currZipFile);
			return retList;
		}
		
		int numberOfThisDisk = zipModel.getEndCentralDirRecord().getNoOfThisDisk();
		
		if (numberOfThisDisk == 0) {
			retList.add(currZipFile);
			return retList;
		} else {
			for (int i = 0; i <= numberOfThisDisk; i++) {
				if (i == numberOfThisDisk) {
					retList.add(zipModel.getZipFile());
				} else {
					String fileExt = ".z0";
					if (i > 9) {
						fileExt = ".z";
					}
					partFile = (zipFileName.indexOf(".") >= 0) ? currZipFile.substring(0, currZipFile.lastIndexOf(".")) : currZipFile + fileExt + (i + 1);
					retList.add(partFile);
				}
			}
		}
		return retList;
	}
	
	public static String getRelativeFileName(String file, String rootFolderInZip, String rootFolderPath) throws ZipException {
		if (!Zip4jUtil.isStringNotNullAndNotEmpty(file)) {
			throw new ZipException("input file path/name is empty, cannot calculate relative file name");
		}
		
		String fileName = null;
		
		if (Zip4jUtil.isStringNotNullAndNotEmpty(rootFolderPath)) {
			
			File rootFolderFile = new File(rootFolderPath);
			
			String rootFolderFileRef = rootFolderFile.getPath();
			
			if (!rootFolderFileRef.endsWith(InternalZipConstants.FILE_SEPARATOR)) {
				rootFolderFileRef += InternalZipConstants.FILE_SEPARATOR;
			}
			
			String tmpFileName = file.substring(rootFolderFileRef.length());
			if (tmpFileName.startsWith(System.getProperty("file.separator"))) {
				tmpFileName = tmpFileName.substring(1);
			}
			
			File tmpFile = new File(file);
			
			if (tmpFile.isDirectory()) {
				tmpFileName = tmpFileName.replaceAll("\\\\", "/");
				tmpFileName += InternalZipConstants.ZIP_FILE_SEPARATOR;
			} else {
				String bkFileName = tmpFileName.substring(0, tmpFileName.lastIndexOf(tmpFile.getName()));
				bkFileName = bkFileName.replaceAll("\\\\", "/");
				tmpFileName = bkFileName + tmpFile.getName();
			}
			
			fileName = tmpFileName;
		} else {
			File relFile = new File(file);
			if (relFile.isDirectory()) {
				fileName = relFile.getName() + InternalZipConstants.ZIP_FILE_SEPARATOR;
			} else {
				fileName = Zip4jUtil.getFileNameFromFilePath(new File(file));
			}
		}
		
		if (Zip4jUtil.isStringNotNullAndNotEmpty(rootFolderInZip)) {
			fileName = rootFolderInZip + fileName;
		}
		
		if (!Zip4jUtil.isStringNotNullAndNotEmpty(fileName)) {
			throw new ZipException("Error determining file name");
		}
		
		return fileName;
	}
	
	public static long[] getAllHeaderSignatures() {
		long[] allSigs = new long[11];
		
		allSigs[0] = InternalZipConstants.LOCSIG;
		allSigs[1] = InternalZipConstants.EXTSIG;
		allSigs[2] = InternalZipConstants.CENSIG;
		allSigs[3] = InternalZipConstants.ENDSIG;
		allSigs[4] = InternalZipConstants.DIGSIG;
		allSigs[5] = InternalZipConstants.ARCEXTDATREC;
		allSigs[6] = InternalZipConstants.SPLITSIG;
		allSigs[7] = InternalZipConstants.ZIP64ENDCENDIRLOC;
		allSigs[8] = InternalZipConstants.ZIP64ENDCENDIRREC;
		allSigs[9] = InternalZipConstants.EXTRAFIELDZIP64LENGTH;
		allSigs[10] = InternalZipConstants.AESSIG;
		
		return allSigs;
	}
}
