/*
Copyright (C) 2000-2010  Ministere de la culture et de la communication (France), AJLSM
See LICENCE file
*/
package fr.gouv.culture.sdx.repository;

//import fr.gouv.culture.sdx.db.AttachedDocument;

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.MalformedURLException;
import java.net.URL;
import java.text.DecimalFormat;

import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.avalon.framework.context.ContextException;
import org.apache.avalon.framework.service.ServiceException;
import org.apache.avalon.framework.service.ServiceManager;
import org.apache.cocoon.util.NetUtils;
import org.apache.cocoon.xml.XMLConsumer;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.excalibur.xml.sax.SAXParser;
import org.xml.sax.ContentHandler;

import fr.gouv.culture.sdx.document.Document;
import fr.gouv.culture.sdx.document.ParsableDocument;
import fr.gouv.culture.sdx.exception.SDXException;
import fr.gouv.culture.sdx.exception.SDXExceptionCode;
import fr.gouv.culture.sdx.utils.Utilities;
import fr.gouv.culture.sdx.utils.configuration.ConfigurationUtils;
import fr.gouv.culture.sdx.utils.constants.ContextKeys;
import fr.gouv.culture.sdx.utils.constants.Node;
import fr.gouv.culture.sdx.utils.database.DatabaseEntity;
import fr.gouv.culture.sdx.utils.save.SaveParameters;
import fr.gouv.culture.sdx.utils.save.Saveable;
import fr.gouv.culture.sdx.utils.zip.ZipWrapper;

/**
 * Implements a repository where files are stored in the server's filesystem.
 *
 * <p>
 * There are two main concerns with this repository : (1) how to retrieve efficiently
 * a document from it's id and (2) how to place files within the filesystem.
 * <p>
 * For the first concern, we use a simplified SDX database to keep track of
 * ids and their location.
 * <p>
 * For the second concern, one must give some parameters to help
 * SDX build a directory structure. These parameters are :
 * <ol>
 * <li><code>baseDirectory</code>, the path to a base directory, can be relative or absolute, a non-existent directory will be created
 * <li><code>extent</code>, the number of directories to create within
 * directories.
 * <li><code>depth</code>, the depth of the directories hierarchy to create.
 * <li><code>size</code>, the number of documents to store within a directory before
 * creating a new one.TODO?:size is currently not used, but should it be?-rbp
 * </ol>
 * <p>
 * A filesystem repository is always managed by SDX. Documents should not be altered
 * from the outside. From a base directory, SDX will create all the structure it needs.
 *
 */
public class FSRepository extends AbstractDatabaseBackedRepository {

    /**A string representation of the doc subdirectory name. */
//	 FIXME : private field never used locally [MP]
    private final static String DOCS_DIRECTORY = "doc";

    /** A constant for the path property in the databases. */
    private final static String PATH_PROPERTY = "path";

    /** A constant for file prefix when preferred filename is unknown. */
    private final static String FILE_PREFIX = "doc";

    /** A constant for file suffix when preferred filename is unknown. */
    private final static String FILE_SUFFIX = ".sdx";

    /**String representation of the "repository" attribute name "baseDirPath". */
    private final String ATTRIBUTE_BASE_DIRECTORY = "baseDirectory";

    /**String representation of the "repository" attribute name "depth". */
    private final String ATTRIBUTE_DEPTH = "depth";

    /**String representation of the "repository" attribute name "extent". */
    private final String ATTRIBUTE_EXTENT = "extent";

    /** The base directory where to store files. */
    private String baseDirPath;

    /** The base directory for documents. */
    private File docDirectory;


    /** The extent, ie the number of directories within intermediate directories. */
    private int extent;

    /** The depth, ie the depth of the hierarchy of directories before finding files. */
    private int depth;

    /** The format to use for directory names. */
    private DecimalFormat directoryFormat;

    /**
     * Creates a repository.
     *
     * <p>
     * A super.getLog() must be set and then this repository must be configured and initialized.
     *
     * @see #enableLogging
     * @see #configure
     * @see #init
     */
    public FSRepository() {
    }

    /** Configures this repository.
     *
     * <p>
     *  In addition to the parameters needed in the base configuration handled by the parent class,
     *  the following parameters are allowed : a base directory (required) for storage of data,
     *  the number of directories per directory (extent, optional, default is 100),
     *  the depth of the directory structure (depth, optional, default is 3)
     *  and the maximum number of documents per directory.TODO?:size(the latter) is currently not used, but should it be?-rbp
     *
     * @param   configuration   The configuration for this repository (based on a xml file).
     *
     *<p> Sample configuration entry:
     *<p>&lt;sdx:repository sdx:type = "FS" sdx:id = "myRepoId" baseDirectory = "baseDirPath" depth = "2" extent = "50"/>
     *@see #documented_application.xconf we should link to this in the future when we have better documentation capabilities
     *
     */
    public void configure(Configuration configuration) throws ConfigurationException {
    	
        try {
        	
            // Let the superclass handle basic configurations
            loadBaseConfiguration(configuration);
            
            // First check for the base directory information
            String baseDirAtt = configuration.getAttribute(ATTRIBUTE_BASE_DIRECTORY);
            
            boolean isabsolute = false;
            // on veut savoir si l'URL est absolue
            if( baseDirAtt.matches("^[a-z]+:.+") ) {
            	// on a un protocole, on travaille avec
            	try {
            		URL _url = new URL(baseDirAtt);
            		// doit-on attendre d'autres protocoles ? [MP]
            		if( !(_url.getProtocol().equals("file")) ) {
        				String[] args = new String[1];
        				args[0] = baseDirAtt;
        				SDXException e = new SDXException(super.getLog(), SDXExceptionCode.ERROR_BUILD_URL, args, null);
        				throw new ConfigurationException( e.getMessage(), e);
            		}
        			isabsolute = true;
            	} catch (MalformedURLException mfe) {
            		throw new ConfigurationException( mfe.getMessage(), mfe);
            	}
            }
            if( baseDirAtt.startsWith(File.separator) ) isabsolute = true;
            
            // L'URL est absolue, on ne travaille pas avec le datadir de 
            // l'application, qu'il existe ou non
            String datadirpath = null;
            if ( !isabsolute )
            {
	            try {
	            	datadirpath = (String) this._context.get(ContextKeys.SDX.Application.DATADIR_DIRECTORY_PATH);
	            	if( Utilities.checkString(datadirpath) ) {
	            		if ( !datadirpath.endsWith(File.separator) ) datadirpath+=File.separator;
	            		/* INFO (MP) : le basedir peut ne pas être relatif au dossier "conf" contenant l'application.xconf
	            		 * Ici, il faudrait savoir si le fichier de configuration se trouve dans ce dossier
	            		 * Si oui, tout va bien
	            		 * Si non, il faut oublier le datadirpath et prendre le répertoire de ce fichier de configuration
	            		 * comme URL de référence au baseDirAtt. */
	            	}
	    		} catch (ContextException e) {
	    			throw new ConfigurationException(e.getMessage(), e);
	    		}
            }
            
            // controle que tout va bien
            ConfigurationUtils.checkConfAttributeValue(ATTRIBUTE_BASE_DIRECTORY, baseDirAtt, configuration.getLocation());
            
            // maintenant, on resoud la path de l'entrepot
            File repoDir = null;
            try {
            	if ( !isabsolute && Utilities.checkString(datadirpath) )
	            {
	            	/* on a un sdx:application/@dataDir.
	            	 * on l'utilise pour resoudre le path de l'entrepot
	            	 * pas la peine de faire appel a la methode Utilities.resolveFile()
	            	 * elle a deja ete utilisee pour resoudre le dataDir lors
	            	 * de la configuration de l'application. */
	            	baseDirAtt = datadirpath + baseDirAtt;
	            	repoDir = Utilities.checkDirectory( baseDirAtt, super.getLog() );
	            }
	            else
	            {
	            	/* pas de sdx:application/@dataDir, on resoud le path de 
	            	 * l'entrepot en utilisant la method Utilities.resolveFile(). */ 
	            	repoDir = Utilities.resolveFile(super.getLog(), configuration.getLocation(), super.getContext(), baseDirAtt, true);
	            }
            } catch (SDXException sdxE) {
                throw new ConfigurationException(sdxE.getMessage(), sdxE);
            }
            
            // String databaseDirPath = _context.getProperty(Application.REPOSITORIES_DIR_PATH) + baseDirAtt + File.separator;
            // assign a value to the baseDirPath field
            baseDirPath = repoDir.getAbsolutePath() + File.separator;

            // Next check for the extent and depth of the directory structure, and assign values from configuration file, if not use the defaults
            depth = configuration.getAttributeAsInteger(ATTRIBUTE_DEPTH, 3);
            extent = configuration.getAttributeAsInteger(ATTRIBUTE_EXTENT, 100);

            // We must check/create the directory structure

            // First the base directory
            Utilities.checkDirectory(baseDirPath, super.getLog());

            docDirectory = Utilities.checkDirectory(baseDirPath, super.getLog());


        } catch (SDXException e) {
            throw new ConfigurationException(e.getMessage(), e);
        }


    }

    /** Creates the store (creates DB, base file location, etc.) if not already done. */
    public void init() throws SDXException {
        //initializing the super class
        super.init();

        // And then we prepare a decimal format according to the extent.
        int l = (new Integer(extent)).toString().length();
        StringBuffer b = new StringBuffer();
        for (int i = 0; i < l; i++) {
            b.append("0");
        }
        directoryFormat = new DecimalFormat(b.toString());

    }

    /** Returns the number of documents within the store (SDX and attached documents). */
    public long size() throws SDXException {
        return _database.size();
    }

    /** Lists the content as SAX events.
     *
     *	These SAX events represent a simple structure, one element per document,
     *	with a type attribute, and id attribute and may be an URL attribute. The elements
     *	are enclosed within a repository element, which has a type attribute and and id
     *	attribute.
     *
     * NOT YET IMPLEMENTED!
     *
     *	@param	hdl		The SAX content handler to feed with events.
     */
    public void lists(ContentHandler hdl) throws SDXException {
        // TODO : get all files, list them using the yet to be defined XML structure
    }

    /** Adds a document.
     *
     *	@param	doc		The prepared document to add. //TODO : what means "prepared" ? -pb
     *	@param	c		A connection to the store (not used).
     */
    public synchronized void add(Document doc, RepositoryConnection c) throws SDXException {
        //ensuring we have valid objects
        super.add(doc, c);
        // First let's try to store the document on the filesystem.
        String filename = doc.getPreferredFilename();
        File oFile = null;
        try {
            // Where the document will be stored...
            String relativeDir = getRelativeDirectory();
            // The directory path under the root directory
            File storeDir = new File(docDirectory, relativeDir);
            // The complete directory where the document will be stored
            Utilities.checkDirectory(storeDir.getAbsolutePath(), super.getLog());
//            storeDir.mkdirs();

            // The File object for the document
            if (Utilities.checkString(filename))
                oFile = new File(storeDir, filename);

            while (oFile == null || !oFile.exists())//ensuring we have a unique file
                oFile = File.createTempFile(FILE_PREFIX, FILE_SUFFIX, storeDir);

            // Now copy the stream to the file
            InputStream is = doc.openStream();
            OutputStream os = new FileOutputStream(oFile);
            IOUtils.copy(is, os);
            os.close();
            is.close();

            // Next record it's location within the database
            DatabaseEntity ent = new DatabaseEntity(doc.getId());
            ent.enableLogging(super.getLog());
            String relativeFilePath = NetUtils.relativize(docDirectory.toURI().toURL().toExternalForm(), oFile.toURI().toURL().toExternalForm());
            if (Utilities.checkString(relativeFilePath))
                ent.addProperty(PATH_PROPERTY, relativeFilePath);
            else {
            }//TODOException?:send an error here stating a relative path could not be created
//            ent.addProperty(PATH_PROPERTY, relativeDir + File.separator + filename);
            if(_database!=null) _database.save(ent);
            else{
            	throw new SDXException("You must use metadata with FSrepository");
            }

        } catch (IOException e) {
            String[] args = new String[3];
            args[0] = doc.getId();
            args[1] = this.getId();
            args[2] = e.getMessage();
            throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_ADD_DOC, args, e);
        }

    }

    /**
     * Returns a directory to use for storing a file.
     */
    private String getRelativeDirectory() {
        /*
            The number of files that can be stored in the repository
            is given by extent ^ (depth + 1). AS an example, if we have an extent
            of 10 and a depth of 3, we may have 10 ^ 4 = 10 000 files before
            having the repository "full".

            The number of directories in the deepest level is given by the formula
            extent ^ depth, and for our example it is 1000.

            The strategy is to take a random number between 1 and the number of
            directories at the deepest level, 1000 in our example. This random number
            will be the directory where the store will stored.

            But we must also find the intermediate directories. If the random number is
            517, the directory will be :

            <base>/06/02/07

			TODO : not clear to me -pb

            When we get this directory, we must make sure that intermediate directories
            exist.
        */

        // First get the random number
        int ran = (int) Math.round(Math.pow(extent, depth) * Math.random()) + 1;

        // Then iterate on the levels and formats the directory
        StringBuffer sb = null;
        sb = new StringBuffer();
        for (int i = 1; i <= depth; i++) {

            sb.append(File.separator + directoryFormat.format(getDirectory(i, ran)));
        }
        return sb.toString();
    }

    /**
     * Returns a directory number.
     */
    private int getDirectory(int level, int dirNo) {

        /*
            Pour extent = 10 et depth = 3, si j'ai 739, je dois retourner :

                - 8 au premier
                - 4 au second
                - 9 au troisieme

            1: 8 est le ceil de la division de 739 / 100 => (739 -   0) / (extent ^ (depth-level))
            2: 4 est le ceil de la division de 39 / 10   => (739 - 700) / (extent ^ (depth-level))
            3: 9 est le ceil de la division de  9 / 1    => (739 - 730) / (extent ^ (depth-level))

            Donc la partie difficile, c'est le 0, 700, 730, etc.

                1:   0 = 0
                2: 700 = floor( ( 739 / (extent ^ 2) ) ) * (extent ^ 2)
                3: 730 = floor( ( 739 / (extent ^ 1) ) ) * (extent ^ 1)

        */

        int offset = (int) Math.floor(dirNo / Math.pow(extent, (depth - level + 1))) * (int) Math.round(Math.pow(extent, (depth - level + 1)));
        double div = (dirNo - offset) / Math.pow(extent, (depth - level));
        int ret = (int) Math.round(Math.floor(div) + 1);
        return ret;

    }

    /**
     * Feeds a stream with a document.
     *
     *	@param	doc		The document to read.
     *	@param	os		The output stream where to write.
     *	@param	c		A connection to the repository.
     */
    public void get(Document doc, OutputStream os, RepositoryConnection c) throws SDXException {
        //ensuring we have valid objects
        super.get(doc, os, c);
        File docFile = getFile(doc);
        if (docFile == null) {
            String[] args = new String[2];
            args[0] = doc.getId();
            args[1] = this.getId();
            throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_NO_DOC_EXISTS_REPO, args, null);
        } else {
            try {
                InputStream is = new FileInputStream(docFile);
                IOUtils.copy(is, os);
                is.close();
                //supplier of this output stream is responsible for it's management
                //os.close();
            } catch (IOException e) {
                String[] args = new String[3];
                args[0] = doc.getId();
                args[1] = this.getId();
                args[2] = e.getMessage();
                throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_GET_DOC, args, e);
            }
        }

    }

    /**
     * Returns a file for a given document.
     */
    private File getFile(Document doc) throws SDXException {

    	if(_database==null){
    		throw new SDXException("You must use metadata with FSRepository");
    	}
    	else{

    		//ensure that the document is valid
    		Utilities.checkDocument(super.getLog(), doc);
    		String docId = doc.getId();
    		String relativePath = _database.getPropertyValue(docId, PATH_PROPERTY);
    		if (Utilities.checkString(relativePath))
    			return new File(docDirectory, relativePath);
    		else {
    			String[] args = new String[2];
    			args[0] = docId;
    			args[1] = this.getId();
    			throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_NO_DOC_EXISTS_REPO, args, null);
    		}
    	}

    }

    /** Deletes all documents from the repository. */
    public synchronized void empty() throws SDXException {
        if(_database!=null) _database.empty();
        try {
            FileUtils.cleanDirectory(docDirectory);
        } catch (IOException e) {
            String[] args = new String[2];
            args[0] = this.getId();
            args[1] = e.getMessage();
            throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_EMPTY, args, e);
        }
    }

    /** Deletes a document.
     *
     *	@param	doc		The document to delete, the only required field for the document is its "id".
     */
    public synchronized void delete(Document doc, RepositoryConnection c) throws SDXException {
        //ensuring we have valid objects
        super.delete(doc, c);
        File file = null;
        try {
            file = getFile(doc);
        } catch (SDXException e) {
            //we don't need to throw an exception if the file doesn't exist
        }
        if (file != null) file.delete();
        if(_database!=null) _database.delete(new DatabaseEntity(doc.getId()));
    }

    /** Retrieves a SDX document as SAX events.
     *
     *@param	doc		    A ParsableDocument, ie XMLDocument or HTMLDocument.
     *@param consumer       A SAX content handler to feed with events.
     *<p>The wrapped contentHandler for including events within an XSP page contentHandler should be created using
     IncludeXMLConsumer stripper = new IncludeXMLConsumer(xspContentHandler);</p>
     *@param	conn		A connection to the store.
     */
    public void toSAX(ParsableDocument doc, XMLConsumer consumer, RepositoryConnection conn) throws SDXException {
        //in the future, we will implement a FSRepositoryConnection that will keep track of saved/deleted files
        // to be able to rollback if needed, as a result of the RepoConnection
        /*TODORefactor?:can we refactor these methods into abstract repository given the changes i made there with the
        *necessity to make specific calls to the openStream methods of the subclasses?-rbp
        */
        //ensuring we have valid objects
        super.toSAX(doc, consumer, conn);
        SAXParser parser = null;
        ServiceManager l_manager = super.getServiceManager();
        try {
            doc.setContent(this.openStream(doc, null, conn));
            parser = (SAXParser) l_manager.lookup(SAXParser.ROLE);
            doc.parse(parser, consumer);
            //parser.parse(new InputSource(file.toURL().toExternalForm()), consumer);
        } catch (ServiceException e) {
            String[] args = new String[1];
            args[0] = e.getMessage();
            //null super.getLog() passed to prevent double logging
            SDXException sdxE = new SDXException(null, SDXExceptionCode.ERROR_ACQUIRE_PARSER, args, e);

            String[] args2 = new String[2];
            args2[0] = this.getId();
            args2[1] = sdxE.getMessage();
            throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_GET_DOC, args2, sdxE);
        }/*catch (SDXException e) {
            //is this catch necessary, i think the finally will be exectued either way, but for safety we'll catch it now?-rbp
            throw e;
        }*/ finally {
            if (parser != null) l_manager.release(parser);
        }

    }

    /**
     * Opens a stream to read a document.
     *
     *	@param	doc		    A document to read.
     *	@param	encoding    An encoding to use for serialization of XML content (may be null).
     *	@param	c		    A connection to the repository.
     *
     *	@return		The input stream from which the serialized content of the document can be read.
     */

    /**
     * Opens a stream to read a document.
     *
     *	@param	doc		    The document to read.
     *	@param	encoding	The encoding to use for serialization of XML content (may be <code> null</code> ).
     * <p>If <code> null</code> or invalid we use a default.
     * <p>TODOImplement: use of encoding currently not implemented , will do soon.
     *	@param	c		    A connection to the repository.
     *
     *	@return		An input stream from which the serialized content of the document can be read.
     */
    public InputStream openStream(Document doc, String encoding, RepositoryConnection c) throws SDXException {
        //ensuring we have valid objects
        super.openStream(doc, encoding, c);
        return openStream(doc);
    }

    /**
     * Opens a stream to a document managed with this repository.
     */
    private InputStream openStream(Document doc) throws SDXException {
        //first get the file for the document with this id
        File docFile = getFile(doc);
        try {
            if (docFile == null) {
                String[] args = new String[2];
                args[0] = doc.getId();
                args[1] = this.getId();
                throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_NO_DOC_EXISTS_REPO, args, null);
            }

            return new FileInputStream(docFile);

        } catch (FileNotFoundException e) {
            String[] args = new String[3];
            args[0] = doc.getId();
            args[1] = this.getId();
            args[2] = e.getMessage();
            throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_GET_DOC, args, e);
        }

    }

    /** Gets a connection for manipulating store's content.
     *
     */
    public RepositoryConnection getConnection() throws SDXException {
        FSRepositoryConnection conn = new FSRepositoryConnection();
        conn.enableLogging(super.getLog());
        //TODO: determine if this is necessary
        //conn.setUp(this._database.getIndexPath());
        return conn;
    }

    protected boolean initToSax(){
    	if(!super.initToSax())
    		return false;
    	else{
    		this._xmlizable_objects.put("Base_Directory",this.baseDirPath);
    		this._xmlizable_objects.put("File_prefix",FSRepository.FILE_PREFIX);
    		this._xmlizable_objects.put("File_suffix",FSRepository.FILE_SUFFIX);
    		this._xmlizable_objects.put("Extent",Integer.toString(this.extent));
    		this._xmlizable_objects.put("Depth",Integer.toString(this.depth));
    		try{
    			this._xmlizable_objects.put("Document_Count",String.valueOf(this.size()));
    		}catch(SDXException e){
    		}
    		return true;
    	}
    }

    /**Init the LinkedHashMap _xmlizable_volatile_objects with the objects in order to describ them in XML
	 * Some objects need to be refresh each time a toSAX is called*/
	protected void initVolatileObjectsToSax() {
		super.initVolatileObjectsToSax();
		this._xmlizable_objects.put("Extent",Integer.toString(this.extent));
		this._xmlizable_objects.put("Depth",Integer.toString(this.depth));
		try{
			this._xmlizable_objects.put("Document_Count",String.valueOf(this.size()));
		}catch(SDXException e){

		}
	}

	/** Save the repository
	 * @see fr.gouv.culture.sdx.utils.save.Saveable#backup(fr.gouv.culture.sdx.utils.save.SaveParameters)
	 */
	public void backup(SaveParameters save_config) throws SDXException {
		super.backup(save_config);
		if(save_config != null)
			if(save_config.getAttributeAsBoolean(Saveable.ALL_SAVE_ATTRIB,false))
			{
				save_config.setAttribute(Node.Name.TYPE,"FS");

				String savedir = save_config.getStoreBasePath()+save_config.getAttribute(Saveable.PATH_ATTRIB,"");
				ZipWrapper zw = new ZipWrapper();
				zw.zipDirectory(savedir+ File.separator + "repo.zip",docDirectory.getAbsolutePath());
			}
	}

	/** Restore the repository
	 * @see fr.gouv.culture.sdx.utils.save.Saveable#restore(fr.gouv.culture.sdx.utils.save.SaveParameters)
	 */
	public void restore(SaveParameters save_config) throws SDXException {

	}

}
