// narcissus.code.compress.js
// 
// Copyright (c) 2010 Guillaume Lathoud
// MIT License

/*global array from jscheck log narcissus object*/


// xxx todo narcissus.code.equal.js -> equal iff narcissus.code.compress gives the same result , then integrate in test.dev.code.compress.js

// xxx todo compress labels as well

from.req(

    'array.js'
    , 'jscheck.js'
    , 'narcissus.jsdef.js'
    , 'narcissus.tree.scope.js'
    , 'narcissus.tree.js'
    , 'narcissus.tree2code.js'
    , 'object.js'

)('narcissus.code.compress.js', function () {

    var jsdef = narcissus.jsdef
    ,   reserved_keywords = array.filter( jsdef.tokens, function (s) { return (s === s.toLowerCase()); } )
    ,   MYNAME = 'narcissus.code.compress'
    ;
    
    var me = narcissus.code.compress = function (/*string*/source_code, /*?boolean?*/do_not_force_all_new_local_varnames) {
        
        var ret = jscheck( source_code );

        ret.source_code = source_code;

        ret.scope    = narcissus.tree.scope( ret.tree );

        var translation = me.new_translation( 

            array.reduce( 
                array.map( ret.scope, function (x) { return x.locals; } )
                , array.concat )

            , narcissus.tree.detect_global_names( ret.tree )
            , do_not_force_all_new_local_varnames
        );

        // Touch the tree, one scope at a time
        
        var scope_tree_arr = array.map( ret.scope, function (x) { return x.tree; } )
        ,   scope_ind_visited = []
        ,   old_locals  = []
        ;

        narcissus.tree.walker_gen( 
                /*f*/function (tree) {

                    var scope_ind = array.indexOf( scope_tree_arr, tree );
                    if (scope_ind > -1)
                        old_locals = ret.scope[ scope_ind ].locals;
                    
                    //

                    var change = { 'name': false, 'value': false };
                    
                    if (tree.type === jsdef.IDENTIFIER) {
                        change.name  = true;
                        change.value = true;

                    } else if (tree.type === jsdef.FUNCTION) {
                        tree.params = array.map( tree.params, function (s) {

                            if (0 > array.indexOf( old_locals, s ))
                                return s;

                            var new_name = translation[ s ];
                            if (! new_name)
                                throw new Error('narcissus.code.compress: I am buggy! #0');
                            
                            return new_name;
                        });
                        change.name = true;

                    } else if (tree.type === jsdef.CATCH) {
                        if (tree.varName) {
                            var new_name = translation[tree.varName];
                            if (new_name)
                                tree.varName = new_name;
                        }
                    } else {
                        return;
                    }

                    object.map( object.filter( change ), function (v, k) {
                        
                        if (!tree[k])
                            return;
                        
                        var ind = array.indexOf( old_locals, tree[k] );
                        if (ind < 0)
                            return;

                        var new_name = translation[ tree[k] ];
                        if (! new_name)
                            throw new Error('narcissus.code.compress: I am buggy! #1');

                        tree[k] = new_name;                    
                    });

                },
                /*stacker*/function (tree, action) {
                    
                    var scope_ind = array.indexOf( scope_tree_arr, tree );
                    if (scope_ind < 0)
                        return;

                    if (action === 'push') {
                        old_locals = ret.scope[ scope_ind ].locals;
                        
                    } else if (action === 'pop') {
                        old_locals = ret.scope[ scope_ind ].locals_parent;
                        
                    } else {
                        throw new Error(MYNAME + ': stacker: buggy!');
                    }
                    
                },
                /*filter*/function (tree, a) {

                    if (tree.type === jsdef.DOT) {
                        if (a > 0)
                            return false;
                    }

                    if (tree.type === jsdef.PROPERTY_INIT) {
                        if (a < 1) 
                            return false;
                    }

                    return true;
                    
                }
        
        )( ret.tree );
        
        ret.compressed_code = narcissus.tree2code( ret.tree );
        
        return ret;
    };

    me.new_translation = function (/*array of string*/arr, /*?array of string, typically list of globals?*/excluded_arr
        , /*?boolean?*/do_not_force_all_new_local_varnames) {

        if (!excluded_arr)
            log.warn( MYNAME + ".new_translation(): you did not give any `excluded_arr` - did you forget globals?" );
        
        excluded_arr = reserved_keywords.concat( excluded_arr || [] );
        
        // Guarantee unicity of the mapping (bijective mapping)
        // Note: `array.unique` is fully deterministic,
        // so `new_translation` is also fully deterministic.
        var arr_unique = array.unique( arr );
        
        if (!do_not_force_all_new_local_varnames) {
            // Guarantee that the new variable names (if any) are
            // different from the old ones.
            excluded_arr = excluded_arr.concat( arr_unique );
        }
        
        return object( array.zip( arr_unique, 
                                  me.new_var_names( arr_unique, excluded_arr ) ) );
    };
    
    me.new_var_names = function (/*array of string*/arr, /*array of string*/excluded_arr) {
        
        var n     = arr.length
        ,   ind   = array.permute( array.ind( n ) )
        ,   ret   = new Array( arr.length )
        ;

        for (var a = ret.length, b = 0; a--; ) {
            
            var varname = ind[a]
            ,   varname_length = varname.length
            ,   varname_excluded = (-1 < array.indexOf( excluded_arr, varname ))
            ,   s

            do {
                
                s = b.toString(36);
                b++;              
                
                if ((!varname_excluded) && (s.length > varname_length)) {
                    s = varname;
                    break;
                }
                
            } while (/^\d/.test( s ) || (-1 < array.indexOf( excluded_arr, s ) ) );
            
            ret[ ind[a] ] = s;
        }
        
        return ret;
    };

});
