import { Attribute } from './attribute.ts';
import { createAttribute } from './typespec.ts';
import { defaults, isValidJSON, transform } from '../tools.ts'
import { log } from '../tools.ts'

interface ICompiled {
    _attributes : AttrSpecs
    Attributes : CloneCtor
    properties : PropertyDescriptorMap
    forEach? : ForEach
    defaults : Defaults
    _toJSON : ToJSON
    _parse : Parse
}

interface AttrSpecs {
    [ key : string ] : Attribute
}

interface AttrValues {
    [ key : string ] : any
}

type CloneCtor = new ( x : AttrValues ) => AttrValues
type ForEach   = ( obj : {}, iteratee : ( val : any, key? : string, spec? : Attribute ) => void ) => void;
type Defaults  = ( attrs? : {} ) => {}
type Parse     = ( data : any ) => any;
type ToJSON    = () => any;

// Compile attributes spec
export function compile( rawSpecs : {}, baseAttributes : AttrSpecs ) : ICompiled {
    const myAttributes = transform( <AttrSpecs>{}, rawSpecs, createAttribute ),
          allAttributes = defaults( <AttrSpecs>{}, myAttributes, baseAttributes ),
          Attributes = createCloneCtor( allAttributes ),
          mixin : ICompiled = {
            Attributes : Attributes,
            _attributes : new Attributes( allAttributes ),
            properties : transform( <PropertyDescriptorMap>{}, myAttributes, x => x.createPropertyDescriptor() ),
            defaults : createDefaults( allAttributes ),
            _toJSON : createToJSON( allAttributes ), // <- TODO: profile and check if there is any real benefit. I doubt it. 
            _parse : createParse( myAttributes, allAttributes )
         }; 

    // Enable optimized forEach if warnings are disabled.
    if( log.level > 0 ){
        mixin.forEach = createForEach( allAttributes );
    }

    return mixin;
}

export function createForEach( attrSpecs : AttrSpecs ) : ForEach {
    let statements = [ 'var v, _a=this._attributes;' ];

    for( let name in attrSpecs ){
        statements.push( `( v = a.${name} ) === void 0 || f( v, "${name}", _a.${name} );` );
    }

    return <ForEach> new Function( 'a', 'f', statements.join( '' ) );
}

export function createCloneCtor( attrSpecs : AttrSpecs ) : CloneCtor {
    var statements = [];

    for( let name in attrSpecs ){
        statements.push( `this.${name} = x.${name};` );
    }

    var CloneCtor = new Function( "x", statements.join( '' ) );
    CloneCtor.prototype = Object.prototype;
    return <CloneCtor> CloneCtor;
}

// Create optimized model.defaults( attrs, options ) function
function createDefaults( attrSpecs : AttrSpecs ) : Defaults {
    let assign_f = ['var v;'], create_f = [];

    function appendExpr( name, expr ){
        assign_f.push( `this.${name} = ( v = a.${name} ) === void 0 ? ${expr} : v;` );
        create_f.push( `this.${name} = ${expr};` );
    }

    // Compile optimized constructor function for efficient deep copy of JSON literals in defaults.
    for( let name in attrSpecs ){
        const attrSpec = attrSpecs[ name ],
              { value, type } = attrSpec;

        if( value === void 0 && type ){
            // if type with no value is given, create an empty object
            appendExpr( name, `i.${name}.create()` );//TODO: consider adding owner reference
        }
        else{
            // If value is given, type casting logic will do the job later, converting value to the proper type.
            if( isValidJSON( value ) ){
                // JSON literals must be deep copied.
                appendExpr( name, JSON.stringify( value ) );
            }
            else if( value === void 0 ){
                // handle undefined value separately. Usual case for model ids.
                appendExpr( name, 'void 0' );
            }
            else{
                // otherwise, copy value by reference.
                appendExpr( name, `i.${name}.value` );
            }
        }
    }

    const CreateDefaults : any = new Function( 'i', create_f.join( '' ) ),
          AssignDefaults : any = new Function( 'a', 'i', assign_f.join( '' ) );

    CreateDefaults.prototype = AssignDefaults.prototype = Object.prototype;

    // Create model.defaults( attrs, options ) function
    // 'attrs' will override default values, options will be passed to nested backbone types
    return function( attrs? : {} ){ //TODO: Consider removing of the CreateDefaults. Currently is not used. May be used in Record costructor, though.
        return attrs ? new AssignDefaults( attrs, this._attributes ) : new CreateDefaults( this._attributes );
    }
}

function createParse( allAttrSpecs : AttrSpecs, attrSpecs : AttrSpecs ) : Parse {
    var statements = [ 'var a=this._attributes;' ],
        create     = false;

    for( let name in allAttrSpecs ){
        const local = attrSpecs[ name ];

        // Is there any 'parse' option in local model definition?
        if( local && local.parse ) create = true;

        // Add statement for each attribute with 'parse' option.
        if( allAttrSpecs[ name ].parse ){
            const s = `r.${name} === void 0 ||( r.${name} = a.${name}.parse.call( this, r.${name}, "${name}") );`;
            statements.push( s );
        }
    }

    if( create ){
        statements.push( 'return r;' );
        return <any> new Function( 'r', statements.join( '' ) );
    }
 }

function createToJSON( attrSpecs : AttrSpecs ) : ToJSON {
    let statements = [ `var json = {},v=this.attributes,a=this._attributes;` ];

    for( let key in attrSpecs ){
        const toJSON = attrSpecs[ key ].toJSON;

        if( toJSON ){
            statements.push( `json.${key} = a.${key}.toJSON.call( this, v.${ key }, '${key}' );` );
        }
    }

    statements.push( `return json;` );

    return <any> new Function( statements.join( '' ) );
}
