//______________________________________________________________________________
//   |       |
//   | knipp |           Knipp Medien und Kommunikation GmbH
//    -------
//
//
//     Knipp RRI Toolkit
//
//
//     Copyright (C) 2005 by Knipp Medien und Kommunikation GmbH
//
//     This library is free software; you can redistribute it and/or
//     modify it under the terms of the GNU Lesser General Public License 
//     as published by the Free Software Foundation; either version 2.1 
//     of the License, or any later version.
// 
//     This library 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. See the 
//     GNU Lesser General Public License for more details.
//
//     You should have received a copy of the GNU Lesser General
//     Public License along with this library; if not, write to the 
//     Free Software Foundation, Inc., 59 Temple Place, Suite 330, 
//     Boston, MA 02111-1307 USA 
//
//
//     initial author:    Klaus Malorny
//
//     version:           1.3 (15112), 2011/05/27 16:00:14
//                        $Revision: 14936 $ 
//                        $Date: 2010-11-05 15:57:40 +0100 (Fri, 05 Nov 2010) $
//                        $Author: Klaus.Malorny $
//

package de.knipp.rritoolkit;

import java.util.Map;
import java.util.HashMap;
import java.util.SortedSet;
import java.util.TreeSet;

import javax.xml.namespace.QName;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.DOMException;

import de.knipp.rritoolkit.request.ContactCheckRequest;
import de.knipp.rritoolkit.request.ContactCreateRequest;
import de.knipp.rritoolkit.request.ContactInfoRequest;
import de.knipp.rritoolkit.request.ContactUpdateRequest;
import de.knipp.rritoolkit.request.DomainChangeHolderRequest;
import de.knipp.rritoolkit.request.DomainChangeProvRequest;
import de.knipp.rritoolkit.request.DomainCheckRequest;
import de.knipp.rritoolkit.request.DomainCreateRequest;
import de.knipp.rritoolkit.request.DomainDeleteRequest;
import de.knipp.rritoolkit.request.DomainInfoRequest;
import de.knipp.rritoolkit.request.DomainTransitRequest;
import de.knipp.rritoolkit.request.DomainUpdateRequest;
import de.knipp.rritoolkit.request.DomainMigrateDescrRequest;
import de.knipp.rritoolkit.request.LoginRequest;
import de.knipp.rritoolkit.request.LogoutRequest;
import de.knipp.rritoolkit.request.QueueDeleteRequest;
import de.knipp.rritoolkit.request.QueueReadRequest;
import de.knipp.rritoolkit.request.AuthInfo1CreateRequest;
import de.knipp.rritoolkit.request.AuthInfo1DeleteRequest;
import de.knipp.rritoolkit.request.AuthInfo2CreateRequest;
import de.knipp.rritoolkit.request.Request;

import de.knipp.rritoolkit.response.ContactCheckResponse;
import de.knipp.rritoolkit.response.ContactCreateResponse;
import de.knipp.rritoolkit.response.ContactInfoResponse;
import de.knipp.rritoolkit.response.ContactUpdateResponse;
import de.knipp.rritoolkit.response.DomainChangeHolderResponse;
import de.knipp.rritoolkit.response.DomainChangeProvResponse;
import de.knipp.rritoolkit.response.DomainCheckResponse;
import de.knipp.rritoolkit.response.DomainCreateResponse;
import de.knipp.rritoolkit.response.DomainDeleteResponse;
import de.knipp.rritoolkit.response.DomainInfoResponse;
import de.knipp.rritoolkit.response.DomainTransitResponse;
import de.knipp.rritoolkit.response.DomainUpdateResponse;
import de.knipp.rritoolkit.response.DomainMigrateDescrResponse;
import de.knipp.rritoolkit.response.LoginResponse;
import de.knipp.rritoolkit.response.LogoutResponse;
import de.knipp.rritoolkit.response.QueueDeleteResponse;
import de.knipp.rritoolkit.response.QueueReadResponse;
import de.knipp.rritoolkit.response.AuthInfo1CreateResponse;
import de.knipp.rritoolkit.response.AuthInfo1DeleteResponse;
import de.knipp.rritoolkit.response.AuthInfo2CreateResponse;
import de.knipp.rritoolkit.response.Response;

import static de.knipp.rritoolkit.DenicConstants.NS_GLOBAL;
import static de.knipp.rritoolkit.DenicConstants.NS_CONTACT;
import static de.knipp.rritoolkit.DenicConstants.NS_DOMAIN;
import static de.knipp.rritoolkit.DenicConstants.NS_MESSAGE;
import static de.knipp.rritoolkit.DenicConstants.NS_TXN;

import static de.knipp.rritoolkit.XmlHelper.getFirstElement;

/**
 *  default implementation of the response factory
 *
 *  @version  $Revision: 14936 $
 *  @author   Klaus Malorny
 */

public class DefaultResponseFactory implements ResponseFactory
{
  /** map from the name of the first element to the class */
  
  private Map<QName, Class<? extends Response>> xmlMap;  
  
  
  
  /** the set of rules */
  
  private SortedSet<ListRule> rules;
  
  
  
  /** the default class */
  
  private Class<? extends Response> defaultClass;
  
  
  
  /** singleton instance with default settings */
  
  private static DefaultResponseFactory singleton;
  
  /**
   *  class constructor
   */
  
  static
  {
    singleton = new DefaultResponseFactory ();
    singleton.createDefaults ();
  }
  
  
  
  /**
   *  constructor. Protected to avoid direct instantiation, but allowing
   *  subclassing. For defaults to be added, {@code createDefaults}
   *  needs to be called.
   */
  
  protected DefaultResponseFactory ()
  {
    xmlMap = new HashMap<QName, Class<? extends Response>> ();
    rules = new TreeSet<ListRule> ();
    defaultClass = null;
  }
  
  
  
  /**
   *  set the default class
   *
   *  @param cls                     the default class
   */
  
  protected void setDefaultClass (Class<? extends Response> cls)
  {
    defaultClass = cls;
  }
  
  
  
  /**
   *  add an XML mapping
   *
   *  @param qName             the name of the XML element
   *  @param cls               the class
   */
  
  protected void addXmlMapping (QName qName, Class<? extends Response> cls)
  {
    xmlMap.put (qName, cls);
  }
  
  /**
   *  add an XML mapping
   *
   *  @param uri               the namespace URI
   *  @param localName         the local name
   *  @param cls               the class
   */
  
  protected void addXmlMapping (String uri, String localName, 
    Class<? extends Response> cls)
  {
    addXmlMapping (new QName (uri, localName), cls);
  }
  
  
  
  /**
   *  add a list rule
   *
   *  @param rule             the rule
   */
  
  protected void addRule (ListRule rule)
  {
    rules.add (rule);
  }
  
  
  
  /**
   *  create the default entries
   */
  
  protected void createDefaults ()
  {
    setDefaultClass (Response.class);

    addXmlMapping (NS_CONTACT, "infoData", ContactInfoResponse.class);
    addXmlMapping (NS_CONTACT, "checkData", ContactCheckResponse.class);
    addXmlMapping (NS_DOMAIN, "infoData", DomainInfoResponse.class);
    addXmlMapping (NS_DOMAIN, "checkData", DomainCheckResponse.class);
    addXmlMapping (NS_MESSAGE, "message", QueueReadResponse.class);
    
    addRule (new ClassRule (1, ContactCheckResponse.class,
      ContactCheckRequest.class, true));

    addRule (new ClassRule (1, ContactCreateResponse.class,
      ContactCreateRequest.class, true));

    addRule (new ClassRule (1, ContactInfoResponse.class,
      ContactInfoRequest.class, true));

    addRule (new ClassRule (1, ContactUpdateResponse.class,
      ContactUpdateRequest.class, true));

    addRule (new ClassRule (1, DomainChangeHolderResponse.class,
      DomainChangeHolderRequest.class, true));

    addRule (new ClassRule (1, DomainChangeProvResponse.class,
      DomainChangeProvRequest.class, true));

    addRule (new ClassRule (1, DomainCheckResponse.class,
      DomainCheckRequest.class, true));

    addRule (new ClassRule (1, DomainCreateResponse.class,
      DomainCreateRequest.class, true));

    addRule (new ClassRule (1, DomainDeleteResponse.class,
      DomainDeleteRequest.class, true));

    addRule (new ClassRule (1, DomainInfoResponse.class,
      DomainInfoRequest.class, true));

    addRule (new ClassRule (1, DomainTransitResponse.class,
      DomainTransitRequest.class, true));

    addRule (new ClassRule (1, DomainUpdateResponse.class,
      DomainUpdateRequest.class, true));

    addRule (new ClassRule (1, DomainMigrateDescrResponse.class,
      DomainMigrateDescrRequest.class, true));
    
    addRule (new ClassRule (1, LoginResponse.class,
      LoginRequest.class, true));

    addRule (new ClassRule (1, LogoutResponse.class,
      LogoutRequest.class, true));

    addRule (new ClassRule (1, QueueDeleteResponse.class,
      QueueDeleteRequest.class, true));

    addRule (new ClassRule (1, QueueReadResponse.class,
      QueueReadRequest.class, true));

    addRule (new ClassRule (1, AuthInfo1CreateResponse.class,
      AuthInfo1CreateRequest.class, true));

    addRule (new ClassRule (1, AuthInfo1DeleteResponse.class,
      AuthInfo1DeleteRequest.class, true));

    addRule (new ClassRule (1, AuthInfo2CreateResponse.class,
      AuthInfo2CreateRequest.class, true));

    addRule (new KeyRule (9, QueueReadResponse.class, "msgcnt"));
    addRule (new KeyRule (10, ContactCheckResponse.class, "handle", "status"));
    addRule (new KeyRule (11, ContactInfoResponse.class, "handle"));

    addRule (new KeyRule (10, DomainInfoResponse.class, "domain", "changed"));
    addRule (new KeyRule (11, DomainCheckResponse.class, "domain"));
  }
  
  
  
  
  /**
   *  return the default factory
   *
   *  @return                         the factory
   */
  
  public static ResponseFactory getFactory ()
  {
    return singleton;
  }
  
  
  /**
   *  create an instance of the given class, mapping the special exceptions
   *  to RuntimeExceptions
   *
   *  @param cls                      the class to instantiate
   *  @return                         the instance
   */
  
  protected Response createInstance (Class<? extends Response> cls)
  {
    try
    {
      return cls.newInstance ();
      
    } catch (InstantiationException e)
    {
      throw new RuntimeException ("failed to instantiate " + cls + 
        ", check implementation!", e);
    
    } catch (IllegalAccessException e)
    {
      throw new RuntimeException ("failed to instantiate " + cls + 
        ", check implementation!", e);
    }
  }  
  
  
  //--- implementation of ResponseFactory --------------------------------------
  
  
  /**
   *  create an instance based on the given XML document. Since the
   *  content of the list is not necessarily self-describing in respect
   *  to the type of response, the request can be supplied to ease the
   *  selection of the correct subclass.
   *
   *  @param doc                      the document
   *  @param request                  the request, may be {@code null}
   *  @return                         the new instance
   *  @throws DOMException            on DOM exceptions
   *  @throws ResponseParseException  if the type could not be determined
   */

  @Override
  public Response createResponse (Document doc, Request request)
    throws DOMException, ResponseParseException
  {
    Element el = getFirstElement (doc, NS_GLOBAL, "registry-response");
    if (el != null) el = getFirstElement (el, NS_TXN, "transaction");
    
    if (el == null) 
      throw new ResponseParseException ("not a valid response");
    
    Class<? extends Response> cls = null;
    el = getFirstElement (el, NS_TXN, "data");
    
    if (el != null)
    {
      Element el2 = getFirstElement (el);
      
      if (el2 == null) 
        throw new ResponseParseException ("unexpected empty data element", el);
      
      cls = xmlMap.get (
        new QName (el2.getNamespaceURI (), el2.getLocalName ()));
    }
    
    if (cls == null)
    {
      cls = defaultClass;
      
      for (ListRule rule: rules)
      {
        if (rule.matches (doc, request))
        {
          cls = rule.getResponseClass ();
          break;
        }
      }
    }
    
    return createInstance (cls);
  }
  
  
  /**
   *  create an instance based on the given key-value pair list. Since the
   *  content of the list is not necessarily self-describing in respect
   *  to the type of response, the request can be supplied to ease the
   *  selection of the correct subclass.
   *
   *  @param list                     the list
   *  @param request                  the request, may be {@code null}
   *  @return                         the created response
   *  @throws ResponseParseException  if the type could not be determined
   */

  @Override
  public Response createResponse (KVMultiList list, Request request)
    throws ResponseParseException
  {
    Class<? extends Response> cls = defaultClass;
    
    for (ListRule rule: rules)
    {
      if (rule.matches (list, request))
      {
        cls = rule.getResponseClass ();
        break;
      }
    }

    return createInstance (cls);
  }
  
  
  
  
  /**
   *  small class that describes a rule for choosing a specific subclass
   *
   *  @version  $Revision: 14936 $
   *  @author   Klaus Malorny
   */
  
  protected abstract static class ListRule implements Comparable<ListRule>
  {
    /** the priority */
    
    private int priority;
    
    
    /** the class that shall be instantiated */
    
    private Class<? extends Response> cls;
    
    
    
    /**
     *  constructor
     *
     *  @param thePriority         the priority. The smaller the number, the
     *                             earlier the rule is used.
     *  @param theClass            the class that shall be instantiated
     */
    
    public ListRule (int thePriority, Class<? extends Response> theClass)
    {
      priority = thePriority;
      cls = theClass;
    }
    
    
    
    /**
     *  test whether the rule matches
     *
     *  @param list                the list
     *  @param request             the request, may be {@code null}
     *  @return                    {@code true} if the rule matches
     */
    
    public abstract boolean matches (KVMultiList list, Request request);
    
    
    
    /**
     *  test whether the rule matches
     *
     *  @param doc                 the document
     *  @param request             the request, may be {@code null}
     *  @return                    {@code true} if the rule matches
     */
    
    public abstract boolean matches (Document doc, Request request);
    
    
    
    
    /**
     *  return the class
     *
     *  @return                    the class
     */
    
    public Class<? extends Response> getResponseClass ()
    {
      return cls;
    }
    
    
    /**
     *  compare this instance to another
     *
     *  @param other               the other object
     *  @return                    the result of the comparison
     */

    @Override
    public int compareTo (ListRule other)
    {
      if (this == other) return 0;
      if (priority != other.priority) return priority < other.priority ? -1 : 1;
      
      // both objects have the same priority. Returning 0 would knock out
      // one of the set. Make them different
      
      return hashCode () < other.hashCode () ? -1 : 1;
    }
  }
  

  
  /**
   *  rule that tests for the existance of certain keys
   *
   *  @version  $Revision: 14936 $
   *  @author   Klaus Malorny
   */
  
  protected static class KeyRule extends ListRule
  {
    /** the keys that must appear in the list */
    
    private String[] requiredKeys;
    
    
    /**
     *  constructor
     *
     *  @param thePriority         the priority. The smaller the number, the
     *                             earlier the rule is used.
     *  @param theClass            the class that shall be instantiated
     *  @param keys                the keys
     */

    public KeyRule (int thePriority, Class<? extends Response> theClass,
      String... keys)
    {
      super (thePriority, theClass);
      requiredKeys = keys;
    }
    
    /**
     *  test whether the rule matches
     *
     *  @param list                the list
     *  @param request             the request, may be {@code null}
     *  @return                    {@code true} if the rule matches
     */

    @Override
    public boolean matches (KVMultiList list, Request request)
    {
      KVList main = list.getMainSection ();
      
      for (String key: requiredKeys)
        if (!main.containsKey (key)) return false;

      return true;
    }
    
    
    /**
     *  test whether the rule matches
     *
     *  @param doc                 the document
     *  @param request             the request, may be {@code null}
     *  @return                    {@code true} if the rule matches
     */

    @Override
    public boolean matches (Document doc, Request request)
    {
      return false;
    }
  }

  
  /**
   *  rule that uses the request for the selection of the suitable response
   *
   *  @version  $Revision: 14936 $
   *  @author   Klaus Malorny
   */
  
  protected class ClassRule extends ListRule
  {
    /** the class that the request must match */
    
    private Class<? extends Request> matchClass;
    
    
    /** flag whether subclasses of the classes also match */
    
    private boolean withSubclasses;
    
    
    /**
     *  constructor
     *
     *  @param thePriority         the priority. The smaller the number, the
     *                             earlier the rule is used.
     *  @param theClass            the class that shall be instantiated
     *  @param theMatchClass       the class that the request must match
     *  @param withSub             whether subclasses also match
     */

    public ClassRule (int thePriority, Class<? extends Response> theClass,
      Class<? extends Request> theMatchClass, boolean withSub)
    {
      super (thePriority, theClass);
      matchClass = theMatchClass;
      withSubclasses = withSub;
    }
    
    
    /**
     *  test whether the rule matches
     *
     *  @param request             the request, may be {@code null}
     *  @return                    {@code true} if the rule matches
     */

    private boolean matches (Request request)
    {
      if (request == null) return false;
      Class<? extends Request> reqClass = request.getClass ();
      
      if (withSubclasses)
        return matchClass.isAssignableFrom (reqClass);
       else
        return matchClass.equals (reqClass);
    }
    
    
    
    /**
     *  test whether the rule matches
     *
     *  @param list                the list
     *  @param request             the request, may be {@code null}
     *  @return                    {@code true} if the rule matches
     */

    @Override
    public boolean matches (KVMultiList list, Request request)
    {
      return matches (request);
    }
    
    
    /**
     *  test whether the rule matches
     *
     *  @param doc                 the document
     *  @param request             the request, may be {@code null}
     *  @return                    {@code true} if the rule matches
     */

    @Override
    public boolean matches (Document doc, Request request)
    {
      return matches (request);
    }
    
  }
  
}
