/**_____________________________________________________________________________
 *
 *                                 OATS - INAF
 *  Osservatorio Astronomico di Tireste - Istituto Nazionale di Astrofisica
 *  Astronomical Observatory of Trieste - National Institute for Astrophysics
 * ____________________________________________________________________________
 *
 * Copyright (C) 20016  Istituto Nazionale di Astrofisica
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation, Inc., 
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 * _____________________________________________________________________________
 **/

package it.inaf.oats.vospacebackend.implementation;

import ca.nrc.cadc.util.FileMetadata;
import ca.nrc.cadc.vos.DataNode;
import it.inaf.oats.vospacebackend.exceptions.ExceptionMessage;
import it.inaf.oats.vospacebackend.exceptions.VOSpaceBackendException;
import it.inaf.oats.vospacebackend.utils.ConfigReader;

import ca.nrc.cadc.vos.Node;
import ca.nrc.cadc.vos.server.NodeID;
import ca.nrc.cadc.vos.VOS;
import ca.nrc.cadc.vos.VOSURI;

import java.sql.SQLException;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.MessageFormat;
import java.util.UUID;

import org.apache.log4j.Logger;
import org.apache.commons.io.FileUtils;

/**
 *
 * @/author bertocco
 */
public class VOSpaceBackendPosix implements VOSpaceBackend {
    
    private static String documentRoot;
    private static String tmpStorageRoot;
    private static final String CONFIG_FILE_NAME = "VOSpace.properties";
    private static final Logger log = Logger.getLogger(VOSpaceBackendImplFactory.class);
    
    public VOSpaceBackendPosix() throws VOSpaceBackendException {
        
        try {
            ConfigReader myConf = new ConfigReader(CONFIG_FILE_NAME);
        
            this.documentRoot = myConf.getProperty("fs.posix.document.root");     
            this.tmpStorageRoot = myConf.getProperty("fs.posix.tmp.storage.root"); 
        } catch (Exception e) {
            ExceptionMessage exMsg = new ExceptionMessage();
            log.debug(MessageFormat.format(exMsg.getMessage("UNABLE_TO_READ_PROPERTIES"), CONFIG_FILE_NAME));
            throw new VOSpaceBackendException(                   
               MessageFormat.format(exMsg.getMessage("UNABLE_TO_READ_PROPERTIES"), CONFIG_FILE_NAME));
            
        }
        log.debug("VOSpace Backend Document Root = " + this.documentRoot);
        log.debug("VOSpace Backend Temporary Document Root = " + this.tmpStorageRoot);
        
    }
    

    public boolean createFile(String vosuri, String tmp_file, String md5_sum) 
                           throws VOSpaceBackendException, SQLException, IOException {
        
        log.debug("Entering in vospacebackendposix.createfile");
        VOSpaceBackendMetadata metadata = new VOSpaceBackendMetadata();
        
        log.debug("md5_checksum received in backendposix.createFile : " + md5_sum);
        String relativePath = this.createPathFromString(md5_sum);
        
        // Gets metadata on frontend
        log.debug("SBE: Checkpint 1 -> before new DatabaseNodePersistenceExt()");
        DatabaseNodePersistenceImpl dbNodePers = new DatabaseNodePersistenceImpl();
        log.debug("SBE: Checkpint 2 -> after new DatabaseNodePersistenceExt() and before myNode = dbNodePers.get(new VOSURI(vosuri));");
        log.debug("SBE: Checkpint 2A ->  vosuri = #" + vosuri + "#");
        Node myNode = null;
        try {
            myNode = dbNodePers.get(new VOSURI(vosuri));
            log.debug("SBE: Checkpint 3 -> after dbNodePers.get(new VOSURI(vosuri)); vosuri = #" + vosuri + "#");
        } catch (Exception e) {
            log.debug("Exception getting node from persistence.");
            log.debug("Exception message : " + e.getMessage());
            StringWriter sw = new StringWriter();
            e.printStackTrace(new PrintWriter(sw));
            String exceptionAsString = sw.toString();
            log.debug(exceptionAsString);
        }
        NodeID nodeID = (NodeID)myNode.appData;
        log.debug("SBE: Checkpint 4 -> nodeID = " + nodeID.toString());
        Long myNodeID = nodeID.getID();
        log.debug("SBE: Checkpint 5  -> nodeID.getID() = " + myNodeID);

        if (!(myNode instanceof DataNode)) {
            log.debug("Node instance NOT found");
            throw new VOSpaceBackendException("Node instance NOT found");
        } 
        log.debug("SBE: Checkpint 6 ");
        DataNode myDataNode = (DataNode)myNode;
        
        // Needs syncronisation BEGIN
        boolean result = false;
        synchronized (this) {
            try {
            
                dbNodePers.setBusyState(myDataNode, VOS.NodeBusyState.notBusy, VOS.NodeBusyState.busyWithWrite);
                log.debug("SBE: Checkpint 7 -> after  dbNodePers.setBusyState");
            } catch (Exception ex) {
                log.debug("Exception in dbNodePers.setBusyState");
                throw new VOSpaceBackendException("TransientException in dbNodePers.setBusyState");
            }
            log.debug("SBE: Checkpint 8 -> tmp_file = " + tmp_file + " --- myNodeID.toString() = " + myNodeID.toString());
            FileRecord putReqData = metadata.setRequest(tmp_file, myNodeID.toString());
            log.debug("SBE: Checkpint 9  -> after metadata.setPutRequest(tmp_file, myNodeID.toString())");
            boolean myresult = putReqData.getIfOperationSuccessful();
            log.debug("SBE: Checkpint 9A -> myresult = " + myresult);
            if(!myresult) {
                log.debug("Fails updating table NodeStoredFileAndNode");
                throw new VOSpaceBackendException("Node instance NOT found");
            }   
            log.debug("SBE: Checkpint 10  -> operation successful");
            log.debug("SBE: Checkpint 10  -> tmp_file = " +tmp_file  + " --- md5_sum = " + md5_sum + " --- relativePath = " + relativePath);

        
            // sets backend metadata
            FileRecord fileMetadata = metadata.setFile(tmp_file, md5_sum, relativePath);
            log.debug("SBE: Checkpint 11  -> after metadata.setFile");
 
            // Writes the file on the file system
            result = fileMetadata.getIfOperationSuccessful();
            if (result) {
                log.debug("File metadata successfully saved. I'm going to store the file content");
                this.fileFromTmpToFinalStorageArea(tmp_file, relativePath, "MOVE"); 
            } else
                log.debug("File backend metadata NOT saved. Need to abort the operation");
            // Set front-end metadata and not busy
            FileMetadata nodeMetadata = new FileMetadata();
            nodeMetadata.setMd5Sum(md5_sum);
            log.debug("SBE: Checkpint 12 ->  After nodeMetadata.setMd5Sum(md5_sum)");

            // INSERT A CORRECT FileSize !!!!!!!!!!!!!!!!!!!!
            nodeMetadata.setContentLength(345678L);
            //nodeMetadata.setContentLength("file_size");
            try {
                log.debug("SBE: Checkpint 13 ->  myDataNode = " + myDataNode + " --- nodeMetadata = " + nodeMetadata);
                dbNodePers.setFileMetadata(myDataNode, nodeMetadata, false);
                log.debug("SBE: Checkpint 13 ->  After dbNodePers.setFileMetadata(myDataNode, nodeMetadata, false)");
            } catch (Exception ex) {
                log.debug("Exception doing databasePersistence.setFileMetadat.");
                throw new VOSpaceBackendException("Node instance NOT found");
            }
        }
        // Needs syncronisation END
        
        return result;
                   
    }
    
    
    /* Retrieve the file, copy it in the temporary location, return the File */
    public File returnFile(String vosuri) 
               throws VOSpaceBackendException, SQLException, IOException {
        
        // Check if fileName is present in metadata DB
        VOSpaceBackendMetadata metadataDB = new VOSpaceBackendMetadata();
        
        // Gets metadata on frontend 
        NodeUtils nodeUtil = new NodeUtils();
        Long myNodeID = nodeUtil.getNodeIdLongfromVosuriStr(vosuri);
        
        if (myNodeID == null) {
            log.debug("Problem encountered reading backend node metadata");
            return null;
        }
               
        FileRecord backendMetadata = metadataDB.getRequest(myNodeID.toString());
        if (!backendMetadata.getIfOperationSuccessful()) {
            log.debug("Backend metadata not found");
            return null;
        }
        
        String storedFileName = backendMetadata.getStoredfileName();
        
        FileRecord myMetadata = metadataDB.getFile(storedFileName);
 
        File outFile = null;
        
        if (myMetadata.getIfOperationSuccessful()) {
            log.debug("Metadata record found for vosuri " + vosuri);
            log.debug("storedFileName = " + myMetadata.getStoredfileName());
            log.debug("relative_path = " + myMetadata.getRelativePath());
            log.debug("md5_checksum = " + myMetadata.getMD5Checksum());
            storedFileName = myMetadata.getStoredfileName();
            String relativePath = myMetadata.getRelativePath();
            String unique_file_id_str = UUID.randomUUID().toString();
            // Needs syncronisation START
            this.fileFromStorageAreaToTmp(storedFileName, unique_file_id_str, relativePath, "COPY");
            outFile = new File(this.getTmpPath() + storedFileName);
            return outFile;
            // Needs syncronisation END
        } else {
            log.debug("Metadata record NOT found for file " + vosuri);
            outFile = null;
            return outFile;
        } 
    
    }
    
    
    public String createPathFromString(String initialStr) throws VOSpaceBackendException{
        
        log.debug("initialStr = " + initialStr);
        log.debug("initialStr.substring(initialStr.length()-2, initialStr.length())" + initialStr.substring(initialStr.length()-2));
        log.debug("initialStr.length()-4, initialStr.length()-2)" + initialStr.substring(initialStr.length()-4, initialStr.length()-2)); 
        
        String relativePath = null;
        try{
            relativePath = new String(File.separator + 
                              initialStr.substring(initialStr.length()-4, initialStr.length()-2) +                              
                              File.separator +
                              initialStr.substring(initialStr.length()-2, initialStr.length()));
        } catch (Exception e) {
            log.debug("Exception creating partial path from string " + initialStr);
            throw new VOSpaceBackendException(e);
        }
        log.debug("relative path = " + relativePath);
        
        return relativePath;
        
    }
    
    public void fileFromTmpToFinalStorageArea(String tmpfile, String relPath, String operation)
                                 throws VOSpaceBackendException {
    
        
        File tmpFile = new File(this.getTmpPath() + tmpfile);  
        File finalStoredFile = new File(this.getStoragePath(relPath) + tmpfile);  
        
        log.debug("tmpStoredFile is: " + tmpFile);
        log.debug("finalStoredFile is: " + finalStoredFile);
        
        this.operateOnFiles(tmpFile, finalStoredFile, operation);
               
    }
    
    public void fileFromStorageAreaToTmp(String stored, String tmp, String relativePath, String operation)
                                 throws VOSpaceBackendException {
        
        File storedFile = new File(this.getStoragePath(relativePath) + stored);
        File tmpFile = new File(this.getTmpPath() + tmp);
         
        log.debug("storedFile is: " + storedFile);
        log.debug("tmpFile is: " + tmpFile);

        this.operateOnFiles(storedFile, tmpFile, operation);

    }
    
    
    public void operateOnFiles (File A, File B, String operation) throws VOSpaceBackendException {
                    
        log.debug("File A is: " + A);
        log.debug("File B is: " + B);
        log.debug("Operation required is " + operation);

        switch (operation) {
            case "MOVE":  
                this.moveFileAToFileB(A, B);
                break;
            case "COPY":  
                this.copyFileAToFileB(A, B);
                break;
            default: 
                log.debug("Error in operation required");
                throw new VOSpaceBackendException("Error in operation required");
        }
        
    }
        
        
    public boolean checksBeforeCopyOrMove(File A, File B)
                                            throws VOSpaceBackendException {
        
        boolean checkOK = false;
    
        if (!A.exists()) {
            log.debug("Move operation impossible: source file" + A.getAbsolutePath() 
                                                          + "does not exists.");
            throw new VOSpaceBackendException("Operation impossible: source file" 
                                              + A.getAbsolutePath() + "does not exists.");             
        }       
        
        String absolutePathB = B.getAbsolutePath();
        String pathB = absolutePathB.substring(0,absolutePathB.lastIndexOf(File.separator)); 
        File pathBFile = new File(pathB);
        if (!pathBFile.exists()) {
            try {
                checkOK = pathBFile.mkdirs();
            } catch (Exception e) {
                log.debug("Exception creating the final destination directory of file "
                                                      + B.getAbsolutePath());
                throw new VOSpaceBackendException(e);                
            }
        } else if (pathBFile.isDirectory()){
            checkOK = true;
        } else {
            log.debug("File " + pathB + " already exsists, but is not a directory.");
            checkOK = false;
        }
        
        return checkOK;
    
    }

    public void moveFileAToFileB (File A, File B) throws VOSpaceBackendException {
                    
        if (this.checksBeforeCopyOrMove(A, B)) {
            try {
                FileUtils.moveFile(A, B);
            } catch (Exception e) {
                log.debug("Exception moving temporary copy of uploaded file in its final destination directory");
                throw new VOSpaceBackendException(e);                
            }
        }
        
    }
    
    public void copyFileAToFileB (File A, File B) throws VOSpaceBackendException {
                    
        if (this.checksBeforeCopyOrMove(A, B)) {
            try {
                FileUtils.copyFile(A, B);
            } catch (Exception e) {
                log.debug("Exception moving temporary copy of uploaded file in its final destination directory");
                throw new VOSpaceBackendException(e);                
            }
        }
        
    }
    
    private String getStoragePath(String relativePath) {
        
        String storagePath = this.documentRoot + relativePath + File.separator;
        return storagePath;
        
    }
    
    private String getTmpPath() {
        
        String tmpFilePath = this.tmpStorageRoot + File.separator;
        return tmpFilePath;
        
    }
        
}
