package gov.va.vinci.dart.dms.biz;

import gov.va.vinci.dart.service.DartObjectFactory;

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.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.security.UnrecoverableKeyException;
import java.util.List;

import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToOne;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import javax.persistence.Version;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.io.Resource;

@Entity
@Table(name="content",schema="hib")
public class Content {
	private static Log log = LogFactory.getLog(Content.class);
	private static int rollingCount = 0;
	
	private static KeyStore keyStore = null;
	
	@Id
	@GeneratedValue(strategy=GenerationType.IDENTITY) // because of course SQL Server doesn't support sequences.
	protected int id;

	@Version
	@Column(name="version")
	protected int version;  // for optimistic locking managed by Hibernate
	
	@Column(name="token")
	private long token;

	@Column(name="typesuffix")
	private String typeSuffix;
	
	@ManyToOne(fetch=FetchType.LAZY)
	@JoinColumn(name="repositoryid")
	Repository repository;
	
	@OneToOne(fetch=FetchType.LAZY)
	@JoinTable(
		      name="contentmap", schema="hib",
		      joinColumns={@JoinColumn(name="origcontentid", referencedColumnName="ID")},
		      inverseJoinColumns={@JoinColumn(name="mappedcontentid", referencedColumnName="ID")})
	Content mappedContent;
	
	
	Content(){}
	
	public static Content findById(final int contentId) {
		return DartObjectFactory.getInstance().getContentDAO().findById(contentId);
	}
	
	public static List<Content> listByRepository(final int repositoryId) {
		return DartObjectFactory.getInstance().getContentDAO().listByRepository(repositoryId);
	}
	
	public static Content create(final String typeSuffix) {
		return create(typeSuffix, 1);  // create the content in the default repository
	}

	public static Content create(final String typeSuffix, final int repositoryId) {
		Content result = new Content();
		
		result.generateToken();
		result.typeSuffix = typeSuffix;
		result.repository = Repository.findById(repositoryId);  
		
		DartObjectFactory.getInstance().getContentDAO().save(result);
		
		return result;
	}
	
	public static Content copy(final Content origContent, final int repositoryId) {
		Content result = new Content();
		
		result.token = origContent.token;
		result.typeSuffix = origContent.typeSuffix;

		result.repository = Repository.findById(repositoryId);  
		
		DartObjectFactory.getInstance().getContentDAO().save(result);
		
		return result;
	}

	public int getId() {
		return id;
	}

	// TESTING ONLY
	public void setId(int id) {
		this.id = id;
	}
	
	public long getToken() {
		return token;
	}

	public String getTypeSuffix() {
		return typeSuffix;
	}

	public Repository getRepository() {
		return repository;
	}
	
	public Content getMappedContent() {
		return mappedContent;
	}

	public void setMappedContent(Content mappedContent) {
		this.mappedContent = mappedContent;
		
		DartObjectFactory.getInstance().getContentDAO().save(this);
	}
	
	public String getRepositoryPath() {
		return  generateFilePath(this.token, typeSuffix);	
	}

	public File getFile(final boolean useFilestorePath) throws IOException {

		log.debug("getFile repository " + repository);
		log.debug("getFile repository directory name" + repository.getRepositoryDirectoryName(useFilestorePath));
		log.debug("getFile local path " + getRepositoryPath());
		log.debug("getFile typeSuffix " + typeSuffix);

		String fileName = getCompleteFilePathName(useFilestorePath);
		
		log.debug("getFile returns " + fileName);
		
		new File(fileName).getParentFile().mkdirs();
	
		return new File(fileName);
	}
	
	// returns the complete file path to the file on the host file system.
	private String getCompleteFilePathName(final boolean useFilestorePath) {
		return repository.getRepositoryDirectoryName(useFilestorePath) + File.separator + getRepositoryPath();
	}
	
	private synchronized long generateToken() {
		long tmp = System.currentTimeMillis();
		tmp *= 0x01000L;
		tmp |= (long)rollingCount;
		
		rollingCount += 1;
		if (rollingCount > 16383) {
			rollingCount = 0;
		}
				
		token = tmp;
		
		return token;
	}
	
	public InputStream openStream(final boolean useFilestorePath) throws FileNotFoundException, IOException, GeneralSecurityException {
		String fileName = getCompleteFilePathName(useFilestorePath);

		if (fileName == null || fileName.length() < 1) {
			throw new FileNotFoundException("Content not allocated for document.");
		}
		
		if (repository.isEncrypted()) {
			
	        Security.addProvider(new com.sun.crypto.provider.SunJCE());  
	        Cipher cipher = Cipher.getInstance("DESEDE/ECB/PKCS5Padding");  
	        log.debug(" -- Algorithm --> " + cipher.getAlgorithm());  
	        
	        if (keyStore == null) {
	        	loadKeyStore( repository.getPassword() );
	        }
	        
	        SecretKey secretKey = (SecretKey) keyStore.getKey(repository.getDomain(), repository.getPassword().toCharArray());
	        
            cipher.init(Cipher.DECRYPT_MODE, secretKey);

            FileInputStream fis = new FileInputStream(fileName);
            return new CipherInputStream(fis, cipher);
		}
		else {
			return new FileInputStream(fileName);
		}
	}
	
	public void write(final InputStream is, final boolean useFilestorePath) throws FileNotFoundException, IOException, GeneralSecurityException {
		if (getRepositoryPath() == null || getRepositoryPath().length() < 1) {
			throw new FileNotFoundException("Content not allocated for document.");
		}

		try {
			createDirPath(getCompleteFilePathName(useFilestorePath));
		}
		catch (IOException e) {
			e.printStackTrace();
		}
		
		saveAsFile(is, useFilestorePath);
	}
	
	private static String generateFilePath(final long token, final String ext) {
		
		long fragment = token / 0x1000000000000L;
		int frag2 = (int)fragment & 0x0ff;

		fragment = token / 0x10000000000L;
		int frag3 = (int)fragment & 0x0ff;

		fragment = token / 0x100000000L;
		int frag4 = (int)fragment & 0x0ff;

		fragment = token / 0x1000000L;
		int frag5 = (int)fragment & 0x0ff;

		fragment = token / 0x10000L;
		int frag6 = (int)(token & 0x0ffffffL);

		StringBuffer buf = new StringBuffer();
		buf.append(Integer.toHexString(frag2));
		buf.append('/');
		buf.append(Integer.toHexString(frag3));
		buf.append('/');
		buf.append(Integer.toHexString(frag4));
		buf.append('/');
		buf.append(Integer.toHexString(frag5));
		buf.append('/');
		buf.append(Integer.toHexString(frag6));
		buf.append('.');
		buf.append(ext);
		
		log.debug("generateFilePath " + buf.toString());
		return buf.toString();
	}
	

	/** Guarantees that the system file store path to a file exists
	 * 
	 * @param filePath
	 * @throws IOException
	 */
	private void createDirPath(final String filePath) throws IOException {
		int idx = filePath.lastIndexOf("/");
		if (idx > 0) {
			 new File(filePath.substring(0,idx)).mkdirs();
		}
	}
	
	/** copies the byte contents of an input stream to an output stream
	 * 
	 * @param os
	 * @param is
	 * @throws IOException
	 */
	private void copyStreamToStream(final OutputStream os, final InputStream is) throws IOException {
		while(true) {
			int b = is.read();
			if (b == -1) {
				break;
			}
			os.write(b);
		}
	}


	/** Read a file and output its contents to a stream 
	 * 
	 * @param os
	 * @param filePathName
	 * @throws FileNotFoundException
	 * @throws IOException
	 */
//	private void readFile(final OutputStream os, final String filePathName) throws FileNotFoundException, IOException {
//		FileInputStream fis = new FileInputStream(filePathName);
//		copyStreamToStream(os, fis);
//		fis.close();
//	}
	
	private static void loadKeyStore(final String key) throws IOException, GeneralSecurityException {
		keyStore = KeyStore.getInstance("JCEKS");

		FileInputStream fis = null;
		
        try {
	        //String fname = System.getProperty("user.home") + File.separator + ".keystore";
	        //FileInputStream fis = new FileInputStream(fname);
        	
			Resource keystoreResource = DartObjectFactory.getInstance().getApplicationContext().getResource("/WEB-INF/classes/" + ".keystore");
	        fis = new FileInputStream(keystoreResource.getFile());
	        
        	if( key != null ) {
        		keyStore.load(fis,key.toCharArray());
        	}
        }
        finally {
        	try {
	        	if( fis != null ) {
	        		fis.close();
	        	}
			} catch(IOException e) {
				log.error("Error closing the stream: " + e.getMessage());
			}
        }
	}
	
	/** Read and save the contents of a stream as a file in the system file store with the specified suffix. 
	 * 
	 * @param is
	 * @param userId
	 * @param suffix
	 * @throws FileNotFoundException
	 * @throws IOException
	 * @throws InvalidKeyException 
	 * @throws NoSuchAlgorithmException 
	 * @throws KeyStoreException 
	 * @throws UnrecoverableKeyException 
	 * @throws NoSuchPaddingException 
	 */
	private void saveAsFile(final InputStream is, final boolean useFilestorePath) throws FileNotFoundException, IOException, GeneralSecurityException {
		
		File myFile = getFile(useFilestorePath);

		if( myFile.exists() == true ) {
			return;
		}
		
		FileOutputStream fos = new FileOutputStream(myFile);
		
		CipherOutputStream cos = null;

		try {
			if (repository.isEncrypted()) {
	
			        Security.addProvider(new com.sun.crypto.provider.SunJCE());  
			        Cipher cipher = Cipher.getInstance("DESEDE/ECB/PKCS5Padding");  
			        log.debug(" -- Algorithm --> " + cipher.getAlgorithm());  
			        
			        if (keyStore == null) {
			        	loadKeyStore( repository.getPassword() );
			        }
			        
			        SecretKey secretKey = (SecretKey) keyStore.getKey(repository.getDomain(), repository.getPassword().toCharArray());
			        
		            cipher.init(Cipher.ENCRYPT_MODE, secretKey);
		
		            cos = new CipherOutputStream(fos, cipher);
		            
		            byte[] block = new byte[256];
		            int i;
		            while ((i = is.read(block)) != -1) {
		            	cos.write(block, 0, i);
		            }
		            
		            cos.close();
		            fos.close();
			}
			else {
				copyStreamToStream(fos, is);
			}
		}
		finally {
			try {
				if( cos != null ) {
					cos.close();
				}
			} catch(IOException e) {
				log.error("Error closing the stream: " + e.getMessage());
			}
			
			try {
				if( fos != null ) {
					fos.close();
				}
			} catch(IOException e) {
				log.error("Error closing the stream: " + e.getMessage());
			}
		}
	}

}
