
import { ImplementationType , ImplementationOption , ImplementationBuildEntry , AnyImplementationOptions , DirectoriesInfo , ResourceConfig , RuntimeConfiguration , BuildAppConfig , ImplementationEntry , ViewBuildEntry } from '../index.d';
import { WebpackAppConfig } from '../index-webpack.d';

import path from 'path';
import fs from 'fs-extra';
import _ from 'lodash';
import fileUtils from '../utils/filepath-utils';

import appConfigUtils from '../utils/module-config-init';
import directoriesUtil from '../tools/directories-utils';
import optionUtils from '../tools/option-utils';

import logUtils from '../utils/log-utils';
const log = logUtils.log;
const warn = logUtils.warn;

//mode: 'controller' | 'helper' | 'model'
function readDir(mode: ImplementationType, dir: string, list: ImplementationBuildEntry[], options: ImplementationOption, generalOptions: AnyImplementationOptions): void {

    var files = fs.readdirSync(dir);
    var dirs = [];
    // log('read dir "'+dir+'" -> ', files);

    files.forEach(function(p){

        var absPath = path.join(dir, p);
        if(fileUtils.isDirectory(absPath)){

            dirs.push(absPath);
            return false;

        } else {

            var normalized = fileUtils.normalizePath(absPath);
            var id = path.basename(absPath, '.js');
            switch(mode){
                case 'helper':
                    if(!/Helper$/.test(id)){
                        warn('impl-utils.addFromDirectory(): invalid file-name for helper '+id+' (must have suffix "Helper"): "'+normalized+'"!');
                        id += 'Helper';
                    }
                    break;
                case 'controller':	// intentional fall through
                case 'model':				// intentional fall through
                default:
                    break;
            }

            log('impl-utils.addFromDirectory(): parsing '+mode+' implemenation '+id+' at "'+normalized+'"');//DEBUG

            var opt = options && options[id];
            if((opt && (opt.exclude || opt.file) || opt === false)){
                //-> ignore/exclude this impl. file!
                log('impl-utils.addFromDirectory(): excluding '+mode+' implemenation '+id+' at "'+normalized+'"!');
                return;//////////////////// EARLY EXIT //////////////////
            }

            var modExport = opt && opt.addModuleExport;
            if(typeof modExport === 'undefined'){
                modExport = generalOptions.addModuleExport || false;
            }

            list.push({
                id: id,
                name: toImplName(id),
                type: mode,
                file: normalized,
                addModuleExport: modExport
            });
        }
    });

    // log('read sub-dirs -> ', dirs);
    var size = dirs.length;
    if(size > 0){
        for(var i = 0; i < size; ++i){
            readDir(mode, dirs[i], list, options, generalOptions);
        }
    }
}

function addFromOptions(implMap: {[id: string]: ImplementationEntry}, list: ImplementationBuildEntry[], appRootDir: string, _generalOptions: AnyImplementationOptions): void {

    var impl: ImplementationEntry, entry: ImplementationBuildEntry;
    for(var id in implMap){

        impl = implMap[id];
        if(impl && impl.file && !impl.exclude){

            entry = _.cloneDeep(impl);
            if(!path.isAbsolute(entry.file)){
                entry.file = path.resolve(appRootDir, entry.file);
            }
            entry.file = fileUtils.normalizePath(entry.file);

            if(entry.id && entry.id !== id){
                warn('impl-utils.addFromOptions(): entry from implOptions for ID "'+id+'" has differing field id with value "'+entry.id+'", overwritting the id field with "'+id+'"!');//FIXME proper webpack error/warning
            }
            entry.id = id;

            if(!entry.name){
                entry.name = toImplName(id);
            }

            if(!/^(controller|helper|model)$/.test(entry.type)){
                warn('impl-utils.addFromOptions(): entry from implOptions for ID "'+id+'" has invalid type '+JSON.stringify(entry.type)+', overwritting the type field with "controller"!');//FIXME proper webpack error/warning
                entry.type = 'controller';
            }

            //TODO verify existence of entry.file?

            if(!contains(list, id, entry.type)){
                log('impl-utils.addFromOptions(): adding ', entry);//DEBUG
                list.push(entry)
            } else {
                warn('impl-utils.addFromOptions(): entry from implOptions for ID '+id+' already exists in implementation-list, ignoring entry!');//FIXME proper webpack error/warning
            }
        }
        else {//DEBUG
            log('impl-utils.addFromOptions(): entry for '+id+' has no file set -> ignore ', impl);//DEBU
        }
    }
}

function contains(implList: ImplementationBuildEntry[], id: string, type: ImplementationType): boolean {
    return implList.findIndex(function(item){
        return item.id === id && item.type === type;
    }) !== -1;
}

function toImplName(id: string): string {
    return id[0].toUpperCase() + id.substring(1);
}

function toAliasPath(impl: ImplementationEntry): string {
    return path.normalize(impl.file).replace(/\.js$/i, '');
}

function toAliasId(impl: ImplementationBuildEntry): string {
    return 'mmirf/'+impl.type+'/'+impl.id;//FIXME formalize IDs for loading views in webpack (?)
}

export = {

    /**
     * [description]
     * @param  {"controller" | "helper" | "model"} mode the kind of implementation (modules) that the directory contains
     * @param  {ImplOptions} options the implementation options where
     * 										options.path: REQUIRED the directory from which to add the implementations, and has the following structure:
     * 																				<directory>/<module-id-1>.js
     * 																				<directory>/<module-id-2>.js
     * 																				...
     * 										options["controllers" | "helpers" | "models"]: OPTIONAL a map of impl. IDs, i.e. {[implID: string]: ImplOption} with specific options for processing the corresponding implementation:
     *														options[types][id].exclude {Boolean}: OPTIONAL if <code>true</code>, the corresponding implementation will be excluded
     *														options[types][id].addModuleExport {Boolean|String}: OPTIONAL if <code>true</code>, the implementation will be exported,
     *																																				i.e. will be made a "requireable module"; the exported variable will be the impl. name,
     *																																				or, if addModuleExport is a String, this String will be used.
     * @param {String} appRootDir the root directory of the app (against which relative paths will be resolved)
     * @param {Array<ImplEntry>} [implList] OPTIONAL list of ImplEntry objects, to which the new entries (read from the options.directory) will be added
     * 																					if omitted, a new list will be created and returned.
     * 										ImplEntry.id {String}: the implementation ID
     * 										ImplEntry.name {String}: the implementation's name (usually the ID with capitalized first letter)
     * 										ImplEntry.type {"controller" | "helper" | "model"}: the implementation's type
     * 										ImplEntry.file {String}: the path to the implementation's file
     *										ImplEntry.addModuleExport {Boolean|String}: OPTIONAL if <code>true</code>, the implementation will be exported,
     *																																				i.e. will be made a "requireable module"; the exported variable will be the impl. name,
     *																																				or, if addModuleExport is a String, this String will be used.
     * @return {Array<ImplEntry>} the list of ImplEntry objects
     */
    implFromDir: function(mode: ImplementationType, options: AnyImplementationOptions, appRootDir: string, implList: ImplementationBuildEntry[]): ImplementationBuildEntry[] {

        var dir = options.path;
        if(!path.isAbsolute(dir)){
            dir = path.resolve(appRootDir, dir);
        }

        var list = implList || [];
        readDir(mode, dir, list, options[mode + 's'], options);

        return list;
    },
    /**
     * add implementations form options[mode+'s'] map {[implID: string]: ImplOption}, if the ImplOption has a <code>file</code> field set.
     * @param  {"controller" | "helper" | "model"} mode the kind of implementation (modules) that the directory contains
     * @param  {ImplOptions} options the implementation options with field options[mode+'s'], e.g. option.controllers for mode "controller"
     * @param {String} appRootDir the root directory of the app (against which relative paths will be resolved)
     * @param  {{Array<ImplEntry>}} [implList] OPTIONAL
     * @return {{Array<ImplEntry>}}
     */
    implFromOptions: function(mode: ImplementationType, options: AnyImplementationOptions, appRootDir: string, implList: ImplementationBuildEntry[]): ImplementationBuildEntry[] {

        var list = implList || [];
        addFromOptions(options[mode + 's'], list, appRootDir, options);

        return list;
    },

    /**
     * apply the "global" options from `options` or default values to the entries
     * from `implList` if its corresponding options-field is not explicitly specified.
     *
     * @param  {ImplOptions} options the implementation options
     * @param  {{Array<ImplEntry>}} implList
     * @return {{Array<ImplEntry>}}
     */
    applyDefaultOptions: function(options: AnyImplementationOptions, implList: ImplementationBuildEntry[] | ViewBuildEntry[]): ImplementationBuildEntry[] | ViewBuildEntry[] {

        implList.forEach(function(impl: ImplementationBuildEntry | ViewBuildEntry){
            [
                {name: 'addModuleExport', defaultValue: false}
            ].forEach(function(fieldInfo){
                optionUtils.applySetting(fieldInfo.name, impl, options, fieldInfo.defaultValue);
            });

        });

        return implList;
    },

    /**
     * add implementations to (webpack) app build configuration
     *
     * @param  {Array<ImplEntry>} implList list of ImplEntry objects:
     * 										impl.id {String}: the impl id (usually the language code, e.g. "en" or "de")
     * 										impl.file {String}: the path to the JSON impl (from which the executable impl will be created)
     * @param  {[type]} appConfig the app configuration to which the implementations will be added
     * @param  {[type]} directories the directories.json representation
     * @param  {ResourcesConfig} _resources the resources configuration
     * @param  {[type]} runtimeConfiguration the configuration.json representation
     */
    addImplementationsToAppConfig: function(implList: ImplementationBuildEntry[], appConfig: BuildAppConfig | WebpackAppConfig, directories: DirectoriesInfo, _resources: ResourceConfig, _runtimeConfiguration: RuntimeConfiguration): void {

        if(!implList || implList.length < 1){
            return;
        }

        implList.forEach(function(impl){

            var aliasId = toAliasId(impl);
            appConfigUtils.addIncludeModule(appConfig, aliasId, toAliasPath(impl));


            log('impl-utils.addImplementationsToAppConfig(): adding '+impl.type+' implemenation '+impl.id+': ['+aliasId+'] -> "'+toAliasPath(impl)+'"!');

            switch(impl.type){
                case 'controller':
                    directoriesUtil.addCtrl(directories, aliasId);
                    break;
                case 'helper':
                    directoriesUtil.addHelper(directories, aliasId);
                    break;
                case 'model':
                    directoriesUtil.addModel(directories, aliasId);
                    break;
                default:
                    warn('impl-utils.addImplementationsToAppConfig(): unknown type '+impl.type+' for implemenation '+impl.id+' at "'+impl.file+'"!');
                    break;
            }

        });
    }
};
