//______________________________________________________________________________
//   |       |
//   | 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.ByteArrayOutputStream;

import java.util.Date;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Formatter;
import java.util.TimeZone;

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

import java.io.UnsupportedEncodingException;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;



/**
 *  class with some utilities that do not fit elsewhere
 *
 *  @version  $Revision: 14936 $
 *  @author   Klaus Malorny
 */

public class Utils
{

    
  /** the date pattern */
    
  private static final Pattern datePattern = Pattern.compile (
    "(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})" +
    "([+\\-])(\\d{2}):(\\d{2})");


  /** the time zone for date conversions */

  private static final TimeZone denicTimeZone =
    TimeZone.getTimeZone ("Europe/Berlin");


  /** hex digits */

  private static final char[] hexDigits =
    { '0', '1', '2', '3', '4', '5', '6', '7',
      '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
    };


  /**
   *  private constructor to prevent instantiation
   */

  private Utils ()
  {
    // empty
  }

  
  /**
   *  helper method to parse a date. As a convience, {@code null} is
   *  accepted and will result in a {@code null} date
   *
   *  @param text                     the date in text representation,
   *                                  may be {@code null}
   *  @return                         the parsed date, or {@code null}
   *                                  if the given text is {@code null}
   *  @throws ResponseParseException  if the input is malformed
   */
  
  public static Date parseDateTime (String text)
    throws ResponseParseException
  {
    if (text == null) return null;
    Matcher m = datePattern.matcher (text);
    
    if (!m.matches ()) 
      throw new ResponseParseException ("unable to parse date");
    
    Calendar cal = new GregorianCalendar ();
    cal.clear ();
    
    cal.set (Integer.parseInt (m.group (1)), Integer.parseInt (m.group (2)) - 1,
      Integer.parseInt (m.group (3)), Integer.parseInt (m.group (4)), 
      Integer.parseInt (m.group (5)), Integer.parseInt (m.group (6)));
    
    int offset = Integer.parseInt (m.group (8)) * 3600000 + 
      Integer.parseInt (m.group (9)) * 60000;
    
    if (m.group (7).equals ("-")) offset = -offset;
    cal.set (Calendar.ZONE_OFFSET, offset);
    cal.set (Calendar.DST_OFFSET, 0);

    return cal.getTime ();
  }

  
  /**
   *  format the given date as a string, conforming to the DENIC specs. As
   *  a convenience, a {@code null} date results in a {@code null}.
   *
   *  @param date                 the date to convert
   *  @return                     the formatted date
   */
  
  public static String formatDateTime (Date date)
  {
    if (date == null) return null;
    Calendar cal = new GregorianCalendar ();
    cal.setTime (date);
    
    int offset = cal.get (Calendar.ZONE_OFFSET) + cal.get (Calendar.DST_OFFSET);
    char sign;
    
    if (offset < 0)
    {
      sign = '-';
      offset = -offset;
    
    } else
    {
      sign = '+';
    }
    
    int offsetHour = offset / 3600000;
    int offsetMinute = (offset / 60000) % 60;
    
    Formatter f = new Formatter ();
    
    return f.format ("%04d-%02d-%02dT%02d:%02d:%02d%c%02d:%02d",
      cal.get (Calendar.YEAR), cal.get (Calendar.MONTH) + 1,
      cal.get (Calendar.DAY_OF_MONTH), cal.get (Calendar.HOUR_OF_DAY),
      cal.get (Calendar.MINUTE), cal.get (Calendar.SECOND), sign,
      offsetHour, offsetMinute).toString ();
  }


  /**
   *  format the given date as 8 digit year/month/day, as required for
   *  the "create AuthInfo 1" operation.
   *
   *  @param date                the date to convert, may be {@code null}
   *  @return                    the date, formatted as {@code YYYYMMDD}, or
   *                             {@code null} if given parameter is also
   *                             {@code null}
   */

  public static String formatDate (Date date)
  {
    if (date == null) return null;
    
    Calendar cal = new GregorianCalendar ();
    cal.setTimeZone (denicTimeZone);
    cal.setTime (date);

    Formatter f = new Formatter ();

    return f.format ("%04d%02d%02d", cal.get (Calendar.YEAR),
      cal.get (Calendar.MONTH) + 1, cal.get (Calendar.DAY_OF_MONTH)).
      toString ();
  }



  /**
   *  convert the given byte sequence to a hex string
   *
   *  @param data                  the binary data
   *  @return                      the hex string
   */

  public static String toHex (byte[] data)
  {
    int length = data.length;
    char[] buffer = new char[length * 2];
    int i = 0;

    for (byte value: data)
    {
      buffer[i++] = hexDigits[value >> 4 & 0xF];
      buffer[i++] = hexDigits[value & 0xF];
    }

    return new String (buffer);
  }



  /**
   *  convert a string conforming to the XML Schema base64Binary syntax to
   *  a sequence of bytes
   *
   *  @param text                        the textual hexadecimal representation,
   *                                     may be {@code null}
   *  @return                            array containing the bytes or
   *                                     {@code null} if the given text is
   *                                     also {@code null}
   *  @throws IllegalArgumentException   if the string does not conform to the
   *                                     Base64 encoding
   */

  public static byte[] fromBase64Binary (String text)
  {
    if (text == null) return null;

    ByteArrayOutputStream bos = new ByteArrayOutputStream ();
    int pos = 0;
    int accumulator = 0;
    boolean end = false;

    for (char ch: text.toCharArray ())
    {
      if (Character.isWhitespace (ch)) continue;

      if (ch == '=')
      {
        if (pos < 2)
          throw new IllegalArgumentException (
            "Base64 end marker (\"=\") found at illegal position in \"" +
            text + "\"");

        if (accumulator != 0)
          throw new IllegalArgumentException (
            "last digit contains superfluous data in \"" + text + "\"");

        end = true;

      } else
      {
        if (end)
          throw new IllegalArgumentException ("invalid character \"" + ch +
            "\" after end marker (\"=\") found in \"" + text + "\"");

        int digit;

        if (ch >= 'A' && ch <= 'Z')
          digit = ch - 'A';
        else if (ch >= 'a' && ch <= 'z')
          digit = ch - 'a' + 26;
        else if (ch >= '0' && ch <= '9')
          digit = ch - '0' + 52;
        else if (ch == '+')
          digit = 62;
        else if (ch == '/')
          digit = 63;
        else
          throw new IllegalArgumentException ("invalid Base64 digit \"" + ch +
            "\" found in \"" + text + "\"");

        accumulator = accumulator << 6 | digit;

        switch (pos)
        {
          case 0:
            break;

          case 1:
            bos.write (accumulator >> 4);
            accumulator &= 0xF;
            break;

          case 2:
            bos.write (accumulator >> 2);
            accumulator &= 0x3;
            break;

          case 3:
            bos.write (accumulator);
            accumulator = 0;
            break;
        }
      }

      pos = (pos + 1) & 3;
    }

    if (pos != 0)
      throw new IllegalArgumentException ("unexpected end of data found in \"" +
        text + "\"");

    return bos.toByteArray ();
  }


  /**
   *  convert a byte sequence to a string conforming to the XML Schema
   *  base64Binary syntax
   *
   *  @param data                    the binary data, may be {@code null}
   *  @return                        the hexadecimal encoded data or
   *                                 {@code null} if the given data is
   *                                 also {@code null}
   */

  public static String toBase64Binary (byte[] data)
  {
    if (data == null) return null;

    int groups = (data.length + 2) / 3;
    int lastBits = (data.length % 3) << 3;
    if (lastBits == 0) lastBits = 24;
    char[] text = new char[groups * 4];
    int ipos = 0;
    int opos = 0;

    for (int i = 1; i <= groups; i++, ipos += 3)
    {
      int bits = i < groups ? 24 : lastBits;
      int value = (data[ipos] & 0xFF) << 16;
      if (bits > 8) value |= (data[ipos + 1] & 0xFF) << 8;
      if (bits > 16) value |= data[ipos + 2] & 0xFF;

      while (bits > 0)
      {
        int digit = value >> 18 & 0x3F;
        int ch;

        if (digit < 26)
          ch = 'A' + digit;
         else if (digit < 52)
          ch = 'a' + digit - 26;
         else if (digit < 62)
          ch = '0' + digit - 52;
         else if (digit == 62)
          ch = '+';
         else
          ch = '/';

        text[opos++] = (char) ch;
        value <<= 6;
        bits -= 6;
      }
    }

    while (opos < text.length) text[opos++] = '=';

    return new String (text);
  }




  /**
   *  method that calculates the SHA-256 hash from a given authInfo
   *
   *  @param authInfo                  the AuthInfo
   *  @return                          the binary hash
   */

  public static byte[] calcAuthInfoHash (String authInfo)
  {
    byte[] encoded;

    try
    {
      encoded = authInfo.getBytes ("UTF-8");

    } catch (UnsupportedEncodingException e)
    {
      throw new RuntimeException ("unexpected exception, UTF-8 should " +
        "be supported on every Java platform", e);
    }

    MessageDigest md;

    try
    {
      md = MessageDigest.getInstance ("SHA-256");

    } catch (NoSuchAlgorithmException e)
    {
      throw new RuntimeException ("unexpected exception, SHA-256 should be " +
        "supported on every Java platform", e);
    }

    return md.digest (encoded);
  }

}
