/*
** Module   : ProjectIndex.java
** Abstract : stores metadata related to the test project
**
** Copyright (c) 2009, Golden Code Development Corporation.
**
** -#- -I- --Date-- -----------------------Description------------------------
** 001 GES 20090220 Stores metadata related to the test project.
*/

/*
** 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.io.*;
import java.util.*;
import org.w3c.dom.*;
import com.goldencode.harness.test.*;

/**
 * Stores metadata related to the test project. The following resources are
 * available:
 * <p>
 * <ul>
 *    <li> Named rectangles. These are {@link Rectangle} instances which can
 *         be referenced by name. This avoids the need to manually encode the
 *         same rectangle coordinates in multiple places throughout the
 *         project.
 *    <li> Search paths. Tests, include files, screen and report baseline files
 *         can all be searched for via lists of search paths.
 *    <li> Download directory. An output directory in which downloaded files
 *         can be placed.
 * </ul>
 */
public class ProjectIndex
{
   /** Known named rectangles. */
   private Map<String, Rectangle> rects = null;
   
   /** List of directories to search for test and include files. */
   private String[] testPaths = null;
   
   /** List of directories to search for baseline files. */
   private String[] baseLinePaths = null;
   
   /** A unique file-system destination for downloaded files. */ 
   private String download = ".";
   
   /** Default XML attribute values. */
   private Map<String, String> attrs = new HashMap<String, String>();
   
   /**
    * Construct a default instance. The contained data must be set subsequently
    * using the explicit setter methods.
    */                    
   public ProjectIndex()
   {
   }
   
   /**
    * Get a rectangle instance associated with the given name.
    *
    * @param    name
    *           The name of the rectangle which is used in the search. The
    *           search is case insensitive.
    *
    * @return   A new rectangle with the same coordinates or <code>null</code>
    *           if no such rectangle exists.
    */
   public Rectangle getRectangle(String name)
   {
      Rectangle result = null;
      
      if (rects != null)
      {
         name = name.toLowerCase();
         Rectangle found = rects.get(name);
         
         if (found != null)
         {
            result = new Rectangle(found);
         }
      }
      
      return result;
   }
   
   /**
    * Add or replace a named rectangle to the list.
    *
    * @param    name
    *           The name of the rectangle. Case is ignored so case differences
    *           cannot be used to differentiate rectangles.
    * @param    rect
    *           The rectangle to associate with this name.
    */
   public void putRectangle(String name, Rectangle rect)
   {
      if (name != null && rect != null)
      {
         if (rects == null)
         {
            rects = new HashMap<String, Rectangle>();
         }
         
         name = name.toLowerCase();
         rects.put(name, new Rectangle(rect));
      }
   }
   
   /**
    * Search for the given test or include file and return the filename found.
    *
    * @param    fname
    *           The specific file for which to search.
    *
    * @return   The specific filename found (with pathing) or the given string
    *           if not found.
    */
   public String findTest(String fname)
   {
      return search(testPaths, fname);
   }
   
   /**
    * Add the given path to the end of the list of paths searched for tests and
    * include files.
    *
    * @param    path
    *           The directory to add.
    */
   public void addTestPath(String path)
   {
      testPaths = appendToArray(testPaths, path);
   }
   
   /**
    * Add the given list of paths to the end of the list of paths searched for
    * tests and include files. Each path must be separated using the platform
    * specific path separator.
    *
    * @param    paths
    *           The list of directories to add.
    */
   public void addTestPathList(String paths)
   {
      if (paths != null)
      {
         String[] list = paths.split(File.pathSeparator);
         
         for (String path : list)
         {
            testPaths = appendToArray(testPaths, path);
         }
      }
   }
   
   /**
    * Search for the given baseline file (screen or report file) and return
    * the filename found.
    *
    * @param    fname
    *           The specific file for which to search.
    *
    * @return   The specific filename found (with pathing) or the given string
    *           if not found.
    */
   public String findBaseLine(String fname)
   {
      return search(baseLinePaths, fname);
   }
   
   /**
    * Add the given path to the end of the list of paths searched for baseline
    * files such as screen captures and report files.
    *
    * @param    path
    *           The directory to add.
    */
   public void addBaseLinePath(String path)
   {
      baseLinePaths = appendToArray(baseLinePaths, path);
   }
   
   /**
    * Add the given list of paths to the end of the list of paths searched for
    * baseline files such as screen captures and report files. Each path must
    * be separated using the platform specific path separator.
    *
    * @param    paths
    *           The list of directories to add.
    */
   public void addBaseLinePathList(String paths)
   {
      if (paths != null)
      {
         String[] list = paths.split(File.pathSeparator);
         
         for (String path : list)
         {
            baseLinePaths = appendToArray(baseLinePaths, path);
         }
      }
   }
   
   /**
    * Get the download directory.
    *
    * @return   The download directory or <code>null</code> if none exists.
    */
   public String getDownloadDirectory()
   {
      return download;
   }
   
   /**
    * Set the download directory.
    *
    * @param    download
    *           The download directory path.
    */
   public void setDownloadDirectory(String download)
   {
      this.download = download;
   }
   
   /**
    * Read the specified attribute and convert it to the proper data type.  If
    * it is not present, attempt to read any default value set for the project
    * and if that fails return the empty string.
    *
    * @param    elem
    *           The node to search for the attribute.
    * @param    name
    *           The attribute name for which to search.
    *
    * @return   The parsed value, the project default value or the empty
    *           string in that order.
    */
   public String readAttr(Element elem, String name)
   {
      return readAttr(elem, name, "");
   }
   
   /**
    * Read the specified attribute and convert it to the proper data type.  If
    * it is not present, attempt to read any default value set for the project
    * and if that fails return the given default value parameter.
    *
    * @param    elem
    *           The node to search for the attribute.
    * @param    name
    *           The attribute name for which to search.
    * @param    defVal
    *           The default value to return if no attribute of that name is
    *           found OR if there is any error.
    *
    * @return   The parsed value, the project default value or the given
    *           default value in that order.
    */
   public String readAttr(Element elem, String name, String defVal)
   {
      String result = elem.getAttribute(name);
      
      if (result == null || result.length() == 0)
      {
         String defTxt = getDefaultAttr(name);
         
         if (defTxt != null && defTxt.length() > 0)
         {
            result = defTxt;
         }
         else
         {
            result = defVal;
         }
      }
      
      return result;
   }
   
   /**
    * Read the specified attribute and convert it to the proper data type.  If
    * it is not present, attempt to read any default value set for the project
    * and if that fails return the given default value parameter.
    *
    * @param    elem
    *           The node to search for the attribute.
    * @param    name
    *           The attribute name for which to search.
    * @param    defVal
    *           The default value to return if no attribute of that name is
    *           found OR if there is any error.
    *
    * @return   The parsed value, the project default value or the given
    *           default value in that order.
    */
   public int readAttrInt(Element elem, String name, int defVal)
   {
      String defTxt = getDefaultAttr(name);
      
      if (defTxt != null && defTxt.length() > 0)
      {
         try
         {
            defVal = Integer.parseInt(defTxt);
         }
         
         catch (NumberFormatException nfe)
         {
            // ignore and return the default
         }
      }
      
      return XmlHelper.getAttributeInt(elem, name, defVal);
   }
   
   /**
    * Read the specified attribute and convert it to the proper data type.  If
    * it is not present, attempt to read any default value set for the project
    * and if that fails return the given default value parameter.
    *
    * @param    elem
    *           The node to search for the attribute.
    * @param    name
    *           The attribute name for which to search.
    * @param    defVal
    *           The default value to return if no attribute of that name is
    *           found OR if there is any error.
    *
    * @return   The parsed value, the project default value or the given
    *           default value in that order.
    */
   public long readAttrLong(Element elem, String name, long defVal)
   {
      String defTxt = getDefaultAttr(name);
      
      if (defTxt != null && defTxt.length() > 0)
      {
         try
         {
            defVal = Long.parseLong(defTxt);
         }
         
         catch (NumberFormatException nfe)
         {
            // ignore and return the default
         }
      }
      
      return XmlHelper.getAttributeLong(elem, name, defVal);
   }
   
   /**
    * Read the specified attribute and convert it to the proper data type.  If
    * it is not present, attempt to read any default value set for the project
    * and if that fails return the given default value parameter.
    *
    * @param    elem
    *           The node to search for the attribute.
    * @param    name
    *           The attribute name for which to search.
    * @param    defVal
    *           The default value to return if no attribute of that name is
    *           found OR if there is any error.
    *
    * @return   The parsed value, the project default value or the given
    *           default value in that order.
    */
   public boolean readAttrBool(Element elem, String name, boolean defVal)
   {
      String defTxt = getDefaultAttr(name);
      
      if (defTxt != null && defTxt.length() > 0)
      {
         if ("true".equalsIgnoreCase(defTxt))
         {
            defVal = true;
         }
      }
      
      return XmlHelper.getAttributeBoolean(elem, name, defVal);
   }
   
   /**
    * Obtain the default value for the named attribute.
    *
    * @param    name
    *           Case-insensitive name for the attribute.
    *
    * @return   The default text value or <code>null</code> if no such named
    *           attribute exists in the defaults.
    */
   public String getDefaultAttr(String name)
   {
      return attrs.get(name.toLowerCase());
   }
   
   /**
    * Add (or replace) the default value for the named attribute.
    *
    * @param    name
    *           Case-insensitive name for the attribute.
    * @param    value
    *           The default text value or <code>null</code> if no such named
    *           attribute exists in the defaults.
    */
   public void putDefaultAttr(String name, String value)
   {
      attrs.put(name.toLowerCase(), value);
   }
   
   /**
    * Search for the given filename in the given list of paths and return the
    * filename found.
    *
    * @param    paths
    *           The list of directories to search.
    * @param    fname
    *           The specific file for which to search.
    *
    * @return   The specific filename found (with pathing) or the given string
    *           if not found.
    */
   private String search(String[] paths, String fname)
   {
      if (paths != null && fname != null && fname.length() != 0)
      {
         for (String nextPath : paths)
         {
            StringBuilder sb = new StringBuilder(nextPath);
            sb.append(fname);
            
            String possible = sb.toString();
            
            File file = new File(possible);
            
            if (file.exists() && file.isFile())
            {
               return possible;
            }
         }
      }
      
      return fname;
   }
   
   /**
    * Append the given string to the given array and return the new larger
    * array.
    *
    * @param    original
    *           Starting array or <code>null</code> if no array exists yet.
    * @param    addition
    *           New element.
    *
    * @return   The larger array with the given addition appended.
    */
   private String[] appendToArray(String[] original, String addition)
   {
      if (addition == null)
         return original;
      
      // make sure the addition ends with the proper separator
      StringBuilder sb = new StringBuilder(addition);
      
      if (!addition.endsWith(File.separator))
         sb.append(File.separator);
      
      addition = sb.toString();
      
      // create the path array if this is the first entry
      if (original == null)
         return new String[] { addition };
      
      // append to the existing list
      String[] result = new String[original.length + 1];
      
      System.arraycopy(original, 0, result, 0, original.length);
      result[original.length] = addition;
      
      return result;
   }
}
