
import { CompilerCallback , StateCompilerOptions } from '../index.d';

import scxml from '@scion-scxml/scxml';
import { fixRawCompileInjectDataModule } from './scxml-fix-datamodel-declaration';

//err.tagname+'>: '+err.reason + '\n  at '+file+':'+err.line+':'+err.column
type ScxmlError = {
    tagname: string;
    reason: string;
    line: number;
    column: number;
};

function isScxmlError(err: Error | ScxmlError): err is ScxmlError {
    return !!( err && (err as ScxmlError).tagname && (err as ScxmlError).reason);
}

//FIXME do fix datamodel declaration in generated scxml model FIXES @scion-scxml/scxml@4.3.25
//TODO remove when not neccessary any more
fixRawCompileInjectDataModule(scxml);

// import logUtils from '../utils/log-utils';
// const log = logUtils.log;

const MODULE_CODE_PREFIX = 'var ScxmlModel = ';

//TODO support optional prepare() arguments? ->
/**
 * Prepares an scxml model for execution by binding it to an execution context
 * @param  {prepareCallback} cb  Callback to execute with the prepared model or an error
 *   The prepared model is a function to be passed into a SCION StateChart object
 * @param  {vm.Context | object} [executionContext] The execution context  (e.g. v8 VM sandbox).
 *   If a vm.Context object is provided, then the compiled SCXML module code is evaluated in that context.
 *   Otherwise, if a regular object is provided, then the given object is "contextified", which means that it is passed to vm.createContext to create a new execution context. Object keys are exposed as global variables on the new context in which the generated SCXML module code is evaluated.
 * @param  {HostContext} [hostContext]  Context provided by the interpreter host
 */

const MODULE_CODE_SUFFIX = ';\n' +
                        'ScxmlModel.prepare = function(prepareCallback, executionContext, hostContext) {' + //TODO support optional arguments executionContext & hostContext?
                            //NOTE use setTimeout() for simulating async execution:
                            'if(!prepareCallback) return; setTimeout(function(){prepareCallback(null, ScxmlModel)}, 0);' +
                        '};\nmodule.exports = ScxmlModel;';

const AMD_PREFIX = 'define(["module"],  function(module){\n';
const AMD_SUFFIX = '\n});';
const STRICT_MODE = '"use strict";\n';

function toError(errList: ScxmlError | Error | Array<ScxmlError|Error>, file: string, _map: any, _meta: any): Error {
    if(Array.isArray(errList)){
        var sb = [];
        var len = errList.length;
        errList.forEach(function(err, index){
            if(isScxmlError(err)){
                sb.push('SCXML ERROR ('+(index+1)+') in <'+err.tagname+'>: '+err.reason + '\n  at '+file+':'+err.line+':'+err.column);
            } else {
                sb.push(err && err.stack? err.stack : 'SCXML ERROR ('+(index+1)+') in file '+file+': '+err);
            }
        });
        return new Error('encountered '+len+' error'+(len === 1? '' : 's')+' while parsing SCXML:\n' + sb.join('\n'));
    }
    return errList instanceof Error? errList : new Error('SCXML ERROR in file '+file+': '+errList);
}

/**
 * compile an SCXML file to exectuable scion statechart model
 *
 * @param  {string} content the SCXML definition as string
 * @param  {string} scxmlFile the path of the SCXML file (for debugging/error information)
 * @param  {ScxmlLoadOptions} options the ScxmlLoadOptions with property mapping (list of ScxmlOptions)
 * @param  {Function} callback the callback when SCXML compilation has been completed: callback(error | null, compiledStatechart, map, meta)
 * @param  {any} [_map] source mapping (unused)
 * @param  {any} [_meta] meta data (unused)
 */
function compile(content: string, scxmlFile: string, options: StateCompilerOptions, callback: CompilerCallback, _map: any, _meta: any): void {

    // log('mmir-scxml-loader: resource -> ', scxmlFile);//DEBU
    const i = options.mapping.findIndex(function(g){
        return g.file === scxmlFile;
    });
    const scxmlInfo = options.mapping[i];

    // log('mmir-scxml-loader: options for resource -> ', scxmlInfo);//DEBU

    if(!scxmlInfo || !scxmlInfo.id){
        let error: string;
        if(options.mapping.length === 0){
            error = 'failed to parse SCXML definition: empty list for SCXML settings [{id: "the ID", file: "the file path", ...}, ...]';
        }
        else if(i === -1 || !scxmlInfo){
            error = 'failed to parse SCXML definition: could not find settings for SCXML in SCXML-settings list: '+JSON.stringify(options.mapping);
        } else if(!scxmlInfo.id){
            error = 'failed to parse SCXML definition: missing field id for SCXML: '+JSON.stringify(scxmlInfo);
        } else {
            error = 'failed to parse SCXML definition: invalid SCXML settings in list: '+JSON.stringify(options.mapping);
        }
        callback(error, null, _map, _meta);
        return;/////////////// EARLY EXIT /////////////////
    }

    // log('mmir-scxml-loader: resource ID at '+i+' -> ', scxmlInfo.id);//, ', parsing content: ', content);//DEBU

    //TODO ID optional settable via loader options?
    const id = scxmlInfo.id;
    const ignoreRuntimeErrors = typeof scxmlInfo.ignoreErrors === 'boolean'? scxmlInfo.ignoreErrors : (options.config && options.config.ignoreErrors === true);
    // log('SCXML parsing, ignoreErrors -> ', ignoreRuntimeErrors, ', options.ignoreErrors: ', options.config, scxmlInfo)//DEBU
    const moduleType = scxmlInfo.moduleType? scxmlInfo.moduleType : (options.config && options.config.moduleType);
    //log('SCXML parsing, moduleType -> ', moduleType, ', options.config: ', options.config, scxmlInfo)//DEBU
    const strictMode = typeof scxmlInfo.strict === 'boolean'? scxmlInfo.strict : (options.config && typeof options.config.strict === 'boolean'? options.config.strict : true);
    //log('SCXML parsing, strictMode -> ', strictMode, ', options.config: ', options.config, scxmlInfo)//DEBU

    scxml.documentStringToModel(id, content, function(err, model){

        if(err){
            callback(toError(err, scxmlFile, _map, _meta));
            return;/////////////// EARLY EXIT /////////////////
        }

        // log('mmir-scxml-loader: successfully created model factory for '+id+'.');//DEBU

        model.prepare(function(err, fnModel) {

                if(err){
                    callback(toError(err, scxmlFile, _map, _meta));
                    return;/////////////// EARLY EXIT /////////////////
                }

                let scxmlCode: string = MODULE_CODE_PREFIX + fnModel.toString() + MODULE_CODE_SUFFIX;

                if(moduleType === 'amd'){
                    scxmlCode = AMD_PREFIX + (strictMode? STRICT_MODE : '') + scxmlCode + AMD_SUFFIX;
                }

                // log('mmir-scxml-loader: created model for '+id+'.');//DEBU

                callback(null, scxmlCode, _map, _meta);
        });

    }, {reportAllErrors: !ignoreRuntimeErrors})

    return;
};

export = {
    compile: compile,
    // fileVersion: stateEngineFactory.getFileVersion() //TODO impl. file-version for compiled SCXML models
}
