//______________________________________________________________________________
//   |       |
//   | 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.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

import java.nio.ByteBuffer;
import java.nio.CharBuffer;

import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CodingErrorAction;

import java.util.Iterator;

import java.util.regex.Pattern;
import java.util.regex.Matcher;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;

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

import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSOutput;
import org.w3c.dom.ls.LSSerializer;
import org.w3c.dom.ls.LSException;

import org.xml.sax.SAXException;

import de.knipp.rritoolkit.request.Request;
import de.knipp.rritoolkit.response.Response;

/**
 *  Class that performs the conversion to and from byte data, as required
 *  for the exchange with the registry server. The class is thread-safe.
 *  Various methods are exposed that represent various steps in the conversion,
 *  merely to allow investigation of the intermediate data in case of problems.
 *
 *  @version  $Revision: 14936 $
 *  @author   Klaus Malorny
 */

public class Codec
{
  /** the response factory we are using */
  
  protected ResponseFactory factory;
  
  
  /** whether requests/responses shall be encoded/decoded to/from XML */
  
  protected boolean useXml;
  
  
  /** the document builder we are using */
  
  protected ThreadLocal<DocumentBuilder> builder;
  
  
  /** pattern to split a key-value list into lines */
  
  private static final Pattern linePattern = Pattern.compile ("\\r?\\n");

  
  /** pattern to decode a key-value pair */
  
  private static final Pattern kvPattern = 
    Pattern.compile ("([\\w\\-]+):\\s*(.*)");
  
  /** pattern to decode section names line */
  
  private static final Pattern sectionsPattern =
    Pattern.compile ("(\\[[\\w\\-\\s]+\\])+");
  
  
  /** pattern to decode an individual section name */
  
  private static final Pattern sectionPattern = 
    Pattern.compile ("\\[([\\w\\-\\s]+)\\]");
  
  
  /** the UTF-8 character set */
  
  private static final Charset utf8Charset = Charset.forName ("UTF-8");
  
  
  
  /**
   *  constructor
   *
   *  @param theFactory              the response factory to use
   *  @param xml                     whether to operate with XML 
   *                                 ({@code true}) or key-value pairs
   *                                 ({@code false})
   */
  
  public Codec (ResponseFactory theFactory, boolean xml)
  {
    factory = theFactory;
    useXml = xml;
    builder = new ThreadLocal<DocumentBuilder> ();
  }
  
  
  /**
   *  default constructor. Uses the default factory and XML.
   */
  
  public Codec ()
  {
    this (DefaultResponseFactory.getFactory (), true);
  }

  
  
  /**
   *  convert the given request to a byte sequence
   *
   *  @param req                the request
   *  @return                   the byte data
   *  @throws EncodeException   if the encoding process failed
   */
  
  public byte[] encodeRequest (Request req)
    throws EncodeException
  {
    byte[] data;
    
    if (useXml)
    {
      try
      {
        Document doc = getBuilder ().newDocument ();
        req.toXml (doc);
        data = docToBytes (doc);
        
      } catch (DOMException e)
      {
        throw new EncodeException ("failed to encode", e);
      }
    } else
    {
      KVList list = new KVList ();
      req.toList (list);
      String text = listToString (list);
      data = stringToBytes (text);
    }
    
    return data;
  }
  
  
  
  
  /**
   *  convert the given byte sequence to a response object. Please note that
   *  it is recommended to specify the request when using the key-value
   *  encoding, otherwise it may happen that the class of the returned instance
   *  varies depending on the response, since the response gives not enough
   *  information of the type of the related request in all cases.
   *
   *  @param data                     the data
   *  @param req                      the request, may be {@code null}
   *  @return                         the response
   *  @throws ResponseParseException  if problems occurred during the parsing
   *                                  of the data
   */
  
  public Response decodeResponse (byte[] data, Request req)
    throws ResponseParseException
  {
    if (useXml)
    {
      Document doc = bytesToDoc (data);
      return docToResponse (doc, req);

    } else
    {
      try
      {
        String text = bytesToString (data);
        KVMultiList list = stringToList (text);
        return listToResponse (list, req);
        
      } catch (IOException e)
      {
        throw new ResponseParseException ("parsing failed", e);
      }
    }
  }
  
  
  
  /**
   *  return whether XML is used for encoding
   *
   *  @return                 {@code true} if so
   */
  
  public boolean xmlUsed ()
  {
    return useXml;
  }
  
  
  //---- helper methods --------------------------------------------------------

  
  /**
   *  convert the given request to a list
   *
   *  @param req                the request
   *  @return                   the list
   *  @throws EncodeException   if the encoding process failed
   */
  
  public KVList requestToList (Request req)
    throws EncodeException
  {
    KVList list = new KVList ();
    req.toList (list);
    return list;
  }
  
  
  /**
   *  add a key-value list to the given string builder
   *
   *  @param list                     the list to add
   *  @param sb                       the string builder to use
   */
  
  protected void addList (KVList list, StringBuilder sb)
  {
    for (KVList.Pair pair: list)
    {
      String key = pair.getKey ();
      
      for (String value: pair.getValue ())
      {
        sb.append (key);
        sb.append (": ");
        sb.append (value);
        sb.append ("\r\n");
      }
    }
  }
  
  

  /**
   *  convert a key-value list to a string
   * 
   *  @param list                      the list
   *  @return                          the list as a string
   */
  
  public String listToString (KVList list)
  {
    StringBuilder sb = new StringBuilder ();
    addList (list, sb);
    return sb.toString ();
  }
  
  
  /**
   *  convert a key-value multilist to a string. Although not required for
   *  the operation, this is provided for convenience
   *
   *  @param list                       the list
   *  @return                           the list as a string
   */
  
  public String listToString (KVMultiList list)
  {
    StringBuilder sb = new StringBuilder ();
    addList (list.getMainSection (), sb);

    Iterator<KVList> i = list.getSubsections ();
    
    while (i.hasNext ())
    {
      KVList section = i.next ();
      
      for (String name: list.getSectionNames (section))
      {
        sb.append ('[');
        sb.append (name);
        sb.append (']');
      }
      
      sb.append ("\r\n");
      addList (section, sb);
    }
    
    return sb.toString ();
  }
  

  
  
  /**
   *  return the given string in the UTF-8 representation
   *
   *  @param text                  the string
   *  @return                      the byte array
   */
  
  public byte[] stringToBytes (String text)
  {
    ByteBuffer buf = utf8Charset.encode (text);
    byte[] data = new byte[buf.limit ()];
    buf.get (data);
    return data;
  }
  
  

  
  /**
   *  convert the list to the response instance
   *
   *  @param list                      the list
   *  @param request                   the request, may be {@code null}
   *                                   if not available
   *  @return                          the response
   *  @throws ResponseParseException   if the response could not be created
   *                                   based on the input
   */
  
  public Response listToResponse (KVMultiList list, Request request)
    throws ResponseParseException
  {
    Response response = factory.createResponse (list, request);
    response.fromList (list);
    
    return response;
  }
  
  
  
  
  
  
  /**
   *  parse the given string into a key-value list
   *
   *  @param text                      the text to parse
   *  @return                          the parsed key-value list
   *  @throws ResponseParseException   if the parsing failed
   */
  
  public KVMultiList stringToList (String text)
    throws ResponseParseException
  {
    int lineCount = 0;
    KVMultiList list = new KVMultiList ();
    KVList current = list.getMainSection ();
    
    Matcher kvMatcher = null;
    Matcher sectionsMatcher = null;
    Matcher sectionMatcher = null;    

    for (String line: linePattern.split (text))
    {
      lineCount++;
      line = line.trim ();
      if (line.length () == 0) continue;  // ignore empty lines
      
      if (kvMatcher == null) 
        kvMatcher = kvPattern.matcher (line);
       else
        kvMatcher.reset (line);
      
      if (kvMatcher.matches ())
      {
        current.add (kvMatcher.group (1).toLowerCase (), kvMatcher.group (2));

      } else
      {
        if (sectionsMatcher == null)
          sectionsMatcher = sectionsPattern.matcher (line);
         else
          sectionsMatcher.reset (line);
        
        if (sectionsMatcher.matches ())
        {
          if (sectionMatcher == null)
            sectionMatcher = sectionPattern.matcher (line);
           else
            sectionMatcher.reset (line);
          
          current = new KVList ();
          int pos = 0;
          
          while (sectionMatcher.find (pos))
          {
            list.addSection (current, sectionMatcher.group (1).toLowerCase ());
            pos = sectionMatcher.end ();
          }
        } else
        {
          throw new ResponseParseException ("invalid content in line " +
            lineCount + ": \"" + line + "\"");
        }
      }
    }

    return list;
  }
  
  
  /**
   *  interpret the given byte array as a sequence of UTF-8 encoded characters
   *  and return it as a string. Contrary to the related String constructor,
   *  malformed input is detected and reported.
   *
   *  @param data                      the data
   *  @return                          the converted string
   *  @throws IOException              if the conversion failed
   */
  
  public String bytesToString (byte[] data)
    throws IOException
  {
    CharsetDecoder dec = utf8Charset.newDecoder ();
    dec.onMalformedInput (CodingErrorAction.REPORT);
    dec.onUnmappableCharacter (CodingErrorAction.REPORT);

    CharBuffer buf = dec.decode (ByteBuffer.wrap (data));
    return buf.toString ();
  }

  
  
  
  /**
   *  return the document builder that shall be used for parsing and
   *  generating XML
   *
   *  @return                         the document builder
   */
  
  protected DocumentBuilder getBuilder ()
  {
    DocumentBuilder b = builder.get ();
    if (b != null) return b;
    
    try
    {
      DocumentBuilderFactory f = DocumentBuilderFactory.newInstance ();
      f.setCoalescing (true);
      f.setNamespaceAware (true);
      f.setValidating (false);
      b = f.newDocumentBuilder ();
    
    } catch (ParserConfigurationException e)
    {
      throw new RuntimeException ("failed to create suitable parser", e);
    }
      
    builder.set (b);
    
    return b;
  }
  
  
  
  /**
   *  convert the request to an XML document
   *
   *  @param req                   the request
   *  @return                      the document
   *  @throws EncodeException      if the encoding failed
   */
  
  public Document requestToDoc (Request req)
    throws EncodeException
  {
    try
    {
      Document doc = getBuilder ().newDocument ();
      req.toXml (doc);
      return doc;
      
    } catch (DOMException e)
    { 
      throw new EncodeException ("failed to encode request", e);
    }
  }
  
  
  
  
  /**
   *  convert the DOM document to a sequence of bytes
   *
   *  @param doc                      the document to convert
   *  @return                         the data
   *  @throws EncodeException         if the serialization failed
   */
  
  public byte[] docToBytes (Document doc)
    throws EncodeException
  {
    DOMImplementation impl = doc.getImplementation ();
    
    DOMImplementationLS lsImpl = 
      (DOMImplementationLS) impl.getFeature ("LS", "3.0");
    
    if (lsImpl == null) 
      throw new IllegalArgumentException ("the DOM implementation does " +
        "not support DOM level 3 load/store");

    ByteArrayOutputStream os = new ByteArrayOutputStream ();
    LSOutput output = lsImpl.createLSOutput ();
    output.setEncoding ("UTF-8");
    output.setByteStream (os);
    LSSerializer ser = lsImpl.createLSSerializer ();
    
    try
    {
      if (!ser.write (doc, output))
        throw new EncodeException ("serialization failed");

    } catch (LSException e)
    { 
      throw new EncodeException ("serialization failed", e);
    }
    
    return os.toByteArray ();
  }
  
  
  /**
   *  convert the document to a response
   *
   *  @param doc                      the document
   *  @param req                      the request
   *  @return                         the response
   *  @throws ResponseParseException  if the parsing failed
   */
  
  public Response docToResponse (Document doc, Request req)
    throws ResponseParseException
  {
    try
    {
      Response resp = factory.createResponse (doc, req);
      resp.fromXml (doc);
      return resp;
      
    } catch (DOMException e)
    {
      throw new ResponseParseException ("failed to parse response", e);
    }
  }
  
  
  
  
  
  /**
   *  convert the byte array to an DOM document
   *
   *  @param data                     the data
   *  @return                         the document
   *  @throws ResponseParseException  if the parsing failed
   */
  
  public Document bytesToDoc (byte[] data)
    throws ResponseParseException
  {
    DocumentBuilder b = getBuilder ();
    
    try
    {
      return b.parse (new ByteArrayInputStream (data));
      
    } catch (SAXException e)
    {
      throw new ResponseParseException ("failed to parse XML document", e);

    } catch (IOException e)
    {
      throw new RuntimeException ("unexpected exception", e);
    }
  }
  
  
}


