// if (!console){
//     var console = {
//         assert: function(){return null;}
//     };
// }
// else if (typeof(console.assert) != 'function') {
//     console.assert = function(){return null;};
// }

var FilterParam = function(kwargs){
    // binds kwargs to instance
    for (var k in this._defaults){
        if (typeof(kwargs[k]) == 'undefined'){
            this[k] = this._defaults[k];
        }
        else {
            this[k] = kwargs[k];
        }
    }
    if (this.value === null){
        this.value = this.identity;
    }
};  
FilterParam.prototype = {
    //DEFAULTS MUST MATCH DEFAULTS ON SERVER
    _defaults: {
        'value': null,
        'min': 0,
        'max': 100,
        'identity': 0,
        'exponent': 1, 
        'multiplier': 1,
        'intercept': 0,
        'scratchArgPos': 0
    },
    set: function(value){
        if (typeof(value) == 'undefined' || value === null || value === ''){
            return false;
        }
        else if (typeof(value) == 'number'){
            if (this.min !== null && value < this.min){
                this.value = this.min;
            }
            else if (this.max !== null && value > this.max){
                this.value = this.max;
            }
            else {
                this.value = value;
            }
        }
        else {
            this.value = value;
        }
        return true;
    },
    getScratchValue: function(){
        if (this.value === null || this.value == this.identity) return '';
          
        var scratchValue;
        if (this.value < 0){
            scratchValue = -1 * Math.pow(Math.abs(this.value), this.exponent) * this.multiplier + this.intercept;
        }
        else {
            scratchValue = Math.pow(this.value, this.exponent) * this.multiplier + this.intercept;
        }
        
        if (Math.round(scratchValue) == scratchValue){
            return scratchValue;
        }
        else {
            scratchValue = Math.round((scratchValue * 1000)) / 1000;
            return scratchValue;
        }
    },
    toDef: function(){
        var defin = {};
        for (var p in this._defaults){
            if (this[p] != this._defaults[p]){
                defin[p] = this[p];
            }
        }
        return defin;
    }
};
// (function() {
//     var fp = new FilterParam({'value':41, 'multiplier':0.01, 'intercept':0.1});
//     console.assert(fp.value == 41, 'Value should be initiatlized.');
//     console.assert(fp.getScratchValue() == 0.51, 'Scratch value should be scaled and shifted.');
//     console.assert(fp.max == 100, 'Default should be set.');
//     fp.set(200);
//     console.assert(fp.value == 100, 'Set should stop at maximum.');    
//     var fp2 = new FilterParam({'identity':1});
//     console.assert(fp2.value == 1, 'Value should be set identity by default.');
//     console.assert(fp2.getScratchValue() === '', 'Values equal to identity should not be requested from Scratch.');
// })();

var Filter = function(kwargs){
    for (var k in kwargs){
        this[k] = kwargs[k];
    }
};
Filter.prototype = {
    getValue: function(){
        return this.getFirstParam().value;
    },
    setValue: function(value){
        var p = this.getFirstParam();
        return p.set(value);
    },
    getFirstParam: function(){
        for (var p in this.params){
            if (this.params[p].scratchArgPos === 0){
                return this.params[p];
            }
        }
        return null;
    },
    toSegment: function(){
        if (this.isBooleanInScratch){
            if (this.getValue()){
                return this.scratchName;                
            }
            else {
                return '';
            }
        }
        var scratchValues = [];
        for (var p in this.params){
            scratchValues[this.params[p].scratchArgPos] = this.params[p].getScratchValue();
        }
        if (scratchValues.join('') === ''){
            return '';                
        }
        else {
            return this.scratchName + '_' + scratchValues.join(',');
        }
    },
    toDef: function(){
        var defin = {'params':{}};
        for (var k in this){
            if (k == 'params'){
                for (var p in this['params']){
                    defin['params'][p] = this['params'][p].toDef();
                }
            }
            //exclude methods in prototype and shortcut references
            else if (!Filter.prototype[k] && !this.params[k]){
                defin[k] = this[k];
            }
        }
        return defin;
    }
};
// (function() {
//     var size = new Filter({'scratchName':'size', 'params':{'height': new FilterParam({'value':200, 'scratchArgPos':1}),'width': new FilterParam({'value':100, 'scratchArgPos':0})}});
//     console.assert(size.toSegment() == 'size_100,200', 'Filter should translate to Scratch syntax.');
//     var hue = new Filter({'scratchName':'hue', 'params':{'anyname': new FilterParam({'value':50})}});
//     hue.setValue(48);
//     console.assert(hue.getValue() == 48, 'Getter and setter should work with first param.');
// })();

var State = function(stateDef){
    var state = {};
    for (var f in stateDef){
        if (!stateDef[f]['scratchName']){
            stateDef[f]['scratchName'] = f;
        }
        state[f] = new Filter(stateDef[f]);
        state[f]['params'] = {};
        for (var p in stateDef[f]['params']){
            state[f]['params'][p] = new FilterParam(stateDef[f]['params'][p]);
        }
    }
    this.filters = state;

    // create shortcut references
    for (var fi in this.filters){
        this[fi] = this.filters[fi];
        for (var pa in this.filters[fi]['params']){
            this.filters[fi][pa] = this.filters[fi]['params'][pa];
        }
    }
};  
State.prototype = {
    toSegments: function(){
        var sortedFilters = [];
        for (var fi in this.filters){
            sortedFilters.push(this.filters[fi]);
        }
        sortedFilters.sort(function(a, b){
            return (b.priority || 0) - (a.priority || 0);
        });

        var segments = [];
        for (var i=0; i < sortedFilters.length; i++){
            // todo: crop has its origin in bottom left... need to fix somewhere
            if (sortedFilters[i].scratchName == 'crop'){
                cps = this.filters['crop'].params;
                sps = this.filters['size'].params;
                var newY = (sps.height.value - cps.y.value - cps.height.value);
                segments.push('crop_'+cps.x.value+','+newY+','+cps.width.value+','+cps.height.value);
            }
            //don't print filters that return empty
            else if (sortedFilters[i].toSegment()){
                segments.push(sortedFilters[i].toSegment());
            }
        }                
        return segments.join('/');
    },
    toDef: function(){
         var defin = {};
         for (var f in this.filters){
            defin[f] = this.filters[f].toDef();
         }                                     
         return defin;
    }
};
// (function() {
//     var hue = new Filter({'scratchName':'hue', 'params':{'anyname': new FilterParam({'value':50})}});
//     var size = new Filter({'scratchName':'size', 'params':{'height': new FilterParam({'value':200, 'scratchArgPos':1}),'width': new FilterParam({'value':100, 'scratchArgPos':0})}});
//     var state = new State({'hue': hue.toDef(), 'size': size.toDef()});
//     console.assert(state.toSegments() == 'size_100,200/hue_50', 'toSegments should get values and join segments.');
// })();

var metaImageFromRemote = function(metaImageID, elementID){
    var newMetaImage = {};
    var d = loadJSONDoc('/open/' + metaImageID);
    d.addCallback(function(metaImageDef){
        var temp = new MetaImage(metaImageDef, elementID);
        update(newMetaImage, temp);
    });
    // warning: newMetaImage properties are not available until callback has fired
    return newMetaImage;
};
var MetaImage = function(metaImageDef, elementID){
    this.image = document.getElementById(elementID);
    this.id = elementID;
    if (!metaImageDef.history.length || !metaImageDef.historyIndex){
        throw 'metaImageDef must have a history.';
    }
    for (var k in metaImageDef) {
        if (k == 'history'){
            this.history = [];
            for (var i=0; i < metaImageDef.history.length; i++){
                //instantiate state using proper constructor
                this.history[i] = new State(metaImageDef.history[i]);
            }
        }
        else {
            this[k] = metaImageDef[k];
        }
    }
    //state attribute starts as current history
    this.state = new State(metaImageDef.history.slice(metaImageDef.historyIndex)[0]);
};                                                           
MetaImage.prototype = {
    communicateWithServer: false,

    domain: 'services.snipshot.com.local',
    pathToScratch: 'scratch',
    inputImageType: 'jpg', 
    outputImageType: 'jpg',
    excludeCrop: true,
    
    commit: function(){
        var json = serializeJSON(this.state.toDef());
        if (this.communicateWithServer){
            var opts = {
                'method': 'POST', 
                'sendContent': 'stateDef=' + urlEncode(json),
                'headers': {'Content-type': 'application/x-www-form-urlencoded'}
            };
            var d = doXHR('/commit/' + this.id, opts);
            return d;
        }
        else return true;
    },
    updateHistory: function(){
        this.history = this.history.slice(0, this.history.length+this.historyIndex+1);
        this.historyIndex = -1;   
        this.history.push(new State(this.state.toDef()));
    },
    save: function(){
        var h = this.history.slice(this.historyIndex)[0];
        // todo: proper object comparison!
        if (serializeJSON(h.toDef()) == serializeJSON(this.state.toDef())){
            return false;
        }
        this.updateHistory();

        if (this.communicateWithServer){
            //delete cookie before AJAX request to save space
            writeCookie('metaImage', '');
            var d = this.commit();
            this.saveToCookie();
            return d;
        }
        else {
            this.saveToCookie();
            return true;
        }
    },
    restoreHistory: function(newIndex){
        // todo: build support for positive index values
        // todo: checking on server side?
        if (!newIndex || newIndex > -1 || Math.abs(newIndex) > this.history.length){
            return false;
        }
        var newState = this.history.slice(newIndex)[0];
        this.state = new State(newState.toDef());
        this.historyIndex = newIndex;

        if (this.communicateWithServer){
            if (this._waitingToRestore){
                this._waitingToRestore.cancel();                
            }
            //delay request to ease load on server            
            this._waitingToRestore = callLater(1, function(id){
                //delete cookie before AJAX request to save space
                writeCookie('metaImage', '');
                var d = doXHR('/restore/' + id + '/' + newIndex);
                this.saveToCookie();
                return d;
            }, this.id);
            
            return this._waitingToRestore;
        }
        else {
            this.saveToCookie();
            return true;
        }
    },
    saveToCookie: function(){
        var stuff = {
            'id':this.id,
            'historyIndex':this.historyIndex, 
            // todo too big... need compress
            // 'stateDef':this.state.toDef()
            'scratchURL': this._buildURL(this.domain, this.pathToScratch, this.inputImageType, this.outputImageType)
        };
        //todo: best cookie name?
        var json = serializeJSON(stuff).replace(/\s/g, '');
        writeCookie('metaImage', json);
        return stuff;
    },
    _buildURL: function(domain, pathToScratch, inputImageType, outputImageType){
    	return 'http://' + domain + '/' + pathToScratch + '/' + this.id + '.' + inputImageType + '/' + this.state.toSegments() + '/snipshot' + '.' + outputImageType;
    },
    loadImage: function(){
    	var scratchURL = this._buildURL(this.domain, this.pathToScratch, this.inputImageType, this.outputImageType);
        // todo: refactor special case for snipshot
        if (this.excludeCrop){
            scratchURL = scratchURL.replace(/\/crop[^\/]*/, '');
        }
    	this.image.src = scratchURL;
        // console.log(scratchURL)
        return this.image.src;
    },
    // HIGHEST LEVEL FUNCTIONS START HERE
    change: function(filterName, value){
        // special cases for convenience
        if (filterName == 'width' || filterName == 'height'){
            this.state.filters.size[filterName].set(value);	           
        }                      
        // special cases for convenience
        else if (filterName == 'contrast' || filterName == 'saturation' || filterName == 'brightness'){
            this.state.filters.color[filterName].set(value);	           
        }
        else {
            this.state.filters[filterName].setValue(value);
        }
        var d = this.save();
        if (d){
            this.loadImage();
            return d;
        } 
        else { 
            return false;
        }
    },
    restore: function(newIndex){
        var d = this.restoreHistory(newIndex);
        if (d){
            this.loadImage();
            return d;
        } 
        else {
            return false;
        }
    },
    undo: function(){
        return this.restore(this.historyIndex-1);
    },
    redo: function(){
        return this.restore(this.historyIndex+1);
    }
    
};
// todo write some tests for metaimage obj


/*
    TODO where to put these cookie functions? 
*/
var writeCookie = function(name, value, hoursToExpiry){
    if (!hoursToExpiry){
        var hoursToExpiry = 12;
    }
    var exp = new Date();
	exp.setTime(exp.getTime() + (hoursToExpiry * 60 * 60 * 1000));
	var domain = document.domain.substring(document.domain.indexOf('snipshot'), document.domain.length);
	document.cookie = name + '=' + value + '; expires=' + exp.toGMTString() + '; domain=.' + domain + '; path=/';
};

var readCookie = function(name){
    if (name.indexOf('=') != name.length-1){ name = name + '='; }
	var pos = document.cookie.indexOf(name);
    if (pos != -1) {
        var start = pos + name.length;
		var end = document.cookie.indexOf(';', start);
		if (end == -1){ end = document.cookie.length; } 
		var value = document.cookie.substring(start, end);
		return value;
	}
};
