package hirondelle.fish.test.doubles;

import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.*;
import javax.servlet.RequestDispatcher;
import javax.servlet.Servlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import java.util.logging.*;
import hirondelle.web4j.util.Util;

/**
 Fake implementation of {@link ServletContext}, used only for testing outside 
 the regular runtime environment.
 
 <P>This class requires that a System property be set correctly.
 See {@link #ROOT_DIRECTORY} for more information.
*/
public class FakeServletContext implements ServletContext {
  
  /**
   Name of a System property ({@value}) that holds the root directory for this web application 
   (with no trailing separator).
   
   <P>This item exists because <tt>ServletContext</tt> needs to translate 
   request paths into explicit file system paths.
    
   <P><span class='highlight'>When using fake objects for testing, this System property must be set 
   to a non-empty value</span>, for example :
   <PRE> '<tt>rootDirectory=C:\myname\myprojects\blah</tt>'.</PRE>
   
   <P>There are various ways of setting this property :
   <ul>
   <li>calling {@link hirondelle.fish.test.TESTAll#setRootDirectory()}
   <li>calling {@link System#setProperty(java.lang.String, java.lang.String)} explicitly 
   <li>launching java with a <tt>-D</tt> option, as in :
   </ul>
    
   <P>'<tt>java ... -DrootDirectory=C:\myname\myprojects\blah</tt> ...'.</tt>
  */
  public static final String ROOT_DIRECTORY = "rootDirectory";
  
  /** Value - {@value}.*/
  public static final String SERVER_INFO = "Fake ServletContext For Testing/1.0";
  /** Value - {@value}.*/
  public static final String SERVLET_CONTEXT_NAME =  "Fake ServletContext For Testing";
  
  /**
   Return the non-empty value of the System property identified by 
   {@link #ROOT_DIRECTORY}.
  */
  public static String getRootDirectory(){
    String result = System.getProperty(ROOT_DIRECTORY);
    if (! Util.textHasContent(result) ) {
      throw new IllegalStateException(
        "FakeServletContext needs a System property named "  + ROOT_DIRECTORY + 
        " . The value of that property is : " + Util.quote(result)
      );
    }
    return result;
  }
  
  /** Simple test harness. Creates a <tt>FakeServletContext</tt>, and emits some of its data. */
  private static void main(String... aArgs) throws FileNotFoundException {
    logger("Starting.");
    FakeServletContext fake = new FakeServletContext();
    logger("Init param: " + fake.getInitParameter("MailServer"));
    logger("Real path for web.xml: " + fake.getRealPath("/WEB-INF/web.xml"));
    logger("Real path for /WEB-INF/: " + fake.getRealPath("/WEB-INF/"));
    logger("Resources in /WEB-INF/classes/ :"  + Util.logOnePerLine(fake.getResourcePaths("/WEB-INF/classes/")));
    
    Set<String> pathsForClasses = getFilePathsBelow("/WEB-INF/classes", fake);
    logger("Classes : " + Util.logOnePerLine(pathsForClasses));
    logger("Done.");
  }
  
  /** Constructor */
  public FakeServletContext() throws FileNotFoundException {
    fInitParams = buildInitParams();
    File rootDirectory = new File(getRootDirectory());
    fPathToFile = populateFileMapping(getFileListingBelow(rootDirectory));
    //logger("Path to File mapping " + Util.logOnePerLine(fPathToFile));
  }

  public String getInitParameter(String aParamName) {
    return fInitParams.get(aParamName);
  }

  public Enumeration getInitParameterNames() {
    return Collections.enumeration(fInitParams.keySet());
  }

  /**
   <P>Returns paths to items under the root directory. 
   
   <P>Example return value, given path patern of "/catalog/":
   <PRE>
   "/catalog/blah.html" 
   "/catalog/xyz.html" 
   "/catalog/specials/"
   </PRE>
   Note that '/catalog/' itself if not returned.
   
   @param aPathPattern starts with "/", path to which items are matched; usually ends with '/' ,
   to indicate a directory.
  */
  public Set getResourcePaths(String aPathPattern) {
    //logger("Getting all resource paths for : " + Util.quote(aPathPattern));
    if (!aPathPattern.startsWith("/")) { 
      throw new IllegalArgumentException("Path must start with '/' : " + Util.quote(aPathPattern)); 
    }
    Set<String> result = new LinkedHashSet<String>();
    int maxNumSeparatorsAllowed = numSeparatorsIn(aPathPattern) + 1;
    Set<String> allPaths = fPathToFile.keySet();
    for (String path : allPaths) {
      if (path.startsWith(aPathPattern) && ! path.equalsIgnoreCase(aPathPattern)) {
        if (numSeparatorsIn(path) <= maxNumSeparatorsAllowed) {
          result.add(path);
        }
      }
    }
    return result;
  }

  /**
   Return the full path to an underlying file or directory. The returned String uses file
   separators of the underlying operating system.
   
   @param aPath such as '/index.html'; directories end in a separator.
  */
  public String getRealPath(String aPath) {
    String result = null;
    //logger("Getting real path for : " + Util.quote(aPath));
    if (!aPath.startsWith("/")) { throw new IllegalArgumentException("Path must start with '/' : "
    + Util.quote(aPath)); }
    File file = fPathToFile.get(aPath);
    if (file != null) {
      result = file.getPath();
    }
    return result;
  }

  public InputStream getResourceAsStream(String aPath) {
    InputStream result = null;
    File file = fPathToFile.get(aPath);
    if (file != null) {
      try {
        result = new FileInputStream(file);
      }
      catch (FileNotFoundException ex) {
        throw new IllegalArgumentException("Cannot open file resource as stream: " + ex);
      }
    }
    return result;
  }

  public void setAttribute(String aName, Object aObject) {
    fAttributes.put(aName, aObject);
  }

  public Object getAttribute(String aName) {
    return fAttributes.get(aName);
  }

  public void removeAttribute(String aName) {
    fAttributes.put(aName, null);
  }

  public Enumeration getAttributeNames() {
   return Collections.enumeration(fAttributes.keySet());
  }

  /** Returns {@value #SERVER_INFO}. */
  public String getServerInfo() {
    return SERVER_INFO;
  }

  /** Returns {@value #SERVLET_CONTEXT_NAME}. */
  public String getServletContextName() {
    return SERVLET_CONTEXT_NAME;
  }

  public void log(String aMessage) {
    fLogger.fine(aMessage);
  }

  public void log(Exception aException, String aMessage) {
    fLogger.severe(aException.toString() + ": " + aMessage);
  }

  public void log(String aMessage, Throwable aThrowable) {
    fLogger.severe(aThrowable.getMessage() + ": " + aMessage);
  }

  /** Returns <tt>2</tt>. */
  public int getMajorVersion() {
    return 2;
  }

  /** Returns <tt>4</tt>. */
  public int getMinorVersion() {
    return 4;
  }

  /** Not implemented - returns <tt>null</tt>. */
   public URL getResource(String aPath) throws MalformedURLException {
     return null;
   }
  
  /** Not implemented - returns <tt>null</tt>. */
  public ServletContext getContext(String aPath) {
    return null;
  }
  
  /** Not implemented - returns <tt>null</tt>. */
  public String getContextPath() {
    return null;
  }
  
  /** Not implemented - returns <tt>null</tt>. */
  public String getMimeType(String aFileName) {
    return null;
  }

  /** Not implemented - returns <tt>null</tt>. */
  public RequestDispatcher getRequestDispatcher(String aPath) {
    return null;
  }

  /** Not implemented - returns <tt>null</tt>. */
  public RequestDispatcher getNamedDispatcher(String aServletName) {
    return null;
  }

  /** Not implemented - returns <tt>null</tt>. */
  public Servlet getServlet(String aName) throws ServletException {
    return null;
  }

  /** Not implemented - returns <tt>null</tt>. */
  public Enumeration getServlets() {
    return null;
  }

  /** Not implemented - returns <tt>null</tt>. */
  public Enumeration getServletNames() {
    return null;
  }

  // PRIVATE //
  private Map<String, String> fInitParams;
  private Map<String, Object> fAttributes = new LinkedHashMap<String, Object>();
  private Map<String, File> fPathToFile;
  private static final String SYSTEM_SEPARATOR = System.getProperty("file.separator");
  private static final String STANDARD_SEPARATOR = "/";
  private static final Logger fLogger = Util.getLogger(FakeServletContext.class);
  
  private static Set<String> getFilePathsBelow(String aStartDirectory, ServletContext aContext){
    //logger("Paths below: " + aStartDirectory);
    Set<String> result = new LinkedHashSet<String>();
    Set<String> paths = aContext.getResourcePaths(aStartDirectory);
    for ( String path : paths) {
      if ( isDirectory(path) ) {
        //recursive call !!!
        result.addAll(getFilePathsBelow(path, aContext));
      }
      else {
        result.add(path);
      }
    }
    return result;
  }
  
  private static boolean isDirectory(String aFullFilePath){
    return aFullFilePath.endsWith(STANDARD_SEPARATOR); 
  }

  private static void logger(Object aObject) {
    System.out.println(String.valueOf(aObject));
  }
  
  private Map<String, String> buildInitParams() {
    Map<String, String> result = new LinkedHashMap<String, String>();
    result.put("MailServer", "NONE");
    return result;
  }

  private Map<String, File> populateFileMapping(List<File> aAllFiles) {
    Map<String, File> result = new LinkedHashMap<String, File>();
    for (File file : aAllFiles) {
      result.put(pathFor(file), file);
    }
    return result;
  }

  private String pathFor(File aFile) {
    String result = aFile.getPath();
    result = chopOffBaseDirectory(result);
    result = ensureCanonicalSeparators(result);
    result = ensureDirectoriesEndInStandardSeparator(aFile, result);
    return result;
  }

  private String chopOffBaseDirectory(String aFilePath) {
    return aFilePath.substring(getRootDirectory().length());
  }

  private String ensureCanonicalSeparators(String aFilePath) {
    return SYSTEM_SEPARATOR.equals("\\") ? aFilePath.replace("\\", "/") : aFilePath;
  }

  private String ensureDirectoriesEndInStandardSeparator(File aFile, String aFilePath) {
    return aFile.isDirectory() ? aFilePath + STANDARD_SEPARATOR : aFilePath;
  }

  /**
    Recursively walk a directory tree and return a List of all Files found; the List is
    sorted using File.compareTo.
    @param aStartingDir is a valid directory, which can be read.
   */
  private List<File> getFileListingBelow(File aStartingDir) throws FileNotFoundException {
    validateDirectory(aStartingDir);
    List<File> result = new ArrayList<File>();
    File[] filesAndDirs = aStartingDir.listFiles();
    List<File> filesDirs = Arrays.asList(filesAndDirs);
    for (File file : filesDirs) {
      result.add(file); // always add, even if directory
      if (!file.isFile()) {
        // must be a directory
        // recursive call!
        List<File> deeperList = getFileListingBelow(file);
        result.addAll(deeperList);
      }
    }
    Collections.sort(result);
    return result;
  }

  private void validateDirectory(File aDirectory) throws FileNotFoundException {
    if (aDirectory == null) { 
      throw new IllegalArgumentException("Directory should not be null."); 
    }
    if (!aDirectory.exists()) { 
      throw new FileNotFoundException("Directory does not exist: " + aDirectory); 
    }
    if (!aDirectory.isDirectory()) { 
      throw new IllegalArgumentException("Is not a directory: " + aDirectory); 
    }
    if (!aDirectory.canRead()) { 
      throw new IllegalArgumentException("Directory cannot be read: " + aDirectory); 
    }
  }

  /** Count the number of standard separators in aPath. */
  private int numSeparatorsIn(String aPath) {
    char standardSeparator = STANDARD_SEPARATOR.charAt(0);
    int result = 0;
    StringCharacterIterator iterator = new StringCharacterIterator(aPath);
    char character = iterator.current();
    while (character != CharacterIterator.DONE) {
      if (character == standardSeparator) {
        result++;
      }
      character = iterator.next();
    }
    return result;
  }
}