/** vim: et:ts=4:sw=4:sts=4

* @license amdefine 0.1.0 Copyright (c) 2011, The Dojo Foundation All Rights Reserved.
* Available via the MIT or new BSD license.
* see: http://github.com/jrburke/amdefine for details
*/

/*jslint node: true */ /*global module, process */ 'use strict';

/**

* Creates a define for node.
* @param {Object} module the "module" object that is defined by Node for the
* current module.
* @param {Function} [requireFn]. Node's require function for the current module.
* It only needs to be passed in Node versions before 0.5, when module.require
* did not exist.
* @returns {Function} a define function that is usable for the current node
* module.
*/

function amdefine(module, requireFn) {

'use strict';
var defineCache = {},
    loaderCache = {},
    alreadyCalled = false,
    path = require('path'),
    makeRequire, stringRequire;

/**
 * Trims the . and .. from an array of path segments.
 * It will keep a leading path segment if a .. will become
 * the first path segment, to help with module name lookups,
 * which act like paths, but can be remapped. But the end result,
 * all paths that use this function should look normalized.
 * NOTE: this method MODIFIES the input array.
 * @param {Array} ary the array of path segments.
 */
function trimDots(ary) {
    var i, part;
    for (i = 0; ary[i]; i+= 1) {
        part = ary[i];
        if (part === '.') {
            ary.splice(i, 1);
            i -= 1;
        } else if (part === '..') {
            if (i === 1 && (ary[2] === '..' || ary[0] === '..')) {
                //End of the line. Keep at least one non-dot
                //path segment at the front so it can be mapped
                //correctly to disk. Otherwise, there is likely
                //no path mapping for a path starting with '..'.
                //This can still fail, but catches the most reasonable
                //uses of ..
                break;
            } else if (i > 0) {
                ary.splice(i - 1, 2);
                i -= 2;
            }
        }
    }
}

function normalize(name, baseName) {
    var baseParts;

    //Adjust any relative paths.
    if (name && name.charAt(0) === '.') {
        //If have a base name, try to normalize against it,
        //otherwise, assume it is a top-level require that will
        //be relative to baseUrl in the end.
        if (baseName) {
            baseParts = baseName.split('/');
            baseParts = baseParts.slice(0, baseParts.length - 1);
            baseParts = baseParts.concat(name.split('/'));
            trimDots(baseParts);
            name = baseParts.join('/');
        }
    }

    return name;
}

/**
 * Create the normalize() function passed to a loader plugin's
 * normalize method.
 */
function makeNormalize(relName) {
    return function (name) {
        return normalize(name, relName);
    };
}

function makeLoad(id) {
    function load(value) {
        loaderCache[id] = value;
    }

    load.fromText = function (id, text) {
        //This one is difficult because the text can/probably uses
        //define, and any relative paths and requires should be relative
        //to that id was it would be found on disk. But this would require
        //bootstrapping a module/require fairly deeply from node core.
        //Not sure how best to go about that yet.
        throw new Error('amdefine does not implement load.fromText');
    };

    return load;
}

makeRequire = function (systemRequire, exports, module, relId) {
    function amdRequire(deps, callback) {
        if (typeof deps === 'string') {
            //Synchronous, single module require('')
            return stringRequire(systemRequire, exports, module, deps, relId);
        } else {
            //Array of dependencies with a callback.

            //Convert the dependencies to modules.
            deps = deps.map(function (depName) {
                return stringRequire(systemRequire, exports, module, depName, relId);
            });

            //Wait for next tick to call back the require call.
            process.nextTick(function () {
                callback.apply(null, deps);
            });
        }
    }

    amdRequire.toUrl = function (filePath) {
        if (filePath.indexOf('.') === 0) {
            return normalize(filePath, path.dirname(module.filename));
        } else {
            return filePath;
        }
    };

    return amdRequire;
};

//Favor explicit value, passed in if the module wants to support Node 0.4.
requireFn = requireFn || function req() {
    return module.require.apply(module, arguments);
};

function runFactory(id, deps, factory) {
    var r, e, m, result;

    if (id) {
        e = loaderCache[id] = {};
        m = {
            id: id,
            uri: __filename,
            exports: e
        };
        r = makeRequire(requireFn, e, m, id);
    } else {
        //Only support one define call per file
        if (alreadyCalled) {
            throw new Error('amdefine with no module ID cannot be called more than once per file.');
        }
        alreadyCalled = true;

        //Use the real variables from node
        //Use module.exports for exports, since
        //the exports in here is amdefine exports.
        e = module.exports;
        m = module;
        r = makeRequire(requireFn, e, m, module.id);
    }

    //If there are dependencies, they are strings, so need
    //to convert them to dependency values.
    if (deps) {
        deps = deps.map(function (depName) {
            return r(depName);
        });
    }

    //Call the factory with the right dependencies.
    if (typeof factory === 'function') {
        result = factory.apply(m.exports, deps);
    } else {
        result = factory;
    }

    if (result !== undefined) {
        m.exports = result;
        if (id) {
            loaderCache[id] = m.exports;
        }
    }
}

stringRequire = function (systemRequire, exports, module, id, relId) {
    //Split the ID by a ! so that
    var index = id.indexOf('!'),
        originalId = id,
        prefix, plugin;

    if (index === -1) {
        id = normalize(id, relId);

        //Straight module lookup. If it is one of the special dependencies,
        //deal with it, otherwise, delegate to node.
        if (id === 'require') {
            return makeRequire(systemRequire, exports, module, relId);
        } else if (id === 'exports') {
            return exports;
        } else if (id === 'module') {
            return module;
        } else if (loaderCache.hasOwnProperty(id)) {
            return loaderCache[id];
        } else if (defineCache[id]) {
            runFactory.apply(null, defineCache[id]);
            return loaderCache[id];
        } else {
            if(systemRequire) {
                return systemRequire(originalId);
            } else {
                throw new Error('No module with ID: ' + id);
            }
        }
    } else {
        //There is a plugin in play.
        prefix = id.substring(0, index);
        id = id.substring(index + 1, id.length);

        plugin = stringRequire(systemRequire, exports, module, prefix, relId);

        if (plugin.normalize) {
            id = plugin.normalize(id, makeNormalize(relId));
        } else {
            //Normalize the ID normally.
            id = normalize(id, relId);
        }

        if (loaderCache[id]) {
            return loaderCache[id];
        } else {
            plugin.load(id, makeRequire(systemRequire, exports, module, relId), makeLoad(id), {});

            return loaderCache[id];
        }
    }
};

//Create a define function specific to the module asking for amdefine.
function define(id, deps, factory) {
    if (Array.isArray(id)) {
        factory = deps;
        deps = id;
        id = undefined;
    } else if (typeof id !== 'string') {
        factory = id;
        id = deps = undefined;
    }

    if (deps && !Array.isArray(deps)) {
        factory = deps;
        deps = undefined;
    }

    if (!deps) {
        deps = ['require', 'exports', 'module'];
    }

    //Set up properties for this module. If an ID, then use
    //internal cache. If no ID, then use the external variables
    //for this node module.
    if (id) {
        //Put the module in deep freeze until there is a
        //require call for it.
        defineCache[id] = [id, deps, factory];
    } else {
        runFactory(id, deps, factory);
    }
}

//define.require, which has access to all the values in the
//cache. Useful for AMD modules that all have IDs in the file,
//but need to finally export a value to node based on one of those
//IDs.
define.require = function (id) {
    if (loaderCache[id]) {
        return loaderCache[id];
    }

    if (defineCache[id]) {
        runFactory.apply(null, defineCache[id]);
        return loaderCache[id];
    }
};

define.amd = {};

return define;

}

module.exports = amdefine;