/*
 * CFData.js
 * Objective-J
 *
 * Created by Francisco Tolmasky.
 * Copyright 2008-2010, 280 North, Inc.
 *
 * 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 (at your option) 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

GLOBAL(CFData) = function()
{
    this._rawString = NULL;

    this._propertyList = NULL;
    this._propertyListFormat = NULL;

    this._JSONObject = NULL;

    this._bytes = NULL;
    this._base64 = NULL;
}

CFData.prototype.propertyList = function()
{
    if (!this._propertyList)
        this._propertyList = CFPropertyList.propertyListFromString(this.rawString());

    return this._propertyList;
}

CFData.prototype.JSONObject = function()
{
    if (!this._JSONObject)
    {
        try
        {
            this._JSONObject = JSON.parse(this.rawString());
        }
        catch (anException)
        {
        }
    }

    return this._JSONObject;
}

CFData.prototype.rawString = function()
{
    if (this._rawString === NULL)
    {
        if (this._propertyList)
            this._rawString = CFPropertyList.stringFromPropertyList(this._propertyList, this._propertyListFormat);

        else if (this._JSONObject)
            this._rawString = JSON.stringify(this._JSONObject);

//        Ideally we would convert these bytes or base64 into a string.
//        else if (this._bytes)
//        else if (this._base64)

        else
            throw new Error("Can't convert data to string.");
    }

    return this._rawString;
}

CFData.prototype.bytes = function()
{
    return this._bytes;
}

CFData.prototype.base64 = function()
{
    return this._base64;
}

GLOBAL(CFMutableData) = function()
{
    CFData.call(this);
}

CFMutableData.prototype = new CFData();

function clearMutableData(/*CFMutableData*/ aData)
{
    this._rawString = NULL;

    this._propertyList = NULL;
    this._propertyListFormat = NULL;

    this._JSONObject = NULL;

    this._bytes = NULL;
    this._base64 = NULL;
}

CFMutableData.prototype.setPropertyList = function(/*PropertyList*/ aPropertyList, /*Format*/ aFormat)
{
    clearMutableData(this);

    this._propertyList = aPropertyList;
    this._propertyListFormat = aFormat;
}

CFMutableData.prototype.setJSONObject = function(/*Object*/ anObject)
{
    clearMutableData(this);

    this._JSONObject = anObject
}

CFMutableData.prototype.setRawString = function(/*String*/ aString)
{
    clearMutableData(this);

    this._rawString = aString;
}

CFMutableData.prototype.setBytes = function(/*Array*/ bytes)
{
    clearMutableData(this);

    this._bytes = bytes;
}

CFMutableData.prototype.setBase64String = function(/*String*/ aBase64String)
{
    clearMutableData(this);

    this._base64 = aBase64String;
}

// Base64 encoding and decoding

var base64_map_to = [
        "A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z",
        "a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z",
        "0","1","2","3","4","5","6","7","8","9","+","/","="],
    base64_map_from = [];

for (var i = 0; i < base64_map_to.length; i++)
    base64_map_from[base64_map_to[i].charCodeAt(0)] = i;

CFData.decodeBase64ToArray = function(input, strip)
{
    if (strip)
        input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");

    var pad = (input[input.length-1] == "=" ? 1 : 0) + (input[input.length-2] == "=" ? 1 : 0),
        length = input.length,
        output = [];

    var i = 0;
    while (i < length)
    {
        var bits =  (base64_map_from[input.charCodeAt(i++)] << 18) |
                    (base64_map_from[input.charCodeAt(i++)] << 12) |
                    (base64_map_from[input.charCodeAt(i++)] << 6) |
                    (base64_map_from[input.charCodeAt(i++)]);

        output.push((bits & 0xFF0000) >> 16);
        output.push((bits & 0xFF00) >> 8);
        output.push(bits & 0xFF);
    }

    // strip "=" padding from end
    if (pad > 0)
        return output.slice(0, -1 * pad);

    return output;
}

CFData.encodeBase64Array = function(input)
{
    var pad = (3 - (input.length % 3)) % 3,
        length = input.length + pad,
        output = [];

    // pad with nulls
    if (pad > 0) input.push(0);
    if (pad > 1) input.push(0);

    var i = 0;
    while (i < length)
    {
        var bits =  (input[i++] << 16) |
                    (input[i++] << 8)  |
                    (input[i++]);

        output.push(base64_map_to[(bits & 0xFC0000) >> 18]);
        output.push(base64_map_to[(bits & 0x3F000) >> 12]);
        output.push(base64_map_to[(bits & 0xFC0) >> 6]);
        output.push(base64_map_to[bits & 0x3F]);
    }

    // pad with "=" and revert array to previous state
    if (pad > 0)
    {
        output[output.length-1] = "=";
        input.pop();
    }
    if (pad > 1)
    {
        output[output.length-2] = "=";
        input.pop();
    }

    return output.join("");
}

CFData.decodeBase64ToString = function(input, strip)
{
    return CFData.bytesToString(CFData.decodeBase64ToArray(input, strip));
}

CFData.decodeBase64ToUtf16String = function(input, strip)
{
    return CFData.bytesToUtf16String(CFData.decodeBase64ToArray(input, strip));
}

CFData.bytesToString = function(bytes)
{
    // This is relatively efficient, I think:
    return String.fromCharCode.apply(NULL, bytes);
}


CFData.encodeBase64String = function(input)
{
    var temp = [];
    for (var i = 0; i < input.length; i++)
        temp.push(input.charCodeAt(i));

    return CFData.encodeBase64Array(temp);
}

CFData.bytesToUtf16String = function(bytes)
{
    // Strings are encoded with 16 bits per character.
    var temp = [];
    for (var i = 0; i < bytes.length; i+=2)
        temp.push(bytes[i+1] << 8 | bytes[i]);
    // This is relatively efficient, I think:
    return String.fromCharCode.apply(NULL, temp);
}


CFData.encodeBase64Utf16String = function(input)
{
    // charCodeAt returns UTF-16.
    var temp = [];
    for (var i = 0; i < input.length; i++)
    {
        var c = input.charCodeAt(i);
        temp.push(input.charCodeAt(i) & 0xFF);
        temp.push((input.charCodeAt(i) & 0xFF00) >> 8);
    }

    return CFData.encodeBase64Array(temp);
}
