/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2008, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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 along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.system.server.profileservice.repository;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Formatter;
import java.util.List;
import java.util.Map;

import org.jboss.deployers.client.plugins.deployment.AbstractDeployment;
import org.jboss.deployers.client.spi.Deployment;
import org.jboss.deployers.spi.attachments.AttachmentsFactory;
import org.jboss.deployers.spi.attachments.MutableAttachments;
import org.jboss.deployers.structure.spi.DeploymentContext;
import org.jboss.deployers.structure.spi.main.MainDeployerInternals;
import org.jboss.deployers.vfs.spi.client.VFSDeploymentFactory;
import org.jboss.deployers.vfs.spi.structure.VFSDeploymentContext;
import org.jboss.logging.Logger;
import org.jboss.managed.api.ManagedCommon;
import org.jboss.managed.api.ManagedComponent;
import org.jboss.managed.api.ManagedObject;
import org.jboss.profileservice.spi.ProfileDeployment;
import org.jboss.system.server.profileservice.attachments.AttachmentMetaData;
import org.jboss.system.server.profileservice.attachments.AttachmentStore;
import org.jboss.system.server.profileservice.attachments.RepositoryAttachmentMetaData;
import org.jboss.system.server.profileservice.attachments.RepositoryAttachmentMetaDataFactory;
import org.jboss.system.server.profileservice.persistence.PersistenceFactory;
import org.jboss.system.server.profileservice.persistence.xml.PersistenceRoot;
import org.jboss.virtual.VirtualFile;

/**
 * The AbstractAttachmentStore.
 *
 * @author <a href="mailto:emuckenh@redhat.com">Emanuel Muckenhuber</a>
 * @version $Revision: 99302 $
 */
public class AbstractAttachmentStore implements AttachmentStore
{

   /** The metadata name */
   public static final String METADATA_NAME = "metadata";

   /** The logger. */
   private static final Logger log = Logger.getLogger(AbstractAttachmentStore.class);

   /** The vfs deployment factory. */
   private static final VFSDeploymentFactory deploymentFactory = VFSDeploymentFactory.getInstance();

   /** The attachment store root. */
   private final URI attachmentStoreRoot;

   /** The attachment serializer. */
   private AbstractFileAttachmentsSerializer serializer;

   /** The main deployer. */
   private MainDeployerInternals mainDeployer;

   /** The persistence factory. */
   private PersistenceFactory persistenceFactory;

   protected static URI getURI(File root)
   {
      if(root == null)
         throw new IllegalArgumentException("Null attachment root.");
      if(root.exists() && root.isDirectory() == false)
         throw new IllegalArgumentException("Attachment root is not a directory.");
      return root.toURI();
   }

   public AbstractAttachmentStore(File root)
   {
      this(getURI(root));
   }

   public AbstractAttachmentStore(URI uri)
   {
      if(uri == null)
         throw new IllegalArgumentException("Null uri.");

      this.attachmentStoreRoot = uri;
   }

   public URI getAttachmentStoreRoot()
   {
      return this.attachmentStoreRoot;
   }

   public MainDeployerInternals getMainDeployer()
   {
      return mainDeployer;
   }

   public void setMainDeployer(MainDeployerInternals mainDeployer)
   {
      this.mainDeployer = mainDeployer;
   }

   public AbstractFileAttachmentsSerializer getSerializer()
   {
      return this.serializer;
   }

   public void setSerializer(AbstractFileAttachmentsSerializer serializer)
   {
      this.serializer = serializer;
   }

   public PersistenceFactory getPersistenceFactory()
   {
      return persistenceFactory;
   }

   public void setPersistenceFactory(PersistenceFactory persistenceFactory)
   {
      this.persistenceFactory = persistenceFactory;
   }

   public Deployment createDeployment(ProfileDeployment deployment) throws Exception
   {
      Deployment mcDeployment = null;
      if(deployment.getRoot() == null)
         mcDeployment = new AbstractDeployment(deployment.getName());
      else
         mcDeployment = deploymentFactory.createVFSDeployment(deployment.getRoot());

      // Add the deployment attachments as PredeterminedManagedObjects
      Map<String, Object> attachments = deployment.getAttachments();
      if(attachments != null && attachments.isEmpty() == false)
      {
         MutableAttachments predetermined = AttachmentsFactory.createMutableAttachments();
         for(String name : attachments.keySet())
            predetermined.addAttachment(name, attachments.get(name));
         mcDeployment.setPredeterminedManagedObjects(predetermined);
      }
      return mcDeployment;
   }

   /**
    * Load the attachment metadata for a deployment.
    *
    * @param relativeDeploymentPath the relative path
    * @return the attachment metadata
    */
   protected RepositoryAttachmentMetaData loadAttachmentMetaData(String relativeDeploymentPath)
   {
      // attachments/simpleName - hash/metadata.xml
      String fixedMetadataPath = getMetaDataPathName(relativeDeploymentPath);

      try
      {  // Try to load the repository attachment metadata
         return getSerializer().loadAttachment(fixedMetadataPath, RepositoryAttachmentMetaData.class);
      }
      catch(Exception e)
      {
         log.error("Failed to load attachment metadata from relative path: "+ relativeDeploymentPath, e);
      }
      return null;
   }

   public RepositoryAttachmentMetaData loadMetaData(VirtualFile deploymentRoot) throws Exception
   {
      if(deploymentRoot == null)
         throw new IllegalArgumentException("Null deployment root.");

      String deploymentPath = createRelativeDeploymentPath(deploymentRoot);
      return loadAttachmentMetaData(deploymentPath);
   }

   public void removeComponent(String ctx, ManagedComponent comp) throws Exception
   {
      if(ctx == null)
         throw new IllegalArgumentException("null deployment ctx name");
      if(comp == null)
         throw new IllegalArgumentException("null managed component");

      saveAttachment(ctx, comp, true);
   }

   public void updateDeployment(String ctx, ManagedComponent comp) throws Exception
   {
      if(ctx == null)
         throw new IllegalArgumentException("null deployment ctx name");
      if(comp == null)
         throw new IllegalArgumentException("null managed component");

      saveAttachment(ctx, comp, false);
   }

   public void saveAttachment(String deploymentName, ManagedComponent component, boolean remove) throws Exception
   {
      VFSDeploymentContext ctx = getDeploymentContext(deploymentName);
      if(ctx == null)
         throw new IllegalStateException("Cannot persist attachment, failed to find deployment: " + deploymentName);

      // Get the root
      VirtualFile root = ctx.getRoot();
      String deploymentPath = createRelativeDeploymentPath(root);

      // Load previous saved information
      RepositoryAttachmentMetaData repositoryMetaData = loadAttachmentMetaData(deploymentPath);
      if(repositoryMetaData == null)
      {
         repositoryMetaData = RepositoryAttachmentMetaDataFactory.createInstance();
         repositoryMetaData.setDeploymentName(root.getName());
      }

      // Get the parent MO
      ManagedCommon parent = component;
      while(parent.getParent() != null)
         parent = parent.getParent();

      // Get the managed object, as a component can also be a child of a managedObject
      ManagedObject managedObject = component.getDeployment().getManagedObject(parent.getName());
      if(managedObject == null && parent instanceof ManagedObject)
         managedObject = (ManagedObject) parent;

      // Get the current attachment
      String attachmentName = managedObject.getAttachmentName();
      List<AttachmentMetaData> attachments = repositoryMetaData.getAttachments();
      if(attachments == null)
      {
         attachments = new ArrayList<AttachmentMetaData>();
         repositoryMetaData.setAttachments(attachments);
      }
      // Extract the attachment
      AttachmentMetaData attachment = null;
      for(AttachmentMetaData a : attachments)
      {
         if(attachmentName.equals(a.getName()))
            attachment = a;
      }

      // Create a new one
      if(attachment == null)
      {
         // Create attachment meta data
         attachment = new AttachmentMetaData();
         // Add attachment meta data
         attachments.add(attachment);
      }

      // Is attachmentName the same as the className ?
      attachment.setName(attachmentName);
      attachment.setClassName(managedObject.getAttachment().getClass().getName());

      // Save the attachment
      String attachmentPath = deploymentPath + attachment.getName();
      // Create the persistence information
      PersistenceRoot persistenceRoot = getSerializer().loadAttachment(attachmentPath, PersistenceRoot.class);
      //
      persistenceRoot = createPersistedMetaData(persistenceRoot, managedObject, component, remove);
      // Serialize the attachment
      getSerializer().saveAttachment(attachmentPath, persistenceRoot);

      // Update the last modified.
      long lastModified = System.currentTimeMillis();
      attachment.setLastModified(lastModified);
      repositoryMetaData.setLastModified(lastModified);

      //  Save the updated repository meta data
      getSerializer().saveAttachment(getMetaDataPathName(deploymentPath), repositoryMetaData);
   }

   /**
   * create the xml meta data for persisting the managed object.
   *
   * @param parent the parent managed object.
   * @param the managed object
   * @param handler the persistence handler
   * @return the xml metadata.
   */
  protected PersistenceRoot createPersistedMetaData(PersistenceRoot root, ManagedObject managedObject, ManagedComponent component, boolean remove)
  {
     if(root == null)
        root = new PersistenceRoot();

     if(remove)
     {
        root = this.persistenceFactory.removeComponent(root, managedObject, component);
     }
     else
     {
        root = this.persistenceFactory.updateComponent(root, managedObject, component);
     }
     if(root.getName() == null)
        root.setName(managedObject.getAttachmentName());
     if(root.getClassName() == null)
        root.setClassName(managedObject.getAttachment().getClass().getName());

     return root;
  }



   public PersistenceRoot loadAttachment(VirtualFile deploymentCtx, AttachmentMetaData attachment) throws Exception
   {
      if(deploymentCtx == null)
         throw new IllegalArgumentException("Null deployment root.");
      if(attachment == null)
         throw new IllegalArgumentException("Null attachment");

      String deploymentPath = createRelativeDeploymentPath(deploymentCtx);

      // Load
      String attachmentPath = deploymentPath + attachment.getName();
      return getSerializer().loadAttachment(attachmentPath, PersistenceRoot.class);
   }

   /**
    * Get the metadata path, based on a relative path.
    *
    * @param deploymentPath the relative path to the deployment
    * @return
    */
   protected String getMetaDataPathName(String deploymentPath)
   {
      return deploymentPath.endsWith(File.separator) ? deploymentPath + METADATA_NAME : deploymentPath + File.separator + METADATA_NAME;
   }

   /**
    * Create the relative path to the persisted deployment attachment meta data.
    * The string is simpleName + "-" + hash (based on the URI of the deployment)
    *
    * @param deployment the deployment
    * @return the relative name
    * @throws Exception
    */
   protected String createRelativeDeploymentPath(VirtualFile vf) throws Exception
   {
      if(vf == null)
         throw new IllegalStateException("Null deployment.");

      // deployment URI
      String pathName = vf.toURI().toString();
      String fileName = vf.getName();
      // Generate hash
      String hash = HashGenerator.createHash(pathName);
      // simple name + "-" + hash
      return fileName + "-" + hash + File.separator;

   }

   /**
    * Get deployment context.
    *
    * @param name the deployment context name
    * @return vfs deployment context or null if doesn't exist or not vfs based
    */
   @SuppressWarnings("deprecation")
   protected VFSDeploymentContext getDeploymentContext(String name)
   {
      if (mainDeployer == null)
         throw new IllegalStateException("Null main deployer.");

      DeploymentContext deploymentContext = mainDeployer.getDeploymentContext(name);
      if (deploymentContext == null || deploymentContext instanceof VFSDeploymentContext == false)
         return null;

      return (VFSDeploymentContext)deploymentContext;
   }

   private static class HashGenerator
   {
      /** The digest. */
      private static MessageDigest digest;

      /**
       * Create a hash based on a deployment vfs path name.
       *
       * @param deployment the deployment
       * @return a hash
       * @throws NoSuchAlgorithmException
       * @throws MalformedURLException
       * @throws URISyntaxException
       */
      public static String createHash(String pathName)
            throws NoSuchAlgorithmException, MalformedURLException, URISyntaxException
      {
         // buffer
         StringBuffer buffer = new StringBuffer();
         // formatter
         Formatter f = new Formatter(buffer);
         // get the bytez
         byte[] bytez = internalCreateHash(pathName);
         for(byte b : bytez)
         {
            // format the byte
            f.format("%02x", b);
         }
         // toString
         return f.toString();
      }

      protected static byte[] internalCreateHash(String pathName) throws NoSuchAlgorithmException
      {
         MessageDigest digest = getDigest();
         try
         {
            // update
            digest.update(pathName.getBytes());
            // return
            return digest.digest();
         }
         finally
         {
            // reset
            digest.reset();
         }
      }

      public static MessageDigest getDigest() throws NoSuchAlgorithmException
      {
         if(digest == null)
            digest = MessageDigest.getInstance("MD5");

         return digest;
      }
   }

}
