var fs = require('fs'),
    crypto = require('crypto'),
    events = require('events'),
    Step = require('step'),
    Format = require('./format'),
    path = require('path'),
    s64 = require('./safe64'),
    sys = require('sys'),
    localized = {},
    locked = {},
    get = require('get'),
    mapnik = require('mapnik'),
    carto = require('carto');

// Map constructor
//
// - `datasource` {String, Object} URL location of the datasource.
// - `options` {Object} options
//   - `mapfile_dir` {String} path to where data files should be cached.
//   - `width` {Number} width of the map
//   - `height` {Number} height of the map
var Map = function(datasource, options) {
    options = options || {};
    this.datasource = datasource;
    this.options = options || {};
    this.language = options.language || this.dslanguage();
    this.options.width = options.width || 256;
    this.options.height = options.height || 256;
    this.mapfile_dir = options.mapfile_dir || '/tmp';

    if (this.dstype() === 'filename') {
        this.data_dir = path.dirname(this.datasource) + '/.cache';
        this.local_data_dir = path.dirname(this.datasource);
    } else {
        this.data_dir = this.mapfile_dir;
        this.local_data_dir = '';
    }
};

// Find the mapfile on disk. If it's a URL, this will be
// the location of the downloaded Carto file. If it's a local path,
// this is the identity function.
//
// - @param {String} datasource.
// - @return {String} location.
Map.prototype.mapfilePos = function() {
    switch (this.dstype()) {
    case 'url':
        return this.mapfile_dir + '/' + s64.encode(this.datasource) + '.xml';
        break;
    case 'filename':
        if (this.language === 'xml') return this.datasource;
        return this.mapfile_dir + '/' + s64.encode(this.datasource) + '.xml';
        break;
    case 'literal':
        var hash = crypto
            .createHash('md5')
            .update(JSON.stringify(this.datasource))
            .digest('hex');
        return this.mapfile_dir + '/' + hash + '.xml';
        break;
    }
};

// Determine the type of datasource passed. Returns either:
// - `url` an http or https url
// - `filename` a filepath
// - `literal` a literal map representation, either XML string or JSON MML.
Map.prototype.dstype = function() {
    if (typeof this.datasource === 'string') {
        if (this.datasource.match(/^http/)) {
            return 'url';
        } else if (this.datasource.match(/^<\?xml/)) {
            return 'literal';
        } else {
            return 'filename';
        }
    } else {
        return 'literal';
    }
};

// Determine the language of datasource passed. Returns either:
// - `xml` a mapnik XML file
// - `carto` a carto MML file
Map.prototype.dslanguage = function() {
    if (typeof this.datasource === 'string') {
        if (this.datasource.match(/xml$/) || this.datasource.match(/^<\?xml/)) {
            return 'xml';
        } else {
            return 'carto';
        }
    } else {
        return 'carto';
    }
};

// TODO: make multi-process safe.
//
// Determine whether a datasource is localized.
Map.prototype.localized = function(set) {
    var id = this.mapfilePos();
    set && (localized[id] = set);
    return localized[id];
};

// Return whether the current mapfile is "locked", ie. currently
// being downloaded concurrently. If locked, returns an EventEmitter that
// emits an `unlock` event once the lock is released.
// TODO: make multi-process safe.
Map.prototype.locked = function(set, err) {
    var id = this.mapfilePos();
    if (typeof set !== 'undefined') {
        if (set) {
            locked[id] = new events.EventEmitter();
        } else if (locked[id]) {
            locked[id].emit('unlock', err);
            locked[id] = false;
        }
    }
    return locked[id] ? locked[id] : false;
};

// Initialize this map. This wraps `localize()` so that it isn't
// repetitively called, and it calls `create()` afterwards to actually
// create a new Mapnik map object if necessary.
Map.prototype.initialize = function(callback) {
    var that = this;

    if (this.localized()) {
        that.create();
        return callback();
    } else if (this.locked()) {
        this.locked().on('unlock', function(err) {
            that.localized() && that.create();
            return callback(err);
        });
        return;
    }

    // The mapfile has not been localized. Lock.
    that.locked(true);
    this.localize(function(err) {
        if (!err) {
            that.localized(true);
            that.create();
        }
        // Unlock the mapfile regardless of whether localization was successful
        // or not. Allows subsequent attempts rather than locking everyone out
        // after a random fail.
        that.locked(false, err);
        return callback(err);
    });
};

// Compile the datasource info a Mapnik XML mapfile with its dependencies
// (shapefiles, etc.) all in place. Will download (for URLs) and render (for
// Carto MML) if necessary or pass through appropriately. Calls `callback`
// when the mapfile can be expected to be in `mapfilePos`. If this isn't
// successful, `callback` gets an error as its first argument.
Map.prototype.localize = function(callback) {
    var that = this;
    Step(
        function() {
            switch (that.dstype()) {
            case 'url':
                // As of node-get 0.1.0, exceptions are no longer thrown
                (new get(that.datasource)).asString(this);
                break;
            case 'filename':
                fs.readFile(that.datasource, 'utf8', this);
                break;
            case 'literal':
                // Clone literal object to prevent mangling.
                this(null, JSON.parse(JSON.stringify(that.datasource)));
                break;
            }
        },
        function(err, data) {
            if (err) return this(err);
            if (that.language === 'carto') {
                var renderer = new carto.Renderer({
                    data_dir: that.data_dir,
                    local_data_dir: that.local_data_dir,
                    validation_data: { fonts: mapnik.fonts() }
                });
                renderer.render(data, this);
            } else {
                this(null, data);
            }
        },
        function(err, compiled) {
            if (err) return this(err);
            fs.writeFile(that.mapfilePos(), compiled, this);
        },
        function(err) {
            callback(err);
        }
    );
};

// Create a new mapnik map object at `this.mapnik`. Requires that the mapfile
// be localized with `this.localize()`. This can be called in repetition because
// it won't recreate `this.mapnik`.
Map.prototype.create = function() {
    if (this.mapnik) return true;

    this.mapnik = new mapnik.Map(
        this.options.width,
        this.options.height
    );

    try {
        this.mapnik.load(this.mapfilePos());
        this.mapnik.buffer_size(128);
        return true;
    } catch (err) {
        return false;
    }
};

// Destroy this map's mapnik instance, first clearing out
// its references to datasources and then attempting to
// delete its memory.
Map.prototype.destroy = function() {
    if (!this.mapnik) return;
    this.mapnik.clear();
    delete this.mapnik;
};

// Render handler for a given tile request.
Map.prototype.render = function(tile, callback) {
    Format.select(tile.format)(tile, this.mapnik, callback);
};

module.exports = Map;

@@ -1025,75 +1025,8 @@
 ) %7B%0A
-        this.data_dir = path.dirname(this.datasource) + '/.cache';%0A
     
@@ -1099,50 +1099,8 @@
 e %7B%0A
-        this.data_dir = this.mapfile_dir;%0A
     

Revert usage of local .cache dir.