/*
** Module   : ResourcePool.java
** Abstract : manages scoped resources like variables and semaphores
**
** Copyright (c) 2009-2021, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 GES 20090220 Manages scoped resources like variables and semaphores.
** 002 CA  20210827 Refactored to allow other test types, beside terminal (which can be executed from the same 
**                  test suite).  Added SOAP and REST web service tests.
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program 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 Affero General Public License for more details.
**
** You should have received a copy of the GNU Affero General Public License
** along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package com.goldencode.harness;

import java.util.*;
import java.util.concurrent.locks.*;

import org.apache.commons.lang3.text.*;

import com.goldencode.lang.*;
import com.goldencode.util.*;

/**
 * Manages scoped resources like variables and semaphores. The following
 * resources are available:
 * <p>
 * <ul>
 *    <li> Variables. These are named containers that store a value. 
 *    <li> Mutex semaphores. These are mutual-exclusion primatives that provide
 *         a simple mechanism for multiple threads to safely synchronize access
 *         to shared resources.
 *    <li> Event semaphores. These are notification and blocking primatives that
 *         allow multiple threads to safely synchronize processing based on
 *         events.
 * </ul>
 * <p>
 * Each resource is named and stored in a given scope. When a scope is added,
 * other resources of the same name can be added to the newer scope and then
 * the older resources are hidden until the newer scope is removed. When a
 * resource is accessed, the scopes are searched from the most recent to the
 * least recent until a resource of that name is found, that found resource
 * will be returned.
 * <p>
 * Variables are test-level resources so they get a scope opened (and closed)
 * at the test plan, at the test-set and for every test.  Mutex and event
 * semaphores are NOT test-level resources. Semaphores only get scopes opened
 * (and closed) at the test plan and at the test-set. 
 * <p>
 * Each resource type is stored in a separate namespace. Lookups are
 * case-insensitive.
 */
public class ResourcePool
{
   /** Mutex semaphores. */
   private ScopedSymbolDictionary<ReentrantLock> mutexes = null;
   
   /** Event semaphores. */
   private ScopedSymbolDictionary<EventSemaphore> events = null;
   
   /** Variables. */
   private ScopedSymbolDictionary<Variable> vars = null;
   
   /** Prompt instances (allows delegation of initial value setting). */
   private List<Prompt> prompts = null;
   
   /**
    * Default constructor.
    */                    
   public ResourcePool()
   {
      mutexes = new ScopedSymbolDictionary<ReentrantLock>(false);
      events  = new ScopedSymbolDictionary<EventSemaphore>(false);
      vars    = new ScopedSymbolDictionary<Variable>(false);
      prompts = new ArrayList<Prompt>();
      
      // force a global scope
      push(false);
   }
   
   /**
    * Copy constructor.
    *
    * @param    original
    *           Instance from which to make a copy.
    */                    
   public ResourcePool(ResourcePool original)
   {
      mutexes = new ScopedSymbolDictionary<ReentrantLock>(original.mutexes);
      events  = new ScopedSymbolDictionary<EventSemaphore>(original.events);
      vars    = new ScopedSymbolDictionary<Variable>(original.vars);
      
      // this instance is shared between all resource pools (it is a load-time
      // thing only, which is a single threaded process so this is safe)
      prompts = original.prompts;
   }
   
   /**
    * Add a new scope to the resource-specific dictionaries.
    *
    * @param    test
    *           <code>true</code> if this scope is associated with test-level
    *           resources. Only those resources can be scoped to a specific
    *           test will get a scope added.
    */
   public void push(boolean test)
   {
      if (!test)
      {
         mutexes.addScope(null);
         events.addScope(null);
      }
      
      // test-specific resources always get a scope
      vars.addScope(null);
   }
   
   /**
    * Remove the most recent (top) scope in the resource-specific dictionaries.
    *
    * @param    test
    *           <code>true</code> if this scope is associated with test-level
    *           resources. Only those resources can be scoped to a specific
    *           test will get a scope removed.
    */
   public void pop(boolean test)
   {
      if (!test)
      {
         mutexes.deleteScope();
         events.deleteScope();
      }
      
      // test-specific resources always get a scope
      vars.deleteScope();
   }
   
   /**
    * Create a new mutex semaphore with the given name.
    *
    * @param    name
    *           The case-insensitive name of the semaphore.
    */
   public void createMutex(String name)
   {
      mutexes.addSymbol(false, name, new ReentrantLock());
   }
   
   /**
    * Find a mutex semaphore with the given name.
    *
    * @param    name
    *           The case-insensitive name of the semaphore.
    *
    * @return   The mutex found in the nearest scope that has that name or
    *           <code>null</code> if no such named mutex exists.
    */
   public ReentrantLock findMutex(String name)
   {
      return mutexes.lookupSymbol(name);
   }
   
   /**
    * Create a new event semaphore with the given name.
    *
    * @param    name
    *           The case-insensitive name of the semaphore.
    * @param    set
    *           <code>true</code> if the initial state of the semaphore should
    *           be "set".
    */
   public void createEventSem(String name, boolean set)
   {
      events.addSymbol(false, name, new EventSemaphore(set));
   }
   
   /**
    * Find a event semaphore with the given name.
    *
    * @param    name
    *           The case-insensitive name of the semaphore.
    *
    * @return   The event semaphore found in the nearest scope that has that
    *           name or <code>null</code> if no such named semaphore exists.
    */
   public EventSemaphore findEventSem(String name)
   {
      return events.lookupSymbol(name);
   }
   
   /**
    * Create a variable with the given name and no initial value.
    *
    * @param    name
    *           The case-insensitive name of the variable.
    * @param    cls
    *           The data type of the variable.
    *
    * @return   The newly created variable.
    */
   public Variable createVariable(String name, Class<?> cls)
   {
      Variable newVar = new Variable(name, cls);
      vars.addSymbol(false, name, newVar);
      return newVar;
   }
   
   /**
    * Create a variable with the given name.
    *
    * @param    name
    *           The case-insensitive name of the variable.
    * @param    cls
    *           The data type of the variable.
    * @param    initial
    *           Text representation of the initial value of the variable.
    *
    * @return   The newly created variable.
    */
   public Variable createVariable(String name, Class<?> cls, String initial)
   {
      Variable newVar = new Variable(name, cls, initial);
      vars.addSymbol(false, name, newVar);
      return newVar;
   }
   
   /**
    * Create a variable with the given name.
    *
    * @param    name
    *           The case-insensitive name of the variable.
    * @param    initial
    *           The initial value.
    *
    * @return   The newly created variable.
    */
   public Variable createVariable(String name, Object initial)
   {
      Variable newVar = new Variable(name, initial);
      vars.addSymbol(false, name, newVar);
      return newVar;
   }
   
   /**
    * Find a variable with the given name.
    *
    * @param    name
    *           The case-insensitive name of the variable.
    *
    * @return   The variable found in the nearest scope that has that
    *           name or <code>null</code> if no such variable exists.
    */
   public Variable findVariable(String name)
   {
      return vars.lookupSymbol(name);
   }
   
   /**
    * Obtain the set of all valid variable names.
    *
    * @return   The list of names with no duplication.
    */
   public String[] getVariableNames()
   {
      Set<String> keys = vars.keySet();
      return keys.toArray(new String[0]);
   }
   
   /**
    * Add a delegated prompt to the list.
    *
    * @param    prompt
    *           The delegated prompt instance to add.
    */
   public void addPrompt(Prompt prompt)
   {
      prompts.add(prompt);
   }
   
   /**
    * Get the list of all delegated prompts.
    *
    * @return   The prompts or <code>null</code> if the list is empty.
    */
   public Prompt[] getPrompts()
   {
      return (prompts == null) ? null : prompts.toArray(new Prompt[0]);
   }

   /**
    * Replace all variables in the given string to their values.
    * 
    * @param    str
    *           The string to process.
    *           
    * @return   The processed string, with all variables replaced with their values.
    */
   public String setVariables(String str)
   {
      Map<String, Object> values = new HashMap<>();
      for (String var : getVariableNames())
      {
         values.put(var, findVariable(var).getValue());
      }
      
      StrSubstitutor substitutor = new StrSubstitutor(values);
      return substitutor.replace(str);
   }
}
