/*** Furcadia InstructionSet Library ***/

//--- Objects -----------------------------------------------------------------
var Instruction = function(b64prefix) {
    // Methods
    this.setName = function(newName) { if (newName != null) { this.name = newName } }
    this.setPrefix = function(newPrefix) { if (newPrefix != null) { this.prefix = newPrefix } }
    this.setBase64Prefix = function(b64prefix) { this.setPrefix(base64Decode(b64prefix)) }
    this.setSeparator = function(newSeparator) { this.argSeparator = (newSeparator != null) ? newSeparator : "" }
    this.setDescription = function(newDesc) { if (newDesc != null) { this.description = newDesc } }
    this.addMeta = function(name,value) { this.metadata.push( [name,value] ) }
    this.setDeprecated = function(value) { this.deprecated = Boolean(value) }
    this.setObsolete = function(value) { this.obsolete = Boolean(value) }
    this.getArgumentByName = getArgumentByName
    

    // Members
    this.prefix = null

    if (b64prefix != null) this.setBase64Prefix(b64prefix)
    
    this.name = this.prefix
    this.description = ""
    this.argSeparator = ""
    
    this.deprecated = false
    this.obsolete = false
  
    this.arguments = new Array()
    this.metadata = new Array()    
};

var Argument = function(typeName, size) {
    // Methods
    this.setName = function(newName) { if (newName != null) { this.name = newName } }
    this.setType = function(typeName) { this.type = (typeName != null) ? typeName : "null" }
    this.setSize = setSize
    this.setSizeRange = setSizeRange
    this.setSummary = function(newSummary) { if (newSummary != null) { this.summary = newSummary } }
    
    this.setRepeatable = function(value) { this.repeatable = Boolean(value) }
    this.setOptional = function(value) { this.optional = Boolean(value) }

    // Members
    this.setType(typeName)
    this.name = "(arg)"
    this.summary = ""
    this.value = null
    this.defaultValue = ""

    this.setSize(size)
    
    this.optional = false
    this.repeatable = false    
};

//--- Method Definitions ------------------------------------------------------
// Set a new static size (in bytes) of an Argument object.
function setSize(newSize) {
    // If size is null, 0 (or less), or *, set it to 0 - this means the size is
    // dynamic/variable. Otherwise, make a distinction between a string and an
    // integer and make sure we end up with an integer in the end.
    this.size = (newSize == null || newSize <= 0 || newSize == "*") ? 0 : (typeof(newSize) == 'number') ? newSize : parseInt(newSize)

    // Setting minsize and maxsize to be the same because the size is static.
    this.minsize = this.size
    this.maxsize = this.size
    
    return this.size
}

// Set a new size range (in bytes) of an Argument object.
function setSizeRange(minSize, maxSize) {
    // If we end up with a static size here, use the appropriate function.
    if (minSize == maxSize)
        return this.setSize(minSize)

    this.size = 0 // Reset to 0 to indicate it's dynamic
    this.minsize = minSize
    this.maxsize = maxSize
    return this.size
}

// Returns an argument that matches the name given or null if not found.
function getArgumentByName(argName)
{
    for (var i = 0; i < this.arguments.length; i++)
        if (this.arguments[i].name == argName)
            return this.arguments[i]
    return null
}


//--- Standalone Functions ----------------------------------------------------
// Encode data from its regular string format to base64.
function base64Encode(data) {
    // TODO: Insert code for this
}

// Decode data from base64 format into a regular string.
function base64Decode(data) {
	data = data.replace(/[^a-z0-9\+\/=]/ig, '');
	if (typeof(atob) == 'function') return atob(data);
	var b64_map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
	var byte1, byte2, byte3;
	var ch1, ch2, ch3, ch4;
	var result = new Array();
	var j=0;
	while ((data.length%4) != 0) {
		data += '=';
	}

	for (var i=0; i <data.length; i+=4) {
		ch1 = b64_map.indexOf(data.charAt(i));
		ch2 = b64_map.indexOf(data.charAt(i+1));
		ch3 = b64_map.indexOf(data.charAt(i+2));
		ch4 = b64_map.indexOf(data.charAt(i+3));

		byte1 = (ch1 << 2)|(ch2 >> 4);
		byte2 = ((ch2 & 15) << 4)|(ch3 >> 2);
		byte3 = ((ch3 & 3) << 6) | ch4;

		result[j++] = String.fromCharCode(byte1);
		if (ch3 != 64) result[j++] = String.fromCharCode(byte2);
		if (ch4 != 64) result[j++] = String.fromCharCode(byte3);
	}

	return result.join('');
}

// Extract the instruction set (an array of Instruction objects) from within
// an XML object generated by reading the instruction file.
function extractInstrSet(xmlObj) {
    var instrSet = new Array()
    var instructions = xmlObj.documentElement
    var tmp = null
    
    for (var instr = instructions.firstChild; instr; instr = instr.nextSibling)
    {
        // If there's something else in the instructionset, ignore it.
        if (instr.nodeName != "instruction") continue;
        
        // Create a new Instruction object and fill it
        var instrObj = new Instruction(null)
        
        for (var i = 0; i < instr.attributes.length; i++)
        {
            var attrName = instr.attributes[i].name
            var attrVal  = instr.attributes[i].value
            
            switch (attrName)
            {
            case "prefix":
                instrObj.setBase64Prefix(attrVal)
                break
            case "name":
                instrObj.setName(attrVal)
                break
            case "deprecated":
                instrObj.setDeprecated(parseInt(attrVal))
                break
            case "obsolete":
                instrObj.setObsolete(parseInt(attrVal))
                break
            default:
                // Unknown attributes are ignored.
                break
            }
            
        }
        
        // If the object has no prefix yet, dump it - it's broken.
        if (instrObj.prefix == null) continue
                
        // Go through the child tags and process them.
        for (var elem = instr.firstChild; elem; elem = elem.nextSibling)
        {
            switch (elem.nodeName)
            {
            case "description":
                instrObj.setDescription(elem.firstChild.data)
                break
                
            case "arguments":
                var tmp = elem.attributes.getNamedItem('separator')
                if (tmp != null)
                    instrObj.setSeparator(tmp.value)
                
                instrObj.arguments = extractArguments(elem)
                break
            
            case "metadata":
                instrObj.metadata = extractMetaData(elem)
                break
            default:
                // Do nothing...
                break
            }
        }
        
        // Add to the rest of the instructions.
        instrSet.push(instrObj)
    }
    
    return instrSet
}

// Extracts the arguments in their processed XML form and returns an array of
// Argument objects, preset with the data.
function extractArguments(argumentsObj) {
    var arguments = new Array()
    
    for (var arg = argumentsObj.firstChild; arg; arg = arg.nextSibling)
    {
        if (arg.nodeName != "argument") continue
        
        var argObj = new Argument("null", 0)
        var minsize = null
        var maxsize = null
        
        for (var i = 0; i < arg.attributes.length; i++)
        {
            var attrName = arg.attributes[i].name
            var attrVal  = arg.attributes[i].value
            
            switch (attrName)
            {
                case "type":
                    argObj.setType(attrVal)
                    break
                case "name":
                    argObj.setName(attrVal)
                    break
                case "size":
                    argObj.setSize(attrVal)
                    break
                case "minsize":
                    minsize = attrVal
                    break
                case "maxsize":
                    maxsize = attrVal
                    break
                case "repeatable":
                    argObj.setRepeatable(attrVal)
                    break
                case "optional":
                    argObj.setOptional(attrVal)
                    break
                case "summary":
                    argObj.setSummary(attrVal)
                    break
                case "default":
                    argObj.defaultValue = attrVal
                    break
                case "value":
                    argObj.value = attrVal
                    break
                default:
                    // Do nothing if it's an unknown attribute.
                    break
            }
            
            // If we have a minsize and maxsize, set a range.
            if (minsize != maxsize)
                argObj.setSizeRange(minsize,maxsize)
        }
        
        // If we have static data, adjust the size to the value's length.
        if (argObj.type == "static")
        {
            argObj.setSize(argObj.value.length)
            argObj.name = "Static Data"
        }
        
        arguments.push(argObj)
    }
    
    return arguments
}

// Extracts the metadata in its processed XML form and returns an array of
// [name,value] pairs.
function extractMetaData(metadataObj) {
    var metadata = new Array()
    
    for (var meta = metadataObj.firstChild; meta; meta = meta.nextSibling)
    {
        if (meta.nodeName != "meta") continue
        
        var name  = meta.attributes.getNamedItem('name')
        var value = meta.attributes.getNamedItem('value')
        
        // If we don't have a name, assume the tag is broken.
        if (name == null) continue
        
        value = (value != null) ? value.value : ""
        metadata.push( [name.value,value] )
    }
    
    return metadata
}
