//
// LDASTools AL - A library collection to provide an abstraction layer
//
// Copyright (C) 2018 California Institute of Technology
//
// LDASTools AL is free software; you may redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 (GPLv2) of the
// License or at your discretion, any later version.
//
// LDASTools AL is distributed in the hope that it will be useful, but
// without any warranty or even the implied warranty of merchantability
// or fitness for a particular purpose. See the GNU General Public
// License (GPLv2) for more details.
//
// Neither the names of the California Institute of Technology (Caltech),
// The Massachusetts Institute of Technology (M.I.T), The Laser
// Interferometer Gravitational-Wave Observatory (LIGO), nor the names
// of its contributors may be used to endorse or promote products derived
// from this software without specific prior written permission.
//
// You should have received a copy of the licensing terms for this
// software included in the file LICENSE located in the top-level
// directory of this package. If you did not, you can view a copy at
// http://dcc.ligo.org/M1500244/LICENSE
//

#include <ldastoolsal_config.h>

#include "base64.hh"

#include <cstring>
#include <ctype.h>
#include <iostream>
#include <stdexcept>

using std::strlen;

//-----------------------------------------------------------------------------
/// An array containing the Base64 characters in order.
//-----------------------------------------------------------------------------
const char* Base64::mTable(
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" );

//-----------------------------------------------------------------------------
/// Default is 76 (based on MIME).
//-----------------------------------------------------------------------------
int Base64::mCharsPerLine( 76 );

//-----------------------------------------------------------------------
/// \param[out] Destination
///        The buffer to hold the base 64 encoded result.
/// \param[in] Source
///        The buffer holding the 8-bit data to encode.
///
/// \result
///        The number of bytes written to \a Destination.
//-----------------------------------------------------------------------
size_t
Base64::encode( std::string& Destination, const bit_vector& Source )
{
    Destination = "";
    unsigned char mask( 1 << 5 );
    int           cnt = 0;
    int           num = 0;
    std::string   digit( " " );

    for ( bit_vector::const_iterator bit = Source.begin( );
          bit != Source.end( ); )
    {
        if ( *bit )
            num |= mask; // Set if bit set
        bit++;
        cnt++;
        mask >>= 1;
        if ( ( cnt == 6 ) || ( bit == Source.end( ) ) )
        {
            digit[ 0 ] = mTable[ num ]; // Setup
            Destination.append( digit ); // Write out number
            num = 0; // Rest the number
            mask = 1 << 5; // Reset the mask
            cnt = 0; // Reset count;
        }
    }
    return Destination.length( ); // Return length of std::string
}

//-----------------------------------------------------------------------------
/// \brief  Encodes Base64
///
/// This method translates a block of binary data into Base-64 encoded data.
/// The encoded data is stored in the memory location provided by the user.
/// This destination buffer must be large enough to store the result.  The user
/// may calculate what the size needs to be via the calculateEncodedSize method
/// (be sure to add 1 for the null character).  This method appends a null
/// character to the end of the encoded string.
///
/// \param destination The buffer in which to store the encoded data.
/// \param source The buffer to encode.
/// \param size The number of bytes in the buffer.
///
/// \return The length of the destination string (not including
///         the terminating null).
//-----------------------------------------------------------------------
size_t
Base64::encode( char* destination, const void* source, size_t size )
{
    int column( 0 ); // The column which is currently being written (this is
                     // always a multiple of four.
    int qnum( 3 ); // The byte number in the quantum currently being
                   // processed (a quantum is a group of 3 bytes which is
                   // being converted to the 4 byte Base-64 representation).
                   // The byte numbers run DOWN from 2->0.  This starts with
                   // 3 but it will be decremented in the 'switch'.

    char* pDest( destination ); // The current character location to write

    // The current source byte being converted
    const unsigned char* pSource(
        reinterpret_cast< const unsigned char* >( source ) );

    unsigned char carry = 0; // The carry-over from the last write (since only
                             // 6 bits are written each time, this stores the
                             // currently unused bits).
    unsigned char c; // The current byte being processed.

    // Just loop until there are no more bytes
    while ( size != 0 )
    {
        // Get the current byte
        c = *( pSource++ );

        // Based upon which member of the quantum this is, perform the
        // appropriate conversion.
        switch ( --qnum )
        {
        // 'c' is the 3rd byte
        case 0:
            *( pDest++ ) = mTable[ ( c >> 6 ) | carry ];
            *( pDest++ ) = mTable[ c & 63 ];

            column += 4;
            if ( column == mCharsPerLine )
            {
                *( pDest++ ) = '\n';
                column = 0;
            }

            break;

        // 'c' is the 2nd byte
        case 1:
            *( pDest++ ) = mTable[ ( c >> 4 ) | carry ];
            carry = ( c << 2 ) & 63;
            break;

        // 'c' is the 1st byte
        case 2:
            *( pDest++ ) = mTable[ c >> 2 ];
            carry = ( c << 4 ) & 63;
            break;
        }

        // If this is the end of the quantum, start over.
        if ( qnum == 0 )
            qnum = 3;

        --size;
    }

    // If we are in the middle of a quantum, take care of what is left over in
    // the carry bit and pad with '=' character.
    switch ( qnum )
    {
    case 1:
        *( pDest++ ) = mTable[ carry ];
        *( pDest++ ) = '=';
        break;

    case 2:
        *( pDest++ ) = mTable[ carry ];
        *( pDest++ ) = '=';
        *( pDest++ ) = '=';
        break;
    }

    // Append the null
    *pDest = '\0';

    return pDest - destination;
}

//-----------------------------------------------------------------------
/// Decode base64 data back into a bit_vector
///
/// \param Destination Storage space for decoded data
/// \param Source Storage space for the encoded data
///
/// \return size_t Number of bytes in the Destination buffer.
//-----------------------------------------------------------------------
size_t
Base64::decode( bit_vector& Destination, const std::string& Source )
{
    // Resize the bit array to fit the data that will be put in
    Destination.resize( Source.length( ) * 6 );
    bit_vector::iterator bit( Destination.begin( ) );
    unsigned char        c;
    const char*          ptr( Source.c_str( ) );

    while ( getChar( &ptr, c ) )
    {
        unsigned char mask( 1 << 5 );

        for ( size_t x = 0; x < 6; x++ )
        {
            ( *bit ) = ( ( c & mask ) > 0 );
            bit++;
            mask >>= 1;
        }
    }
    return Destination.size( );
}

//-----------------------------------------------------------------------------
/// This translates a Base-64 encoded string into binary data.
///
/// \param destination
///        The location to write the decoded data.  This must be large
///        enough to store the data.  The required size can
///        be calculated via the 'calculateDecodedSize' method.
/// \param source
///        Base64 data.  Decoding will stop when the first
///        non-Base64 character, an '=', or a null terminator occurs.
///
/// \return The number of bytes decoded.
//-----------------------------------------------------------------------
size_t
Base64::decode( void* destination, const char* source )
{
    // The current byte being written
    unsigned char* pDest( reinterpret_cast< unsigned char* >( destination ) );

    unsigned char carry = 0; // The carry-over, since we are reading the
                             // equivalent of 6 bits at a time.

    int qnum( 4 ); // The byte number in the quantum currently being
                   // processed (here, a quantum is a group of 4 chars which is
                   // being converted into the 3 byte binary representation).
                   // The byte numbers run DOWN from 3->0.  This starts with
                   // 4 but it will be decremented in the 'switch'.

    bool finish( false ); // A flag indicating the end of the data has been
                          // reached.
    unsigned char c; // The byte being written.

    // Get the value of the first Base-64 character.
    finish = !getChar( &source, c );

    // Loop until finished
    while ( !finish )
    {
        switch ( --qnum )
        {
        case 3:
            carry = c << 2;
            break;

        case 2:
            *( pDest++ ) = ( carry | ( c >> 4 ) );
            carry = c << 4;
            break;

        case 1:
            *( pDest++ ) = ( carry | ( c >> 2 ) );
            carry = c << 6;
            break;

        case 0:
            *( pDest++ ) = ( carry | c );
            break;
        }

        if ( qnum == 0 )
            qnum = 4;

        // Get the value of the next Base-64 character.
        finish = !getChar( &source, c );
    }

    // Return the size
    return pDest - reinterpret_cast< unsigned char* >( destination );
}

//-----------------------------------------------------------------------------
/// \brief  Calculate Encoded Size
///
/// Calculates the number of characters required to represent the requested
/// number of bytes in Base-64.  This does not include the null-terminator.
///
/// \param size The number of bytes to encode.
///
/// \return size_t The size of the encoded string (minus the null-terminator).
//-----------------------------------------------------------------------
size_t
Base64::calculateEncodedSize( size_t size )
{
    size_t dsize = 4 * ( size_t )( ( size + 2 ) / 3 );
    return dsize + ( dsize / mCharsPerLine );
}

//-----------------------------------------------------------------------------
/// \brief  Calculate Decoded Size
///
/// Calculates the number of bytes which the passed Base-64 string will decode
/// into.
///
/// \param s The string to check.
///
/// \return size_t The number of bytes.
//-----------------------------------------------------------------------
size_t
Base64::calculateDecodedSize( const char* s )
{
    // count number of base64 characters
    size_t      count = 0;
    const char* p = s;

    for ( size_t i = strlen( s ); ( i > 0 ) && ( *p != '=' ); i--, p++ )
    {
        if ( ( *p >= 'A' && *p <= 'Z' ) || ( *p >= 'a' && *p <= 'z' ) ||
             ( *p >= '0' && *p <= '9' ) || *p == '+' || *p == '/' )
        {
            count++;
        }
    }

    size_t size = count / 4 * 3;
    switch ( count % 4 )
    {
    case 0:
        break;

    case 2:
        size++;
        break;

    case 3:
        size += 2;
        break;
    }

    return size;
}

//-----------------------------------------------------------------------------
/// \brief  Set number of characters per line.
///
/// Sets number of characters per line for the encoded data.
///
/// \param num Number of characters.
///
/// \return size_t The number of bytes.
//-----------------------------------------------------------------------
void
Base64::setNumCharactersPerLine( const int num )
{
    if ( num <= 0 )
    {
        throw std::runtime_error( "Base64: negative or zero value for "
                                  "number of characters per line." );
    }

    if ( ( num % 4 ) != 0 )
    {
        throw std::runtime_error( "Base64: number of characters per line "
                                  "must be multiple of 4." );
    }

    mCharsPerLine = num;
    return;
}

//-----------------------------------------------------------------------
/// \param source
///	 The address of the pointer to the next Base-64
///      character in the string.  The pointer will be updated by this
///      method as it searches along the string.
/// \param c
///	The value of the next Base64 character found (0-63).
///     If a character was not found, the value is
///
/// \return
///	 True if a Base-64 character other than '=' was found.
///      false indicates that the string is finished.
//-----------------------------------------------------------------------
bool
Base64::getChar( const char** source, unsigned char& c )
{
    bool finish = false;

    c = *( ( *source )++ );
    if ( c == '\0' )
    {
        finish = true;
    }

#if __GNUC__ >= 3
    while ( std::isspace( c ) && !finish )
#else
    while ( isspace( c ) && !finish )
#endif
    {
        c = ( *( ( *source )++ ) );
        if ( c == '\0' )
        {
            finish = true;
        }
    }

    // If a character was read, check to make sure it is valid
    if ( !finish )
    {
        if ( ( c >= 'A' ) && ( c <= 'Z' ) )
        {
            c -= 'A';
        }
        else if ( ( c >= 'a' ) && ( c <= 'z' ) )
        {
            c -= 'a' - 26;
        }
        else if ( ( c >= '0' ) && ( c <= '9' ) )
        {
            c -= '0' - 52;
        }
        else if ( c == '+' )
        {
            c = 62;
        }
        else if ( c == '/' )
        {
            c = 63;
        }
        else
        {
            finish = true;
        }
    }

    // return true if a character was read
    return !finish;
}
