//
//  Base64.m
//
//  Version 1.1
//
//  Created by Nick Lockwood on 12/01/2012.
//  Copyright (C) 2012 Charcoal Design
//
//  Distributed under the permissive zlib License
//  Get the latest version from here:
//
//  https://github.com/nicklockwood/Base64
//
//  This software is provided 'as-is', without any express or implied
//  warranty.  In no event will the authors be held liable for any damages
//  arising from the use of this software.
//
//  Permission is granted to anyone to use this software for any purpose,
//  including commercial applications, and to alter it and redistribute it
//  freely, subject to the following restrictions:
//
//  1. The origin of this software must not be misrepresented; you must not
//  claim that you wrote the original software. If you use this software
//  in a product, an acknowledgment in the product documentation would be
//  appreciated but is not required.
//
//  2. Altered source versions must be plainly marked as such, and must not be
//  misrepresented as being the original software.
//
//  3. This notice may not be removed or altered from any source distribution.
//

#import "Base64.h"


#import <Availability.h>
#if !__has_feature(objc_arc)
#error This library requires automatic reference counting
#endif


@implementation NSData (Base64)

+ (NSData *)dataWithBase64EncodedString:(NSString *)string
{
  const char lookup[] =
  {
    99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
    99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
    99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 62, 99, 99, 99, 63,
    52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 99, 99, 99, 99, 99, 99,
    99,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14,
    15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 99, 99, 99, 99, 99,
    99, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
    41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 99, 99, 99, 99, 99
  };

  NSData *inputData = [string dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
  long long inputLength = [inputData length];
  const unsigned char *inputBytes = [inputData bytes];

  long long maxOutputLength = (inputLength / 4 + 1) * 3;
  NSMutableData *outputData = [NSMutableData dataWithLength:maxOutputLength];
  unsigned char *outputBytes = (unsigned char *)[outputData mutableBytes];

  int accumulator = 0;
  long long outputLength = 0;
  unsigned char accumulated[] = {0, 0, 0, 0};
  for (long long i = 0; i < inputLength; i++)
  {
    unsigned char decoded = lookup[inputBytes[i] & 0x7F];
    if (decoded != 99)
    {
      accumulated[accumulator] = decoded;
      if (accumulator == 3)
      {
        outputBytes[outputLength++] = (accumulated[0] << 2) | (accumulated[1] >> 4);
        outputBytes[outputLength++] = (accumulated[1] << 4) | (accumulated[2] >> 2);
        outputBytes[outputLength++] = (accumulated[2] << 6) | accumulated[3];
      }
      accumulator = (accumulator + 1) % 4;
    }
  }

  //handle left-over data
  if (accumulator > 0) outputBytes[outputLength] = (accumulated[0] << 2) | (accumulated[1] >> 4);
  if (accumulator > 1) outputBytes[++outputLength] = (accumulated[1] << 4) | (accumulated[2] >> 2);
  if (accumulator > 2) outputLength++;

  //truncate data to match actual output length
  outputData.length = outputLength;
  return outputLength? outputData: nil;
}

- (NSString *)base64EncodedStringWithWrapWidth:(NSUInteger)wrapWidth
{
  //ensure wrapWidth is a multiple of 4
  wrapWidth = (wrapWidth / 4) * 4;

  const char lookup[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

  long long inputLength = [self length];
  const unsigned char *inputBytes = [self bytes];

  long long maxOutputLength = (inputLength / 3 + 1) * 4;
  maxOutputLength += wrapWidth? (maxOutputLength / wrapWidth) * 2: 0;
  unsigned char *outputBytes = (unsigned char *)malloc(maxOutputLength);

  long long i;
  long long outputLength = 0;
  for (i = 0; i < inputLength - 2; i += 3)
  {
    outputBytes[outputLength++] = lookup[(inputBytes[i] & 0xFC) >> 2];
    outputBytes[outputLength++] = lookup[((inputBytes[i] & 0x03) << 4) | ((inputBytes[i + 1] & 0xF0) >> 4)];
    outputBytes[outputLength++] = lookup[((inputBytes[i + 1] & 0x0F) << 2) | ((inputBytes[i + 2] & 0xC0) >> 6)];
    outputBytes[outputLength++] = lookup[inputBytes[i + 2] & 0x3F];

    //add line break
    if (wrapWidth && (outputLength + 2) % (wrapWidth + 2) == 0)
    {
      outputBytes[outputLength++] = '\r';
      outputBytes[outputLength++] = '\n';
    }
  }

  //handle left-over data
  if (i == inputLength - 2)
  {
    // = terminator
    outputBytes[outputLength++] = lookup[(inputBytes[i] & 0xFC) >> 2];
    outputBytes[outputLength++] = lookup[((inputBytes[i] & 0x03) << 4) | ((inputBytes[i + 1] & 0xF0) >> 4)];
    outputBytes[outputLength++] = lookup[(inputBytes[i + 1] & 0x0F) << 2];
    outputBytes[outputLength++] =   '=';
  }
  else if (i == inputLength - 1)
  {
    // == terminator
    outputBytes[outputLength++] = lookup[(inputBytes[i] & 0xFC) >> 2];
    outputBytes[outputLength++] = lookup[(inputBytes[i] & 0x03) << 4];
    outputBytes[outputLength++] = '=';
    outputBytes[outputLength++] = '=';
  }

  if (outputLength >= 4)
  {
    //truncate data to match actual output length
    outputBytes = realloc(outputBytes, outputLength);
    return [[NSString alloc] initWithBytesNoCopy:outputBytes
                                          length:outputLength
                                        encoding:NSASCIIStringEncoding
                                    freeWhenDone:YES];
  }
  else if (outputBytes)
  {
    free(outputBytes);
  }
  return nil;
}

- (NSString *)base64EncodedString
{
  return [self base64EncodedStringWithWrapWidth:0];
}

@end


@implementation NSString (Base64)

+ (NSString *)stringWithBase64EncodedString:(NSString *)string
{
  NSData *data = [NSData dataWithBase64EncodedString:string];
  if (data)
  {
    return [[self alloc] initWithData:data encoding:NSUTF8StringEncoding];
  }
  return nil;
}

- (NSString *)base64EncodedStringWithWrapWidth:(NSUInteger)wrapWidth
{
  NSData *data = [self dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES];
  return [data base64EncodedStringWithWrapWidth:wrapWidth];
}

- (NSString *)base64EncodedString
{
  NSData *data = [self dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES];
  return [data base64EncodedString];
}

- (NSString *)base64DecodedString
{
  return [NSString stringWithBase64EncodedString:self];
}

- (NSData *)base64DecodedData
{
  return [NSData dataWithBase64EncodedString:self];
}

@end
