// NegociatedFrame.java
// $Id: NegociatedFrame.java,v 1.1 1998/01/29 16:25:02 bmahe Exp $
// (c) COPYRIGHT MIT and INRIA, 1996.
// Please first read the full copyright statement in file COPYRIGHT.html

package org.w3c.jigsaw.frames;

import java.io.*;
import java.util.*;

import org.w3c.tools.resources.*;
import org.w3c.jigsaw.http.* ;
import org.w3c.jigsaw.html.*;
import org.w3c.jigsaw.html.HtmlGenerator ;
import org.w3c.www.mime.* ;
import org.w3c.www.http.*;

import org.w3c.tools.resources.ProtocolException;
import org.w3c.tools.resources.NotAProtocolException;

public class NegociatedFrame extends HTTPFrame {
  
  class VariantState {
    ResourceReference variant = null ;
    double qs      = 0.0 ;
    double qe      = 0.0 ;
    double qc      = 0.0 ;
    double ql      = 0.0 ;
    double q       = 0.0 ;	// quality (mime type one)
    double Q       = 0.0 ;	// the big Q

    public String toString() {
      try {
	Resource res = variant.lock();
	String name = (String) res.getIdentifier() ;
	if ( name == null )
	  name = "<noname>" ;
	return "[" + name 
	  + " qs=" + qs 
	  + " qe=" + qe
	  + " ql=" + ql
	  + " q =" + q
	  + " Q =" + getQ() 
	  +"]" ;
      } catch (InvalidResourceException ex) {
	return "invalid";
      } finally {
	variant.unlock();
      }
    }

    void setContentEncodingQuality (double qe) {
      this.qe = qe ;
    }

    void setQuality (double q) {
      this.q = q ;
    }

    void setQuality (HttpAccept a) {
      q = a.getQuality() ;
    }

    void setLanguageQuality (double ql) {
      this.ql = ql ;
    }

    void setLanguageQuality (HttpAcceptLanguage l) {
      this.ql = l.getQuality() ;
    }

    double getLanguageQuality () {
      return ql ;
    }

    ResourceReference getResource () {
      return variant ;
    }

    double getQ() {
      return qe * q * qs * ql ;
    }

    VariantState (ResourceReference variant, double qs) {
      this.qs      = qs ;
      this.variant = variant ;
    }
  }

  private static Class httpFrameClass = null;

  static {
    try {
      httpFrameClass = Class.forName("org.w3c.jigsaw.frames.HTTPFrame") ;
    } catch (Exception ex) {
      throw new RuntimeException("No HTTPFrame class found.");
    }
  }

  /**
   * Turn debugging on/off.
   */
  private static final boolean debug = false;
  /**
   * Minimum quality for a resource to be considered further.
   */
  private static final double REQUIRED_QUALITY = 0.0001 ;
  /**
   * The Vary header field for this resource is always the same.
   */
  protected static HttpTokenList VARY = null;

  /**
   * Attribute index - The set of names of variants.
   */
  protected static int ATTR_VARIANTS = -1 ;

  static {
    // Compute and initialize the Vary header once and for all
    String vary[] = { "Accept",
		      "Accept-Charset",
		      "Accept-Language",
		      "Accept-Encoding" };
    VARY = HttpFactory.makeStringList(vary);
  }

  static {
    Attribute   a = null ;
    Class     cls = null ;
    try {
      cls = Class.forName("org.w3c.jigsaw.frames.NegociatedFrame") ;
    } catch (Exception ex) {
      ex.printStackTrace() ;
      System.exit(1) ;
    }
    // The names of the varint we negotiate
    a = new StringArrayAttribute("variants"
				 , null
				 , Attribute.EDITABLE) ;
    ATTR_VARIANTS = AttributeRegistry.registerAttribute(cls, a) ;
  }

  /**
   * Get the variant names.
   */
  public String[] getVariantNames() {
    return (String[]) getValue(ATTR_VARIANTS, null) ;
  }

  public void setVariants(String variants[]) {
    setValue(ATTR_VARIANTS, variants);
  }

  /**
   * Get the variant resources.
   * This is somehow broken, it shouldn't allocate the array of variants
   * on each call. However, don't forget that the list of variants can be
   * dynamically edited, this means that if we are to keep a cache of it 
   * (as would be the case if we kept the array of variants as instance var)
   * we should also take care of editing of attributes (possible, but I
   * just don't have enough lifes).
   * @return An array of ResourceReference, or <strong>null</strong>.
   * @exception ProtocolException If one of the variants doesn't exist.
   */

  public ResourceReference[] getVariantResources() 
    throws ProtocolException
  {
    // Get the variant names:
    String names[] = getVariantNames() ;
    if ( names == null )
      return null ;
    // Look them up in our parent directory:
    ResourceReference variants[] = new ResourceReference[names.length] ;
    ResourceReference r_parent     = resource.getParent() ;
    try {
      DirectoryResource parent = (DirectoryResource) r_parent.lock();
      for (int i = 0 ; i < names.length ; i++) {
	variants[i] = parent.lookup(names[i]) ;
	if (variants[i] == null)
	  throw new HTTPException(names[i]+": couldn't be restored.");
      }
    } catch (InvalidResourceException ex) {
      throw new HTTPException("invalid parent for negotiation");
    } finally {
      r_parent.unlock();
    }
    return variants ;
  }

  /**
   * Print the current negotiation state.
   * @param header The header to print first.
   * @param states The current negotiation states.
   */
  
  protected void printNegotiationState (String header, Vector states) {
    if ( debug ) {
      System.out.println ("------" + header) ;
      for (int i = 0 ; i < states.size() ; i++) {
	VariantState state = (VariantState) states.elementAt(i) ;
	System.out.println (state) ;
      }
      System.out.println ("-----") ;
    }
  }
  
  /**
   * Negotiate among content encodings.
   * <p>BUG: This will work only for single encoded variants.
   * @param states The current negotiation states.
   * @param request The request to handle.
   */
  
  protected boolean negotiateContentEncoding (Vector states,
					      Request request) 
    throws ProtocolException
  {
    if ( ! request.hasAcceptEncoding() ) {
      // All encodings accepted:
      for (int i = 0 ; i < states.size() ; i++) {
	VariantState state = (VariantState) states.elementAt(i) ;
	state.setContentEncodingQuality(1.0) ;
      }
    } else {
      String encodings[] = request.getAcceptEncoding() ;
      for (int i = 0 ; i < states.size() ; i++) {
	VariantState state    = (VariantState) states.elementAt(i) ;
	ResourceReference rr  = state.getResource();
	try {
	  FramedResource resource = (FramedResource)rr.lock() ;
	  HTTPFrame itsframe = 
	    (HTTPFrame) resource.getFrame(httpFrameClass);
	  if (itsframe != null) {
	    if ( ! itsframe.definesAttribute ("content-encoding") ) {
	      state.setContentEncodingQuality (1.0) ;
	    } else {
	      String ve = itsframe.getContentEncoding() ;
	      state.setContentEncodingQuality (0.001) ;
	    encoding_loop:
	      for (int j = 0 ; j < encodings.length ; j++) {
		if ( ve.equals (encodings[j]) ) {
		  state.setContentEncodingQuality(1.0) ;
		  break encoding_loop ;
		}
	      }
	    }
	  }
	} catch (InvalidResourceException ex) {
	  
	} finally {
	  rr.unlock();
	}
      }
      // FIXME We should check here against unlegible variants as now
    }
    return false ;
  }

  /**
   * Negotiate on charsets.
   * <p>BUG: Not implemented yet.
   * @param states The current states of negotiation.
   * @param request The request to handle.
   */

  protected boolean negotiateCharsetQuality (Vector states
					     , Request request) {
    return false ;
  }

  /**
   * Negotiate among language qualities.
   * <p>BUG: This will only work for variants that have one language tag.
   * @param states The current states of negotiation.
   * @param request The request to handle.
   */

  protected boolean negotiateLanguageQuality (Vector states
					      , Request request) 
    throws ProtocolException
  {
    if ( ! request.hasAcceptLanguage() ) {
      for (int i = 0 ; i < states.size() ; i++) {
	VariantState state = (VariantState) states.elementAt(i) ;
	state.setLanguageQuality (1.0) ;
      }
    } else {
      HttpAcceptLanguage languages[] = request.getAcceptLanguage() ;
      boolean  varyLang    = false ;
      for (int i = 0 ; i < states.size() ; i++) {
	VariantState state    = (VariantState) states.elementAt(i) ;
	ResourceReference rr  = state.getResource();
	try {
	  FramedResource resource = (FramedResource)rr.lock() ;
	  HTTPFrame itsframe = 
	    (HTTPFrame) resource.getFrame(httpFrameClass);
	  if (itsframe != null) {
	    if ( ! itsframe.definesAttribute ("content-language") ) {
	      state.setLanguageQuality (-1.0) ;
	    } else {
	      varyLang = true ;
	      String lang = itsframe.getContentLanguage() ;
	      int jidx    = -1 ;
	      for (int j = 0 ; j < languages.length ; j++) {
		if ( languages[j].getLanguage().equals(lang) )
		  jidx = j ;
	      }
	      if ( jidx < 0 ) 
		state.setLanguageQuality(0.001) ;
	      else 
		state.setLanguageQuality (languages[jidx]) ;
	    }
	  }
	} catch (InvalidResourceException ex) {
	  //FIXME
	} finally {
	  rr.unlock();
	}
      }
      if ( varyLang ) {
	for (int i = 0 ; i < states.size() ; i++) {
	  VariantState s = (VariantState) states.elementAt(i);
	  if ( s.getLanguageQuality() < 0 )
	    s.setLanguageQuality (0.5) ;
	}
      } else {
	for (int i = 0 ; i < states.size() ; i++) {
	  VariantState s = (VariantState) states.elementAt(i) ;
	  s.setLanguageQuality (1.0) ;
	}
      }
    }
    return false ;
  }

  /**
   * Negotiate among content types.
   * @param states The current states of negotiation.
   * @param request The request to handle.
   */

  protected boolean negotiateContentType (Vector states,
					  Request request) 
    throws ProtocolException
  {
    if ( ! request.hasAccept() ) {
      // All variants get a quality of 1.0
      for (int i = 0 ; i < states.size() ; i++) {
	VariantState state = (VariantState) states.elementAt(i) ;
	state.setQuality (1.0) ;
      }
    } else {
      // The browser has given some preferences:
      HttpAccept accepts[] = request.getAccept() ;
      for (int i = 0 ; i < states.size() ; i++ ) {
	VariantState state = (VariantState) states.elementAt(i) ;
	// Get the most specific match for this variant:
	ResourceReference rr = state.getResource();
	try {
	  FramedResource resource = (FramedResource)rr.lock() ;
	  HTTPFrame itsframe = 
	    (HTTPFrame) resource.getFrame(httpFrameClass);
	  if (itsframe != null) {
	    MimeType vt = itsframe.getContentType();
	    int jmatch = -1 ;
	    int jidx   = -1 ;
	    for (int j = 0 ; j < accepts.length ; j++) {
	      int match = vt.match (accepts[j].getMimeType()) ;
	      if ( match > jmatch ) {
		jmatch = match ;
		jidx   = j ;
	      }
	    }
	    if ( jidx < 0 )
	      state.setQuality (0.0) ;
	    else 
	      state.setQuality(accepts[jidx]) ;
	  }
	} catch (InvalidResourceException ex) {
	  //FIXME
	} finally {
	  rr.unlock();
	}
      }
    }
    return false ;
  }

  /**
   * Negotiate among the various variants for the Resource.
   * We made our best efforts to be as compliant as possible to the HTTP/1.0
   * content negotiation algorithm.
   */
  protected ResourceReference negotiate (Request request) 
    throws ProtocolException
  {
    // Check for zero or one variant:
    ResourceReference variants[] = getVariantResources() ;
    if ( variants.length < 2 ) {
      if ( variants.length == 0 ) {
	Reply reply = request.makeReply(HTTP.NONE_ACCEPTABLE) ;
	reply.setContent ("<p>No acceptable variants.") ;
	throw new HTTPException (reply) ;
      } else {
	return variants[0] ;
      }
    }
    // Build a vector of variant negociation states, one per variants:
    Vector states = new Vector (variants.length) ;
    for (int i = 0 ; i < variants.length ; i++) {
      double qs = 1.0 ;
      try {
	FramedResource resource = (FramedResource)variants[i].lock() ;
	HTTPFrame itsframe = 
	  (HTTPFrame) resource.getFrame(httpFrameClass);
	if (itsframe != null) {
	  if ( itsframe.definesAttribute ("quality") )
	    qs = itsframe.getQuality() ;
	  if ( qs > REQUIRED_QUALITY )
	    states.addElement (new VariantState (variants[i], qs)) ;
	}
      } catch (InvalidResourceException ex) {
	//FIXME
      } finally {
	variants[i].unlock();
      }
    }
    // Content-encoding negociation:
    if ( debug )
      printNegotiationState ("init:", states) ;
    if ( negotiateContentEncoding (states, request) ) 
      // Remains a single acceptable variant:
      return ((VariantState) states.elementAt(0)).getResource() ;
    if ( debug )
      printNegotiationState ("encoding:", states) ;
    // Charset quality negociation:
    if ( negotiateCharsetQuality (states, request) ) 
      // Remains a single acceptable variant:
      return ((VariantState) states.elementAt(0)).getResource() ;
    if ( debug )
      printNegotiationState ("charset:", states) ;
    // Language quality negociation:
    if ( negotiateLanguageQuality (states, request) ) 
      // Remains a single acceptable variant:
      return ((VariantState) states.elementAt(0)).getResource() ;
    if ( debug )
      printNegotiationState ("language:", states) ;
    // Content-type negociation:
    if ( negotiateContentType (states, request) )
      // Remains a single acceptable variant:
      return ((VariantState) states.elementAt(0)).getResource() ;
    if ( debug )
      printNegotiationState ("type:", states) ;
    // If we reached this point, this means that multiple variants are 
    // acceptable at this point. Keep the one that have the best quality.
    if ( debug )
      printNegotiationState ("before Q selection:", states) ;
    double qmax = REQUIRED_QUALITY ;
    for (int i = 0 ; i < states.size() ; ) {
      VariantState state = (VariantState) states.elementAt(i) ;
      if ( state.getQ() > qmax ) {
	for (int j = i ; j > 0 ; j--)
	  states.removeElementAt(0) ;
	qmax = state.getQ() ;
	i = 1 ;
      } else {
	states.removeElementAt(i) ;
      }
    }
    if ( debug )
      printNegotiationState ("After Q selection:", states) ;
    if ( qmax == REQUIRED_QUALITY ) {
      Reply reply = request.makeReply(HTTP.NONE_ACCEPTABLE) ;
      reply.setContent ("<p>No acceptable variant.") ;
      throw new HTTPException (reply) ;
    } else if ( states.size() == 1 ) {
      return ((VariantState) states.elementAt(0)).getResource() ;
    } else {
      // Respond with multiple choice (for the time being, there should
      // be a parameter to decide what to do.
      Reply reply = request.makeReply(HTTP.MULTIPLE_CHOICE) ;
      HtmlGenerator g = new HtmlGenerator ("Multiple choice for "+
					   resource.getIdentifier()) ;
      g.append ("<ul>") ;
      for (int i = 0 ; i < states.size() ; i++) {
	VariantState state = (VariantState) states.elementAt(i) ;
	String name = null;
	ResourceReference rr = state.getResource();
	try {
	  name = rr.lock().getIdentifier();
	  g.append ("<li>" 
		    + "<a href=\"" + name + "\">" + name + "</a>"
		    + " Q= " + state.getQ()) ;
	} catch (InvalidResourceException ex) {
	  //FIXME
	} finally {
	  rr.unlock();
	}
      }
      reply.setStream (g) ;
      throw new HTTPException (reply) ;
    }
  }

  public void registerResource(FramedResource resource) {
    super.registerOtherResource(resource);
  }

  /**
   * Perform an HTTP request.
   * Negotiate among the variants, the best variant according to the request
   * fields, and make this elect3d variant serve the request.
   * @param request The request to handle.
   * @exception ProtocolException If negotiating among the resource variants 
   *    failed.
   */

  public ReplyInterface perform(RequestInterface req)
    throws ProtocolException, NotAProtocolException
  {
    ReplyInterface repi = performFrames(req);
    if (repi != null)
      return repi;

    if (! checkRequest(req))
      return null;

    Request request = (Request) req;
    // Run content negotiation now:
    ResourceReference selected = negotiate(request) ;
    // This should never happen: either the negotiation succeed, or the
    // negotiate method should return an error.
    if ( selected == null ) {
      Reply error = request.makeReply(HTTP.INTERNAL_SERVER_ERROR) ;
      error.setContent("Error negotiating among resource's variants.");
      throw new HTTPException(error) ;
    }
    // FIXME content neg should be done at lookup time
    // FIXME enhencing the reply should be done at outgoingfilter
    // Get the original variant reply, and add its location as a header:
    try {
      FramedResource resource = (FramedResource) selected.lock();
      Reply reply = (Reply)resource.perform(request) ;
      reply.setHeaderValue(reply.H_VARY, VARY);
      HTTPFrame itsframe = 
	(HTTPFrame) resource.getFrame(httpFrameClass);
      if (itsframe != null) {
	reply.setContentLocation(itsframe.getURL(request).toExternalForm()) ;
	return reply;
      }
      Reply error = request.makeReply(HTTP.INTERNAL_SERVER_ERROR) ;
      error.setContent("Error negotiating : "+
		       "selected resource has no HTTPFrame");
      throw new HTTPException(error) ;
    } catch (InvalidResourceException ex) {
      Reply error = request.makeReply(HTTP.INTERNAL_SERVER_ERROR) ;
      error.setContent("Error negotiating : Invalid selected resource");
      throw new HTTPException(error) ;
    } finally {
      selected.unlock();
    }
  }
  
}
