/*
 * Copyright (c) 1998-2008 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source 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, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.vfs;

import com.caucho.util.CharBuffer;

import java.util.Map;

/**
 * Abstract FilesystemPath, the parent of hierarchical Paths like
 * FilePath or HttpPath.
 */
abstract public class FilesystemPath extends Path {
  protected FilesystemPath _root;
  protected BindPath _bindRoot;
  protected String _pathname;
  protected String _userPath;

  /**
   * Create a new filesystemPath
   *
   * @param root Root of url space
   * @param userPath the user's path
   * @param pathname Canonical path
   */
  protected FilesystemPath(FilesystemPath root,
                           String userPath,
			   String pathname)
  {
    super(root);

    if (pathname == null)
      throw new NullPointerException();

    _pathname = pathname;
    _userPath = userPath;

    if (root != null) {
      _root = root;
      _bindRoot = root._bindRoot;
    }
  }

  /**
   * Return the parent Path
   */
  public Path getParent()
  {
    if (_pathname.length() <= 1)
      return fsWalk("/", null, "/");

    int length = _pathname.length();
    int lastSlash = _pathname.lastIndexOf('/');

    if (lastSlash < 1)
      return fsWalk("/", null, "/");
    
    if (lastSlash == length - 1) {
      lastSlash = _pathname.lastIndexOf('/', length - 2);
      if (lastSlash < 1)
        return fsWalk("/", null, "/");
    }
  
    return fsWalk("..", null, _pathname.substring(0, lastSlash));
  }

  /**
   * schemeWalk is called by Path for a scheme lookup like file:/tmp/foo
   *
   * @param userPath the user's lookup() path
   * @param attributes the user's attributes
   * @param filePath the actual lookup() path
   * @param offset offset into filePath
   */
  protected Path schemeWalk(String userPath,
                            Map<String,Object> attributes,
			    String filePath,
                            int offset)
  {
    String canonicalPath;

    if (filePath.length() > offset
	&& (filePath.charAt(offset) == '/'
	    || filePath.charAt(offset) == _separatorChar))
      canonicalPath = normalizePath("/", filePath, offset, _separatorChar);
    else
      canonicalPath = normalizePath(_pathname, filePath, offset,
                                    _separatorChar);


    return fsWalk(userPath, attributes, canonicalPath);
  }

  /**
   * Lookup a path relative to the current filesystem's root.
   * Filesystems will specialize fsWalk.
   *
   * @param userPath the exact string passed by the user's lookup()
   * @param newAttributes the user's new attributes
   * @param newPath the normalized real path
   *
   * @return the matching path
   */
  abstract public Path fsWalk(String userPath,
                                 Map<String,Object> newAttributes, 
				 String newPath);

  /**
   * wrapper for the real normalize path routine to use CharBuffer.
   *
   * @param oldPath The parent Path's path
   * @param newPath The user's new path
   * @param offset Offset into the user path
   *
   * @return the normalized path
   */
  static protected String normalizePath(String oldPath, 
                                        String newPath,
                                        int offset,
                                        char separatorChar)
  {
    CharBuffer cb = new CharBuffer();
    normalizePath(cb, oldPath, newPath, offset, separatorChar);
    return cb.toString();
  }

  /**
   * Normalizes a filesystemPath path.
   *
   * <ul>
   * <li>foo//bar -> foo/bar
   * <li>foo/./bar -> foo/bar
   * <li>foo/../bar -> bar
   * <li>/../bar -> /bar
   * </ul>
   *
   * @param cb charBuffer holding the normalized result
   * @param oldPath the parent path
   * @param newPath the relative path
   * @param offset where in the child path to start
   */
  static protected void normalizePath(CharBuffer cb, String oldPath, 
                                      String newPath, int offset,
                                      char separatorChar)
  {
    cb.clear();
    cb.append(oldPath);
    if (cb.length() == 0 || cb.getLastChar() != '/')
      cb.append('/');

    int length = newPath.length();
    int i = offset;
    while (i < length) {
      char ch = newPath.charAt(i);
      char ch2;

      switch (ch) {
      default:
        if (ch != separatorChar) {
          cb.append(ch);
          i++;
          break;
        }
        // the separator character falls through to be treated as '/'

      case '/':
        // "//" -> "/"
	if (cb.getLastChar() != '/')
	  cb.append('/');
	i++;
	break;

      case '.':
	if (cb.getLastChar() != '/') {
	  cb.append('.');
	  i++;
	  break;
	}

        // "/." -> ""
	if (i + 1 >= length) {
	  i += 2;
	  break;
	}

        switch (newPath.charAt(i + 1)) {
        default:
          if (newPath.charAt(i + 1) != separatorChar) {
            cb.append('.');
            i++;
            break;
          }
          // the separator falls through to be treated as '/'
            
          // "/./" -> "/"
        case '/':
          i += 2;
          break;

          // "foo/.." -> ""
        case '.':
          if ((i + 2 >= length ||
               (ch2 = newPath.charAt(i + 2)) == '/' || ch2 == separatorChar) &&
              cb.getLastChar() == '/') {
            int segment = cb.lastIndexOf('/', cb.length() - 2);
            if (segment == -1) {
              cb.clear();
              cb.append('/');
            } else
              cb.setLength(segment + 1);

            i += 3;
          } else {
            cb.append('.');
            i++;
          }
          break;
        }
      }

    }

    // strip trailing "/"
    /*
    if (cb.length() > 1 && cb.getLastChar() == '/')
      cb.setLength(cb.length() - 1);
    */
  }

  /**
   * Returns the root.
   */
  public FilesystemPath getRoot()
  {
    return _root;
  }

  /**
   * Returns the path portion of the URL.
   */
  public String getPath()
  {
    return _pathname;
  }

  /**
   * Return's the application's name for the path, e.g. for
   * a relative path.
   */
  public String getUserPath()
  {
    return _userPath != null ? _userPath : _pathname;
  }

  public void setUserPath(String path)
  {
    _userPath = path;
  }

  /**
   * For chrooted filesystems return the real system path.
   */
  public String getFullPath()
  {
    if (_root == this)
      return getPath();

    String rootPath = _root.getFullPath();
    String path = getPath();

    if (rootPath.length() <= 1)
      return path;
    else if (path.length() <= 1)
      return rootPath;
    else
      return rootPath + path;
  }

  public String getTail()
  {
    String path = getPath();

    int length = path.length();
    int p = path.lastIndexOf('/');
    if (p == -1)
      return "";
    else if (p < length - 1)
      return path.substring(p + 1);
    else {
      p = path.lastIndexOf('/', length - 2);
      if (p < 0)
        return "";
      return path.substring(p + 1, length - 1);
    }
  }

  /**
   * Essentially chroot
   */
  public Path createRoot(SchemeMap schemeMap)
  {
    FilesystemPath restriction = (FilesystemPath) copy();
    
    restriction._schemeMap = schemeMap;
    restriction._root = this;
    restriction._pathname = "/";
    restriction._userPath = "/";

    return restriction;
  }

  public void bind(Path context)
  {
    if (_bindRoot == null)
      _bindRoot = _root._bindRoot;

    if (_bindRoot == null) {
      _bindRoot = new BindPath(_root);
      _root._bindRoot = _bindRoot;
    }

    _bindRoot.bind(getPath(), context);
  }

  public int hashCode()
  {
    return getURL().hashCode();
  }

  public boolean equals(Object b)
  {
    if (this == b)
      return true;
    else if (b == null || ! getClass().equals(b.getClass()))
      return false;

    Path bPath = (Path) b;

    return getURL().equals(bPath.getURL());
  }
}
