/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2007, 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.deployers.vfs.spi.deployer;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.HashMap;

import org.jboss.deployers.spi.DeploymentException;
import org.jboss.deployers.spi.deployer.helpers.AbstractParsingDeployerWithOutput;
import org.jboss.deployers.structure.spi.DeploymentUnit;
import org.jboss.deployers.vfs.spi.structure.VFSDeploymentUnit;
import org.jboss.virtual.VirtualFile;

/**
 * AbstractVFSParsingDeployer.
 *
 * @param <T> the type of output
 * @author <a href="adrian@jboss.org">Adrian Brock</a>
 * @author <a href="ales.justin@jboss.org">Ales Justin</a>
 * @version $Revision: 1.1 $
 */
public abstract class AbstractVFSParsingDeployer<T> extends AbstractParsingDeployerWithOutput<T> implements FileMatcher
{
   /** The alt mappings key */
   private static final String ALT_MAPPINGS_MAP_KEY = "AltMappingsMap";

   /** The allow multiple fiels flag */
   private boolean allowMultipleFiles;

   /**
    * Create a new AbstractVFSParsingDeployer.
    * 
    * @param output the type of output
    * @throws IllegalArgumentException for null output
    */
   public AbstractVFSParsingDeployer(Class<T> output)
   {
      super(output);
   }

   public boolean isDeployable(VirtualFile file)
   {
      String fileName = file.getName();
      String suffix = getSuffix();
      if (suffix == null)
         return getNames() != null && getNames().contains(fileName);
      else
         return fileName.endsWith(suffix);
   }

   /**
    * Callback to do prechecking on the deployment
    * 
    * @param unit the unit
    * @return true by default
    * @throws DeploymentException for any error
    */
   protected boolean accepts(VFSDeploymentUnit unit) throws DeploymentException
   {
      return true;
   }

   @Override
   protected boolean accepts(DeploymentUnit unit) throws DeploymentException
   {
      // Ignore non-vfs deployments
      if (unit instanceof VFSDeploymentUnit == false)
      {
         log.trace("Not a vfs deployment: " + unit.getName());
         return false;
      }
      return accepts((VFSDeploymentUnit) unit);
   }

   /**
    * Open stream and validate if not null.
    *
    * @param file the virtual file
    * @return non-null input stream
    * @throws Exception for any error or if file's stream is null
    */
   protected InputStream openStreamAndValidate(VirtualFile file) throws Exception
   {
      if (file == null)
         throw new IllegalArgumentException("Null file");

      InputStream inputStream = SecurityActions.openStream(file);
      if (inputStream == null)
         throw new IOException("Null file stream: " + file);

      return inputStream;
   }

   /**
    * Get the alt mappings map.
    *
    * @param unit the deployment unit
    * @return alt mappings map from attachments
    */
   @SuppressWarnings("unchecked")
   protected static Map<String, Class<?>> getAltMappings(DeploymentUnit unit)
   {
      if (unit == null)
         throw new IllegalArgumentException("Null deployment unit");

      return unit.getAttachment(ALT_MAPPINGS_MAP_KEY, Map.class);
   }

   /**
    * Get metadata file.
    * First try altDD, then fallback to original name.
    *
    * @param unit the vfs deployment unit
    * @param altExpectedClass the expected class of altDD
    * @param originalName the original file name
    * @param checkMetaDataFile should we fall back to metadata file
    * @return metadata file or null if it doesn't exist
    */
   protected VirtualFile getMetadataFile(VFSDeploymentUnit unit, Class<?> altExpectedClass, String originalName, boolean checkMetaDataFile)
   {
      String altPrefix = (altExpectedClass != null ? altExpectedClass.getName() : originalName);
      VirtualFile file = unit.getAttachment(altPrefix + ".altDD", VirtualFile.class);

      if (file != null && altExpectedClass != null)
      {
         Map<String, Class<?>> altMappingsMap = getAltMappings(unit);
         if (altMappingsMap == null)
         {
            altMappingsMap = new HashMap<String, Class<?>>();
            unit.addAttachment(ALT_MAPPINGS_MAP_KEY, altMappingsMap, Map.class);
         }
         altMappingsMap.put(file.getName(), altExpectedClass);
      }
      if(checkMetaDataFile && file == null)
         file = unit.getMetaDataFile(originalName);

      return file;
   }

   /**
    * Match file name to metadata class.
    *
    * @param unit the deployment unit
    * @param fileName the file name
    * @return matching metadata class
    */
   protected Class<?> matchFileToClass(DeploymentUnit unit, String fileName)
   {
      Map<String, Class<?>> altMappingsMap = getAltMappings(unit);
      return altMappingsMap != null ? altMappingsMap.get(fileName) : null;
   }

   @Override
   protected T parse(DeploymentUnit unit, String name, T root) throws Exception
   {
      if (ignoreName(unit, name))
         return null;

      // Try to find the metadata
      VFSDeploymentUnit vfsDeploymentUnit = (VFSDeploymentUnit) unit;

      VirtualFile file = getMetadataFile(vfsDeploymentUnit, getOutput(), name, true);
      return (file != null) ? parseAndInit(vfsDeploymentUnit, file, root) : null;
   }

   protected T parse(DeploymentUnit unit, Set<String> names, T root) throws Exception
   {
      if (names == null || names.isEmpty())
         throw new IllegalArgumentException("Null or empty names.");

      VFSDeploymentUnit  vfsDeploymentUnit = (VFSDeploymentUnit)unit;

      List<VirtualFile> files = new ArrayList<VirtualFile>();
      Set<String> missingFiles = new HashSet<String>();
      Set<String> ignoredFiles = new HashSet<String>();

      for (String name : names)
      {
         if (ignoreName(unit, name))
         {
            ignoredFiles.add(name);
         }
         else
         {
            VirtualFile file = getMetadataFile(vfsDeploymentUnit, matchFileToClass(unit, name), name, true);
            if (file != null)
               files.add(file);
            else
               missingFiles.add(name);
         }
      }

      if (missingFiles.size() + ignoredFiles.size() == names.size())
         return null;

      return mergeFiles(vfsDeploymentUnit, root, files, missingFiles);
   }

   @Override
   protected T parse(DeploymentUnit unit, String name, String suffix, T root) throws Exception
   {
      // Should we include the deployment
      // The infrastructure will only check leafs anyway so no need to check here
      if (name == null && isIncludeDeploymentFile())
         name = unit.getName();

      // Try to find the metadata
      VFSDeploymentUnit vfsDeploymentUnit = (VFSDeploymentUnit) unit;

      // let's check altDD first
      VirtualFile file = getMetadataFile(vfsDeploymentUnit, getOutput(), name, false);
      if (file != null)
         return parseAndInit(vfsDeploymentUnit, file, root, true);

      // try all name+suffix matches
      List<VirtualFile> files = vfsDeploymentUnit.getMetaDataFiles(name, suffix);
      switch (files.size())
      {
         case 0 :
            return null;
         case 1 :
            return parseAndInit(vfsDeploymentUnit, files.get(0), root, true);
         default :
            return handleMultipleFiles(vfsDeploymentUnit, root, files);
      }
   }

   /**
    * Parse the file, initialize the result if exists.
    *
    * @param unit the deployment unit
    * @param file the file
    * @param root the root
    * @return parsed result
    * @throws Exception for any error
    */
   protected T parseAndInit(VFSDeploymentUnit unit, VirtualFile file, T root) throws Exception
   {
      return parseAndInit(unit, file, root, false);
   }

   /**
    * Parse the file, initialize the result if exists.
    *
    * @param unit the deployment unit
    * @param file the file
    * @param root the root
    * @param checkIgnore do we check for ignored names
    * @return parsed result
    * @throws Exception for any error
    */
   protected T parseAndInit(VFSDeploymentUnit unit, VirtualFile file, T root, boolean checkIgnore) throws Exception
   {
      if (checkIgnore && ignoreName(unit, file.getName()))
         return null;

      T result = parse(unit, file, root);
      if (result != null)
         init(unit, result, file);
      return result;
   }

   protected T parse(DeploymentUnit unit, Set<String> names, String suffix, T root) throws Exception
   {
      if (names == null || names.isEmpty())
         throw new IllegalArgumentException("Null or empty names.");

      VFSDeploymentUnit  vfsDeploymentUnit = (VFSDeploymentUnit)unit;

      List<VirtualFile> files = new ArrayList<VirtualFile>();
      Set<String> missingFiles = new HashSet<String>();
      Set<String> ignoredFiles = new HashSet<String>();

      for (String name : names)
      {
         if (ignoreName(unit, name))
         {
            ignoredFiles.add(name);
         }
         else
         {
            // try finding altDD file
            VirtualFile file = getMetadataFile(vfsDeploymentUnit, matchFileToClass(unit, name), name, false);
            if (file == null)
            {
               List<VirtualFile> matched = vfsDeploymentUnit.getMetaDataFiles(name, suffix);
               if (matched != null && matched.isEmpty() == false)
                  files.addAll(matched);
               else
                  missingFiles.add(name);
            }
            else
            {
               files.add(file);
            }
         }
      }

      if (missingFiles.size() + ignoredFiles.size() == names.size())
         return null;

      return mergeFiles(vfsDeploymentUnit, root, files, missingFiles);
   }


   /**
    * Merge files into one piece of metatdata
    *
    * @param unit the unit
    * @param root possibly null pre-existing root
    * @param files matching meta files
    * @param missingFiles file names that are missing matching file
    * @return merged metadata
    * @throws Exception for any error
    */
   protected T mergeFiles(VFSDeploymentUnit unit, T root, List<VirtualFile> files, Set<String> missingFiles) throws Exception
   {
      return null;
   }

   /**
    * Handle multiple files.
    *
    * @param unit the vfs deployment unit
    * @param root possibly null pre-existing root
    * @param files the matching files
    * @return null or merged single result
    * @throws Exception for any error
    */
   protected T handleMultipleFiles(VFSDeploymentUnit unit, T root, List<VirtualFile> files) throws Exception
   {
      if (allowsMultipleFiles(files) == false)
         throw new IllegalArgumentException("Multiple matching files not allowed: " + files);

      for (VirtualFile file : files)
      {
         if (ignoreName(unit, file.getName()) == false)
         {
            T result = parse(unit, file, root);
            if (result != null)
            {
               init(unit, result, file);
               unit.addAttachment(file.toURL().toString(), result, getOutput());
            }
         }
      }

      return null;
   }

   /**
    * Check if we allow multiple files.
    *
    * Make sure you have deployers down
    * the chain that will handle generated
    * multiple attachments if this method returns true.
    * 
    * @param files the matching files
    * @return true if we allow, false otherwise
    */
   protected boolean allowsMultipleFiles(List<VirtualFile> files)
   {
      return allowMultipleFiles;
   }

   /**
    * Parse a deployment
    * 
    * @param unit the deployment unit
    * @param file the metadata file
    * @param root - possibly null pre-existing root
    * @return the metadata
    * @throws Exception for any error
    */
   protected abstract T parse(VFSDeploymentUnit unit, VirtualFile file, T root) throws Exception;
   
   /**
    * Initialise the metadata
    * 
    * @param unit the unit
    * @param metaData the metadata
    * @param file the metadata file
    * @throws Exception for any error
    */
   protected void init(VFSDeploymentUnit unit, T metaData, VirtualFile file) throws Exception
   {
   }

   /**
    * Set allow multiple files.
    *
    * @param allowMultipleFiles the allow multiple files flag
    */
   public void setAllowMultipleFiles(boolean allowMultipleFiles)
   {
      this.allowMultipleFiles = allowMultipleFiles;
   }
}
