package org.jboss.seam.security.permission;

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

import org.jboss.seam.annotations.security.permission.Permissions;

/**
 * Permission actions can either be persisted as a comma-separated list of values, or as a
 * bit-masked numerical value where certain bits represent specific actions for that class. This
 * is a helper class that handles the conversion automatically and presents a unified API for 
 * dealing with these persistent actions.  
 *  
 * @author Shane Bryzak
 */
public class PermissionMetadata implements Serializable
{
   private static final long serialVersionUID = 7407543457388705950L;
   private Map<Class<?>,Boolean> usesActionMask = new HashMap<Class<?>,Boolean>();
   private Map<Class<?>,Map<String,Long>> classActions = new HashMap<Class<?>,Map<String,Long>>();
   
   private synchronized void initClassActions(Class<?> cls)
   {
      if (!classActions.containsKey(cls))
      {
         Map<String,Long> actions = new HashMap<String,Long>();
         
         boolean useMask = false;
         
         Permissions p = (Permissions) cls.getAnnotation(Permissions.class);
         if (p != null)
         {
            org.jboss.seam.annotations.security.permission.Permission[] permissions = p.value();
            if (permissions != null)
            {
               for (org.jboss.seam.annotations.security.permission.Permission permission : permissions)
               {
                  actions.put(permission.action(), permission.mask());
                  
                  if (permission.mask() != 0)
                  {
                     useMask = true;
                  }
               }
            }            
         }
         
         // Validate that all actions have a proper mask
         if (useMask)
         {
            Set<Long> masks = new HashSet<Long>();
            
            for (String action : actions.keySet())
            {
               Long mask = actions.get(action);
               if (masks.contains(mask))
               {
                  throw new IllegalArgumentException("Class " + cls.getName() + 
                        " defines a duplicate mask for permission action [" + action + "]");
               }
                              
               if (mask == 0)
               {
                  throw new IllegalArgumentException("Class " + cls.getName() +
                        " must define a valid mask value for action [" + action + "]");
               }
               
               if ((mask & (mask - 1)) != 0)
               {
                  throw new IllegalArgumentException("Class " + cls.getName() +
                        " must define a mask value that is a power of 2 for action [" + action + "]");
               }
               
               masks.add(mask);               
            }
         }

         usesActionMask.put(cls, useMask);
         classActions.put(cls, actions);
      }
   }
   
   protected class ActionSet
   {
      private Set<String> members = new HashSet<String>();
      private Class<?> targetClass;
      
      public ActionSet(Class<?> targetClass, String members)
      {
         this.targetClass = targetClass;
         addMembers(members);
      }
      
      public void addMembers(String members)
      {
         if (members == null) return;
         
         if (usesActionMask.get(targetClass))
         {
            // bit mask-based actions
            long vals = Long.valueOf(members);
            
            Map<String,Long> actions = classActions.get(targetClass);
            for (String action : actions.keySet())
            {
               long mask = actions.get(action).longValue();
               if ((vals & mask) != 0)
               {
                  this.members.add(action);
               }
            }            
         }
         else
         {
            // comma-separated string based actions
            String[] actions = members.split(",");
            for (String action : actions)
            {
               this.members.add(action);
            }
         }         
      }
      
      public boolean contains(String action)
      {
         return members.contains(action);
      }
      
      public ActionSet add(String action)
      {
         members.add(action);
         return this;
      }
      
      public ActionSet remove(String action)
      {
         members.remove(action);
         return this;
      }
      
      public Set<String> members()
      {
         return members;
      }
      
      public boolean isEmpty()
      {
         return members.isEmpty();
      }
      
      @Override
      public String toString()
      {        
         if (usesActionMask.get(targetClass))
         {
            Map<String,Long> actions = classActions.get(targetClass);
            long mask = 0;
            
            for (String member : members)
            {
               mask |= actions.get(member).longValue();
            }
            
            return "" + mask;
         }
         else
         {
            StringBuilder sb = new StringBuilder();
            for (String member : members)
            {
               if (sb.length() > 0) sb.append(',');
               sb.append(member);
            }
            return sb.toString();            
         }
      }
   }
   
   public ActionSet createActionSet(Class<?> targetClass, String members)
   {      
      if (!classActions.containsKey(targetClass)) initClassActions(targetClass);
      
      return new ActionSet(targetClass, members);
   }
   
   public List<String> listAllowableActions(Class<?> targetClass)
   {
      if (!classActions.containsKey(targetClass)) initClassActions(targetClass);
      
      List<String> actions = new ArrayList<String>();
      for (String action : classActions.get(targetClass).keySet())
      {
         actions.add(action);
      }
      
      return actions;
   }
}
