/*
 * ====================================================================
 *
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 1999 The Apache Software Foundation.  All rights 
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer. 
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution, if
 *    any, must include the following acknowlegement:  
 *       "This product includes software developed by the 
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowlegement may appear in the software itself,
 *    if and wherever such third-party acknowlegements normally appear.
 *
 * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software
 *    Foundation" must not be used to endorse or promote products derived
 *    from this software without prior written permission. For written 
 *    permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache"
 *    nor may "Apache" appear in their names without prior written
 *    permission of the Apache Group.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 * [Additional notices, if required by prior licensing conditions]
 *
 */ 

package org.apache.tomcat.util.http;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.StringTokenizer;

import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.buf.MessageBytes;

/**
 * A collection of cookies - reusable and tuned for server side performance.
 * Based on RFC2965 ( and 2109 )
 *
 * This class is not synchronized.
 *
 * @author Costin Manolache
 * @author kevin seguin
 */
public final class Cookies { // extends MultiMap {

    // expected average number of cookies per request
    public static final int INITIAL_SIZE=4; 
    ServerCookie scookies[]=new ServerCookie[INITIAL_SIZE];
    int cookieCount=0;
    boolean unprocessed=true;

    MimeHeaders headers;
    
    /**
     *  Construct a new cookie collection, that will extract
     *  the information from headers.
     *
     * @param headers Cookies are lazy-evaluated and will extract the
     *     information from the provided headers.
     */
    public Cookies(MimeHeaders headers) {
	this.headers=headers;
    }

    /**
     * Construct a new uninitialized cookie collection.
     * Use {@link #setHeaders} to initialize.
     */
    // [seguin] added so that an empty Cookies object could be
    // created, have headers set, then recycled.
    public Cookies() {
    }

    /**
     * Set the headers from which cookies will be pulled.
     * This has the side effect of recycling the object.
     *
     * @param headers Cookies are lazy-evaluated and will extract the
     *     information from the provided headers.
     */
    // [seguin] added so that an empty Cookies object could be
    // created, have headers set, then recycled.
    public void setHeaders(MimeHeaders headers) {
        recycle();
        this.headers=headers;
    }

    /**
     * Recycle.
     */
    public void recycle() {
    	for( int i=0; i< cookieCount; i++ ) {
	    if( scookies[i]!=null )
		scookies[i].recycle();
	}
	cookieCount=0;
	unprocessed=true;
    }

    /**
     * EXPENSIVE!!!  only for debugging.
     */
    public String toString() {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        pw.println("=== Cookies ===");
        int count = getCookieCount();
        for (int i = 0; i < count; ++i) {
            pw.println(getCookie(i).toString());
        }
        return sw.toString();
    }

    // -------------------- Indexed access --------------------
    
    public ServerCookie getCookie( int idx ) {
	if( unprocessed ) {
	    getCookieCount(); // will also update the cookies
	}
	return scookies[idx];
    }

    public int getCookieCount() {
	if( unprocessed ) {
	    unprocessed=false;
	    processCookies(headers);
	}
	return cookieCount;
    }

    // -------------------- Adding cookies --------------------

    /** Register a new, unitialized cookie. Cookies are recycled, and
     *  most of the time an existing ServerCookie object is returned.
     *  The caller can set the name/value and attributes for the cookie
     */
    public ServerCookie addCookie() {
	if( cookieCount >= scookies.length  ) {
	    ServerCookie scookiesTmp[]=new ServerCookie[2*cookieCount];
	    System.arraycopy( scookies, 0, scookiesTmp, 0, cookieCount);
	    scookies=scookiesTmp;
	}
	
	ServerCookie c = scookies[cookieCount];
	if( c==null ) {
	    c= new ServerCookie();
	    scookies[cookieCount]=c;
	}
	cookieCount++;
	return c;
    }


    // code from CookieTools 

    /** Add all Cookie found in the headers of a request.
     */
    public  void processCookies( MimeHeaders headers ) {
	if( headers==null )
	    return;// nothing to process
	// process each "cookie" header
	int pos=0;
	while( pos>=0 ) {
	    // Cookie2: version ? not needed
	    pos=headers.findHeader( "Cookie", pos );
	    // no more cookie headers headers
	    if( pos<0 ) break;

	    MessageBytes cookieValue=headers.getValue( pos );
	    if( cookieValue==null || cookieValue.isNull() ) continue;

	    // Uncomment to test the new parsing code
	    if( cookieValue.getType() == MessageBytes.T_BYTES ) {
		if( dbg>0 ) log( "Parsing b[]: " + cookieValue.toString());
		ByteChunk bc=cookieValue.getByteChunk();
		processCookieHeader( bc.getBytes(),
				     bc.getOffset(),
				     bc.getLength());
	    } else {
		if( dbg>0 ) log( "Parsing S: " + cookieValue.toString());
		processCookieHeader( cookieValue.toString() );
	    }
	    pos++;// search from the next position
	}
    }

    /** Process a byte[] header - allowing fast processing of the
     *  raw data
     */
    void processCookieHeader(  byte bytes[], int off, int len )
    {
	if( len<=0 || bytes==null ) return;
	int end=off+len;
	int pos=off;
	
	int version=0; //sticky
	ServerCookie sc=null;
	

	while( pos<end ) {
	    byte cc;
	    // [ skip_spaces name skip_spaces "=" skip_spaces value EXTRA ; ] *
	    if( dbg>0 ) log( "Start: " + pos + " " + end );
	    
	    pos=skipSpaces(bytes, pos, end);
	    if( pos>=end )
		return; // only spaces
	    int startName=pos;
	    if( dbg>0 ) log( "SN: " + pos );
	    
	    // Version should be the first token
	    boolean isSpecial=false;
	    if(bytes[pos]=='$') { pos++; isSpecial=true; }

	    pos= findDelim1( bytes, startName, end); // " =;,"
	    int endName=pos;
	    // current = "=" or " " or DELIM
	    pos= skipSpaces( bytes, endName, end ); 
	    if( dbg>0 ) log( "DELIM: " + endName + " " + (char)bytes[pos]);

	    if(pos >= end ) {
		// it's a name-only cookie ( valid in RFC2109 )
		if( ! isSpecial ) {
		    sc=addCookie();
		    sc.getName().setBytes( bytes, startName,
					   endName-startName );
		    sc.getValue().setString("");
		    sc.setVersion( version );
		    if( dbg>0 ) log( "Name only, end: " + startName + " " +
				     endName);
		}
		return;
	    }

	    cc=bytes[pos];
	    pos++;
	    if( cc==';' || cc==',' ) {
		if( ! isSpecial && startName!= endName ) {
		    sc=addCookie();
		    sc.getName().setBytes( bytes, startName,
					   endName-startName );
		    sc.getValue().setString("");
		    sc.setVersion( version );
		    if( dbg>0 ) log( "Name only: " + startName + " " + endName);
		}
		continue;
	    }
	    
	    // we should have "=" ( tested all other alternatives )
	    int startValue=skipSpaces( bytes, pos, end);
	    int endValue=startValue;
	    
	    // quote is valid only in version=1 cookies
	    cc=bytes[pos];
	    if( ( version == 1 || isSpecial ) && ( cc== '\'' || cc=='"' ) ) {
		startValue++;
		endValue=indexOf( bytes, startValue, end, cc );
		pos=endValue+1; // to skip to next cookie
 	    } else {
		endValue=findDelim2( bytes, startValue, end );
		pos=endValue+1;
	    }
	    
	    // if not $Version, etc
	    if( ! isSpecial ) {
		sc=addCookie();
		sc.getName().setBytes( bytes, startName, endName-startName );
		sc.getValue().setBytes( bytes, startValue, endValue-startValue);
		sc.setVersion( version );
		if( dbg>0 ) log( "New: " + sc.getName() + "X=X" + sc.getValue());
		continue;
	    }
	    
	    // special - Path, Version, Domain, Port
	    if( dbg>0 ) log( "Special: " + startName + " " + endName);
	    // XXX TODO
	    if( equals( "$Version", bytes, startName, endName ) ) {
		if(dbg>0 ) log( "Found version " );
		if( bytes[startValue]=='1' && endValue==startValue+1 ) {
		    version=1;
		    if(dbg>0 ) log( "Found version=1" );
		}
		continue;
	    }
	    if( sc==null ) {
		// Path, etc without a previous cookie
		continue;
	    }
	    if( equals( "$Path", bytes, startName, endName ) ) {
		sc.getPath().setBytes( bytes, startValue, endValue-startValue );
	    }
	    if( equals( "$Domain", bytes, startName, endName ) ) {
		sc.getDomain().setBytes( bytes, startValue, endValue-startValue );
	    }
	    if( equals( "$Port", bytes, startName, endName ) ) {
		// sc.getPort().setBytes( bytes, startValue, endValue-startValue );
	    }
	}
    }

    // -------------------- Utils --------------------
    public static int skipSpaces(  byte bytes[], int off, int end ) {
	while( off < end ) {
	    byte b=bytes[off];
	    if( b!= ' ' ) return off;
	    off ++;
	}
	return off;
    }

    public static int findDelim1( byte bytes[], int off, int end )
    {
	while( off < end ) {
	    byte b=bytes[off];
	    if( b==' ' || b=='=' || b==';' || b==',' )
		return off;
	    off++;
	}
	return off;
    }

    public static int findDelim2( byte bytes[], int off, int end )
    {
	while( off < end ) {
	    byte b=bytes[off];
	    if( b==';' || b==',' )
		return off;
	    off++;
	}
	return off;
    }

    public static int indexOf( byte bytes[], int off, int end, byte qq )
    {
	while( off < end ) {
	    byte b=bytes[off];
	    if( b==qq )
		return off;
	    off++;
	}
	return off;
    }

    public static int indexOf( byte bytes[], int off, int end, char qq )
    {
	while( off < end ) {
	    byte b=bytes[off];
	    if( b==qq )
		return off;
	    off++;
	}
	return off;
    }
    
    // XXX will be refactored soon!
    public static boolean equals( String s, byte b[], int start, int end) {
	int blen = end-start;
	if (b == null || blen != s.length()) {
	    return false;
	}
	int boff = start;
	for (int i = 0; i < blen; i++) {
	    if (b[boff++] != s.charAt(i)) {
		return false;
	    }
	}
	return true;
    }
    

    // ---------------------------------------------------------
    // -------------------- DEPRECATED, OLD --------------------
    
    private void processCookieHeader(  String cookieString )
    {
	if( dbg>0 ) log( "Parsing cookie header " + cookieString );
	// normal cookie, with a string value.
	// This is the original code, un-optimized - it shouldn't
	// happen in normal case

	StringTokenizer tok = new StringTokenizer(cookieString,
						  ";", false);
	while (tok.hasMoreTokens()) {
	    String token = tok.nextToken();
	    int i = token.indexOf("=");
	    if (i > -1) {
		
		// XXX
		// the trims here are a *hack* -- this should
		// be more properly fixed to be spec compliant
		
		String name = token.substring(0, i).trim();
		String value = token.substring(i+1, token.length()).trim();
		// RFC 2109 and bug 
		value=stripQuote( value );
		ServerCookie cookie = addCookie();
		
		cookie.getName().setString(name);
		cookie.getValue().setString(value);
		if( dbg > 0 ) log( "Add cookie " + name + "=" + value);
	    } else {
		// we have a bad cookie.... just let it go
	    }
	}
    }

    /**
     *
     * Strips quotes from the start and end of the cookie string
     * This conforms to RFC 2109
     * 
     * @param value            a <code>String</code> specifying the cookie 
     *                         value (possibly quoted).
     *
     * @see #setValue
     *
     */
    private static String stripQuote( String value )
    {
	//	log("Strip quote from " + value );
	if (((value.startsWith("\"")) && (value.endsWith("\""))) ||
	    ((value.startsWith("'") && (value.endsWith("'"))))) {
	    try {
		return value.substring(1,value.length()-1);
	    } catch (Exception ex) { 
	    }
	}
	return value;
    }  


    // log
    static final int dbg=0;
    public void log(String s ) {
	System.out.println("Cookies: " + s);
    }

    /*
    public static void main( String args[] ) {
	test("foo=bar; a=b");
	test("foo=bar;a=b");
	test("foo=bar;a=b;");
	test("foo=bar;a=b; ");
	test("foo=bar;a=b; ;");
	test("foo=;a=b; ;");
	test("foo;a=b; ;");
	// v1 
	test("$Version=1; foo=bar;a=b"); 
        test("$Version=\"1\"; foo='bar'; $Path=/path; $Domain=\"localhost\"");
	test("$Version=1;foo=bar;a=b; ; ");
	test("$Version=1;foo=;a=b; ; ");
	test("$Version=1;foo= ;a=b; ; ");
	test("$Version=1;foo;a=b; ; ");
	test("$Version=1;foo=\"bar\";a=b; ; ");
	test("$Version=1;foo=\"bar\";$Path=/examples;a=b; ; ");
	test("$Version=1;foo=\"bar\";$Domain=apache.org;a=b");
	test("$Version=1;foo=\"bar\";$Domain=apache.org;a=b;$Domain=yahoo.com");
	// rfc2965
	test("$Version=1;foo=\"bar\";$Domain=apache.org;$Port=8080;a=b");

	// wrong
	test("$Version=1;foo=\"bar\";$Domain=apache.org;$Port=8080;a=b");
    }

    public static void test( String s ) {
	System.out.println("Processing " + s );
	Cookies cs=new Cookies(null);
	cs.processCookieHeader( s.getBytes(), 0, s.length());
	for( int i=0; i< cs.getCookieCount() ; i++ ) {
	    System.out.println("Cookie: " + cs.getCookie( i ));
	}
	    
    }
    */

}
