/*
 * 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.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.jboss.logging.Logger;
import org.jboss.profileservice.spi.Profile;
import org.jboss.profileservice.spi.ProfileKey;
import org.jboss.profileservice.spi.metadata.ProfileKeyMetaData;
import org.jboss.profileservice.spi.metadata.ProfileMetaData;
import org.jboss.profileservice.spi.metadata.SubProfileMetaData;

/**
 * The abstract profile factory.
 * 
 * @author <a href="mailto:emuckenh@redhat.com">Emanuel Muckenhuber</a>
 * @version $Revision: 86739 $
 */
public abstract class AbstractBootstrapProfileFactory
{
   /** The profile map. */
   private Map<ProfileKey, ProfileMetaData> profileMap = new HashMap<ProfileKey, ProfileMetaData>();
   
   /** The profiles map. */
   private Map<ProfileKey, List<ProfileMetaData>> profilesMetaData = new HashMap<ProfileKey, List<ProfileMetaData>>();
   
   /** The profile factory. */
   private AbstractProfileFactory profileFactory;
   
   /** The logger */
   protected final Logger log = Logger.getLogger(getClass()); 

   public AbstractProfileFactory getProfileFactory()
   {
      return profileFactory;
   }
   
   public void setProfileFactory(AbstractProfileFactory profileFactory)
   {
      this.profileFactory = profileFactory;
   }
   
   /**
    * Create the meta data required for this profile.
    * 
    * @param key the root profile key.
    * @throws Exception
    */
   protected abstract void createProfileMetaData(ProfileKey key, URL url) throws Exception;  
   
   /**
    * Create the profiles required for this profile. 
    * 
    * @param rootKey the root profile key.
    * @param uri the profile root uri for parsing.
    * @return a collection of profiles
    * @throws Exception
    */
   public Collection<Profile> createProfiles(ProfileKey rootKey, URL url) throws Exception
   {
      if(rootKey == null)
         throw new IllegalArgumentException("Null profile key");
         
      // Create the profile meta data.
      createProfileMetaData(rootKey, url);
      
      Map<ProfileKey, Profile> profiles = new HashMap<ProfileKey, Profile>();
      // Create the real profiles
      createProfiles(profiles, rootKey);
      
      return profiles.values();
   }

   /**
    * Create profiles
    * 
    * @param profiles the profiles map 
    * @param key the ProfileKey
    * @throws Exception
    */
   public void createProfiles(Map<ProfileKey, Profile> profiles, ProfileKey key) throws Exception
   {
      ProfileMetaData profileMetaData = this.profileMap.get(key);
      if(profileMetaData == null)
         throw new IllegalStateException("Could not find metaData for key: " + key);

      List<ProfileKey> subProfiles = new ArrayList<ProfileKey>();
      
      // Create the profile
      createProfile(profiles, subProfiles, key, profileMetaData);

   }

   /**
    * Create a Profile with it's sub-profiles.
    * 
    * @param profiles the profiles map
    * @param subProfiles the sub-profiles list
    * @param key the ProfileKey
    * @param profileMetaData the profile meta data
    * @throws Exception
    */
   public void createProfile(Map<ProfileKey, Profile> profiles, List<ProfileKey> subProfiles, ProfileKey key, ProfileMetaData profileMetaData) throws Exception
   {
      // Don't process a profile twice
      if(profiles.containsKey(key))
         return;
      
      if(log.isTraceEnabled())
         log.trace("Creating profile for key: " + key);
      
      // First recursively process the sub profiles
      processSubProfiles(profiles, subProfiles, profileMetaData.getSubprofiles());
      
      // Create the profile
      // Provide a new copy of the keys
      Profile profile = profileFactory.createProfile(key, profileMetaData, new ArrayList<ProfileKey>(subProfiles));
      
      // Add to the profileMap
      profiles.put(key, profile);
      
      // FIXME add a implicit dependency for the next
      if(subProfiles.contains(key) == false)
         subProfiles.add(key);
   }
   
   /**
    * Process the sub-profiles.
    * 
    * @param profiles the profiles map
    * @param subProfileKeys the sub-profiles
    * @param subProfilesMetaData a list of sub-profile metadata 
    * @throws Exception
    */
   private void processSubProfiles(Map<ProfileKey, Profile> profiles, List<ProfileKey> subProfileKeys, List<SubProfileMetaData> subProfilesMetaData) throws Exception
   {
      if(subProfilesMetaData == null || subProfilesMetaData.isEmpty())
         return;
      
      for(SubProfileMetaData subProfile : subProfilesMetaData)
      {
         ProfileKey subProfileKey = createProfileKey(subProfile);
         if(this.profileMap.containsKey(subProfileKey))
         {
            createProfile(profiles, subProfileKeys, subProfileKey, this.profileMap.get(subProfileKey));
         }
         else if(this.profilesMetaData.containsKey(subProfileKey))
         {
            List<ProfileMetaData> subProfileAliases = this.profilesMetaData.get(subProfileKey);
            processSubProfileAlias(profiles, subProfileKeys, subProfileAliases);
         }
         else
         {
            throw new IllegalStateException("Could not resolve profile meta data for key: " + subProfileKey);
         }
      }     
   }
   
   /**
    * Process the profiles alias for a sub-profile.
    * 
    * @param profiles the profiles map
    * @param subProfileKeys the sub-profiles
    * @param subprofileAliases the profile meta data for the sub-profiles
    * @throws Exception
    */
   private void processSubProfileAlias(Map<ProfileKey, Profile> profiles, List<ProfileKey> subProfileKeys, List<ProfileMetaData> subprofileAliases) throws Exception
   {
      if(subprofileAliases == null || subprofileAliases.isEmpty())
         return;
      
      for(ProfileMetaData metaData : subprofileAliases)
      {
         ProfileKey key = createProfileKey(metaData);
         createProfile(profiles, subProfileKeys, key, metaData);
      }
   }
   
   /**
    * Add profile meta data.
    * 
    * @param key the profile key.
    * @param metaData the profile meta data.
    */
   protected void addProfile(ProfileKey key, ProfileMetaData metaData)
   {
      // The keys have be unique
      if(this.profileMap.containsKey(key))
         throw new IllegalStateException("Duplicate key: " + key);

      if(this.profilesMetaData.containsKey(key))
         throw new IllegalStateException("Duplicate key: " + key);
      
      this.profileMap.put(key, metaData);
   }
   
   /**
    * Add the profiles meta data, which is basically is a reference to
    * a list of profile meta data.
    * 
    * @param key the profile key.
    * @param metaData a list of profile meta data.
    */
   protected void addProfiles(ProfileKey key, List<ProfileMetaData> metaData)
   {
      if(this.profileMap.containsKey(key))
      {
         // Ignore the default key, which gets generated for <profiles/>
         if(ProfileKey.DEFAULT.equals(key.getDomain())
               && ProfileKey.DEFAULT.equals(key.getServer())
               && ProfileKey.DEFAULT.equals(key.getName()))
         {
            return;
         }
         else
         {
            throw new IllegalStateException("Duplicate key: " + key);  
         }
      }

      // Create a entry for <profiles>
      List<ProfileMetaData> profileList = this.profilesMetaData.get(key);
      if(profileList == null)
      {
         profileList = new ArrayList<ProfileMetaData>();
         this.profilesMetaData.put(key, profileList);
      }
      
      if(metaData != null && metaData.isEmpty() == false)
      {
         profileList.addAll(metaData);
      }
   }
   
   public static ProfileKey createProfileKey(ProfileKeyMetaData md)
   {
      return new ProfileKey(md.getDomain(), md.getServer(), md.getName());
   }
}