/*
 * Copyright (c) 1998-2006 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 java.io.*;
import java.net.*;
import java.util.*;

import org.xml.sax.*;

import com.caucho.util.*;
import com.caucho.xml.*;

/**
 * The HTTP scheme.  Currently it supports GET and POST.
 *
 * <p>TODO: support WEBDAV, enabling the full Path API.
 */
public class HttpPath extends FilesystemPath {
  protected static L10N L = new L10N(HttpPath.class);

  protected static LruCache<String,CacheEntry> _cache =
    new LruCache<String,CacheEntry>(1024);
  
  protected String _host;
  protected int _port;
  protected String _query;

  protected CacheEntry _cacheEntry;

  /**
   * Creates a new HTTP root path with a host and a port.
   *
   * @param host the target host
   * @param port the target port, if zero, uses port 80.
   */
  HttpPath(String host, int port)
  {
    super(null, "/", "/");

    _root = this;
    _host = host;
    _port = port == 0 ? 80 : port;
  }

  /**
   * Creates a new HTTP sub path.
   *
   * @param root the HTTP filesystem root
   * @param userPath the argument to the calling lookup()
   * @param newAttributes any attributes passed to http
   * @param path the full normalized path
   * @param query any query string
   */
  HttpPath(FilesystemPath root,
           String userPath, Map<String,Object> newAttributes,
	   String path, String query)
  {
    super(root, userPath, path);

    _host = ((HttpPath) root)._host;
    _port = ((HttpPath) root)._port;
    _query = query;
  }

  /**
   * Overrides the default lookup to parse the host and port
   * before parsing the path.
   *
   * @param userPath the path passed in by the user
   * @param newAttributes attributes passed by the user
   *
   * @return the final path.
   */
  public Path lookupImpl(String userPath, Map<String,Object> newAttributes)
  {
    String newPath;

    if (userPath == null)
      return _root.fsWalk(getPath(), newAttributes, "/");

    int length = userPath.length();
    int colon = userPath.indexOf(':');
    int slash = userPath.indexOf('/');

    // parent handles scheme:xxx
    if (colon != -1 && (colon < slash || slash == -1))
      return super.lookupImpl(userPath, newAttributes);
      
      // //hostname
    if (slash == 0 && length > 1 && userPath.charAt(1) == '/')
      return schemeWalk(userPath, newAttributes, userPath, 0);

      // /path
    else if (slash == 0)
      newPath = normalizePath("/", userPath, 0, '/');

      // path
    else
      newPath = normalizePath(_pathname, userPath, 0, '/');

    // XXX: does missing root here cause problems with restrictions?
    return _root.fsWalk(userPath, newAttributes, newPath);
  }

  /**
   * Walk down the path starting from the portion immediately following
   * the scheme.  i.e. schemeWalk is responsible for parsing the host and
   * port from the URL.
   *
   * @param userPath the user's passed in path
   * @param attributes the attributes for the new path
   * @param uri the normalized full uri
   * @param offset offset into the uri to start processing, i.e. after the
   *  scheme.
   *
   * @return the looked-up path.
   */
  protected Path schemeWalk(String userPath,
                            Map<String,Object> attributes,
			    String uri,
                            int offset)
  {
    int length = uri.length();

    if (length < 2 + offset ||
        uri.charAt(offset) != '/' ||
        uri.charAt(offset + 1) != '/')
      throw new RuntimeException(L.l("bad scheme in `{0}'", uri));

    CharBuffer buf = CharBuffer.allocate();
    int i = 2 + offset;
    int ch = 0;
    for (; i < length && (ch = uri.charAt(i)) != ':' && ch != '/' && ch != '?';
	 i++) {
      buf.append((char) ch);
    }

    String host = buf.close();
    if (host.length() == 0)
      throw new RuntimeException(L.l("bad host in `{0}'", uri));

    int port = 0;
    if (ch == ':') {
      for (i++; i < length && (ch = uri.charAt(i)) >= '0' && ch <= '9'; i++) {
	port = 10 * port + uri.charAt(i) - '0';
      }
    }

    if (port == 0)
      port = 80;

    HttpPath root = create(host, port);

    return root.fsWalk(userPath, attributes, uri.substring(i));
  }

  /**
   * Scans the path portion of the URI, i.e. everything after the
   * host and port.
   *
   * @param userPath the user's supplied path
   * @param attributes the attributes for the new path
   * @param uri the full uri for the new path.
   *
   * @return the found path.
   */
  protected Path fsWalk(String userPath,
			Map<String,Object> attributes,
			String uri)
  {
    String path;
    String query = null;
    int queryIndex = uri.indexOf('?');
    if (queryIndex >= 0) {
      path = uri.substring(0, queryIndex);
      query = uri.substring(queryIndex + 1);
    } else
      path = uri;

    if (path.length() == 0)
      path = "/";

    return create(_root, userPath, attributes, path, query);
  }

  protected HttpPath create(String host, int port)
  {
    return new HttpPath(host, port);
  }

  protected HttpPath create(FilesystemPath root,
                            String userPath,
			    Map<String,Object> newAttributes,
                            String path, String query)
  {
    return new HttpPath(root, userPath, newAttributes, path, query);
  }

  /**
   * Returns the scheme, http.
   */
  public String getScheme()
  {
    return "http";
  }

  /**
   * Returns a full URL for the path.
   */
  public String getURL()
  {
    int port = getPort();

    return (getScheme() + "://" + getHost() + 
	    (port == 80 ? "" : ":" + getPort()) + 
	    getPath() + 
	    (_query == null ? "" : "?" + _query));
  }

  /**
   * Returns the host part of the url.
   */
  public String getHost()
  {
    return _host;
  }

  /**
   * Returns the port part of the url.
   */
  public int getPort()
  {
    return _port;
  }

  /**
   * Returns the user's path.
   */
  public String getUserPath()
  {
    return _userPath;
  }

  /**
   * Returns the query string.
   */
  public String getQuery()
  {
    return _query;
  }

  /**
   * Returns the last modified time.
   */
  public long getLastModified()
  {
    return getCache().lastModified;
  }

  /**
   * Returns the file's length
   */
  public long getLength()
  {
    return getCache().length;
  }

  /**
   * Returns true if the file exists.
   */
  public boolean exists()
  {
    return getCache().lastModified >= 0;
  }

  /**
   * Returns true if the file exists.
   */
  public boolean isFile()
  {
    return ! getPath().endsWith("/") && getCache().lastModified >= 0;
  }

  /**
   * Returns true if the file is readable.
   */
  public boolean canRead()
  {
    return isFile();
  }

  /**
   * Returns the last modified time.
   */
  public boolean isDirectory()
  {
    return getPath().endsWith("/") && getCache().lastModified >= 0;
  }
  
  /**
   * @return The contents of this directory or null if the path does not
   * refer to a directory.
   */
  public String []list() throws IOException
  {
    try {
      HttpStream stream = (HttpStream) openReadWriteImpl();
      stream.setMethod("PROPFIND");
      stream.setAttribute("Depth", "1");

      WriteStream os = new WriteStream(stream);
      os.println("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
      os.println("<propfind xmlns=\"DAV:\"><prop>");
      os.println("<resourcetype/>");
      os.println("</prop></propfind>");
      os.flush();

      ReadStream is = new ReadStream(stream);
    
      ListHandler handler = new ListHandler(getPath());
      XmlParser parser = new XmlParser();
      parser.setContentHandler(handler);

      parser.parse(is);

      is.close();
      os.close();
      stream.close();

      ArrayList<String> names = handler.getNames();
      String []list = new String[names.size()];
      names.toArray(list);
      
      return list;
    } catch (Exception e) {
      throw new IOException(L.l("list() is not supported by this server"));
    }
  }

  protected CacheEntry getCache()
  {
    if (_cacheEntry == null) {
      synchronized (_cache) {
        _cacheEntry = _cache.get(getPath());
        if (_cacheEntry == null) {
          _cacheEntry = new CacheEntry();
          _cache.put(getPath(), _cacheEntry);
        }
      }
    }
    
    long now = Alarm.getCurrentTime();
    synchronized (_cacheEntry) {
      try {
        if (_cacheEntry.expires > now)
          return _cacheEntry;
      
        HttpStreamWrapper stream = (HttpStreamWrapper) openReadImpl();
        stream.setHead(true);
        stream.setSocketTimeout(120000);

        String status = (String) stream.getAttribute("status");
        if (status.equals("200")) {
          String lastModified = (String) stream.getAttribute("last-modified");

          _cacheEntry.lastModified = 0;
          if (lastModified != null) {
            QDate date = QDate.getGlobalDate();
            synchronized (date) {
              _cacheEntry.lastModified = date.parseDate(lastModified);
            }
          }

          String length =  (String) stream.getAttribute("content-length");
          _cacheEntry.length = 0;
          if (length != null) {
            _cacheEntry.length = Integer.parseInt(length);
          }
        }
        else
          _cacheEntry.lastModified = -1;
        
        _cacheEntry.expires = now + 5000;
      
        stream.close();
        return _cacheEntry;
      } catch (Exception e) {
        _cacheEntry.lastModified = -1;
        _cacheEntry.expires = now + 5000;

        return _cacheEntry;
      }
    }
  }
  

  /**
   * Returns a read stream for a GET request.
   */
  public StreamImpl openReadImpl() throws IOException
  {
    return HttpStream.openRead(this);
  }

  /**
   * Returns a read/write pair for a POST request.
   */
  public StreamImpl openReadWriteImpl() throws IOException
  {
    return HttpStream.openReadWrite(this);
  }

  @Override
  protected Path cacheCopy()
  {
    return new HttpPath(getRoot(), getUserPath(),
			null,
			getPath(), _query);
  }
  
  /**
   * Returns the string form of the http path.
   */
  public String toString()
  {
    return getURL();
  }

  /**
   * Returns a hashCode for the path.
   */
  public int hashCode()
  {
    return 65537 * super.hashCode() + 37 * _host.hashCode() + _port;
  }

  /**
   * Overrides equals to test for equality with an HTTP path.
   */
  public boolean equals(Object o)
  {
    if (! (o instanceof HttpPath))
      return false;

    HttpPath test = (HttpPath) o;

    if (! _host.equals(test._host))
      return false;
    else if (_port != test._port)
      return false;
    else if (_query != null && ! _query.equals(test._query))
      return false;
    else if (_query == null && test._query != null)
      return false;
    else
      return true;
  }

  static class CacheEntry {
    long lastModified;
    long length;
    boolean canRead;
    long expires;
  }

  static class ListHandler extends org.xml.sax.helpers.DefaultHandler {
    String _prefix;
    ArrayList<String> _names = new ArrayList<String>();
    boolean _inHref;

    ListHandler(String prefix)
    {
      _prefix = prefix;
    }
    
    ArrayList<String> getNames()
    {
      return _names;
    }

    public void startElement (String uri, String localName,
			      String qName, Attributes attributes)
    {
      if (localName.equals("href"))
        _inHref = true;
    }

    public void characters(char []data, int offset, int length)
	throws SAXException
    {
      if (! _inHref)
        return;

      String href = new String(data, offset, length).trim();
      if (! href.startsWith(_prefix))
        return;

      href = href.substring(_prefix.length());
      if (href.startsWith("/"))
        href = href.substring(1);

      int p = href.indexOf('/');
      if (href.equals("") || p == 0)
        return;

      if (p < 0)
        _names.add(href);
      else
        _names.add(href.substring(0, p));
    }

    public void endElement (String uri, String localName, String qName)
	throws SAXException
    {
      if (localName.equals("href"))
        _inHref = false;
    }
  }
}
