"use strict";

var Fashion = require('./export/Base.js'),
    Base = Fashion.Base;

var Context = require('./Context.js');
var Preprocessor = require('./Preprocessor.js');
var Transpiler = require('./Transpiler.js');
var Env = require('./Env.js');
var SassFile = require('./SassFile.js');

var Parser = require('./parse/Parser.js');
var Scanner = require('./parse/Scanner.js');

class Builder extends Base {

    constructor(config) {
        super(config);

        this.scripts = {};
        Fashion.lastBuilder = this;
        this.context = new Context(this.context);

        this.context.libraries = this.context.libraries || {
                compass: "../lib/compass/stylesheets/",
                blueprint: "../lib/blueprint/stylesheets/"
            };
    }

    getContext() {
        return this.context;
    }

    compile(scss, file) {
        var context = this.getContext(),
            jsCode = context.convert(scss, file),
            fn = context.compile(jsCode),
            css = fn(context.runtime, {}, context.preprocessor.getDynamicsMap());
        return css;
    }


    createSetters(vars) {
        var setters = '',
            varName;
        if (typeof vars === 'string') {
            setters = vars;
        } else {
            for (varName in vars) {
                setters += varName + ": dynamic(" + vars[varName] + ");\n";
            }
        }
        return setters;
    }

    createVarsScope(vars) {
        var context = this.context,
            parser = new Parser(),
            preprocessor = new Preprocessor({
                runtime: context.runtime
            }),
            newContext = new Context({
                runtime: context.runtime,
                preprocessor: preprocessor
            }),
            setters = this.createSetters(vars),
            allVariables = Fashion.chain(context.getVariables()),
            setterAst, setterVariables, sortedAst, settersCode, settersFunc,
            newGlobals;


        setterAst = parser.parse(setters);
        preprocessor.preprocess(setterAst, true);
        preprocessor.loadPreprocessorCache(context.preprocessor);

        setterVariables = preprocessor.getVariables();
        Fashion.apply(allVariables, setterVariables);

        preprocessor.variables = allVariables;
        context.dynamicsMap = preprocessor.getDynamicsMap();
        sortedAst = preprocessor.getSortedDynamicAstNodes();

        settersCode = newContext.convert(sortedAst);
        newContext.runtime.setCaches(context.transpiler);
        settersFunc = newContext.compile(settersCode);

        // execute the generated fn to setup a global scope that
        // has all the parsed values;
        settersFunc(context.runtime, null, context.dynamicsMap);
        newGlobals = context.runtime.getGlobalScope();
        return newGlobals;
    }

    rebuildCss(vars) {
        var context = this.context,
            func = context.getFunc(),
            css;

        // now, re-executed the cached fn using the provided setters
        // as initial state
        css = func(context.runtime, vars, context.dynamicsMap);
        return css;
    }

    /**
        File can be of type string or object with following parameters:
        {
            path: '',                   // the path to the scss file to build
            split: 4095,                // the split threshold
            compress: false,            // whether to copress output
            variables: {                // global variable overrides
                '$base-color': ''
            }
        }
     * 
     */
    build(file, callback) {
        var me = this,
            context, sassFile, split, vars;

        if (typeof file !== 'string') {
            split = file.split;
            me.compressed = file.compress;
            vars = file.variables;
            file = file.path;
        }

        // reset some calculated path variables
        this._sassFileUiPath = null;
        this._sassFileSavePath = null;

        function onError(error) {
            Fashion.log("Build error for " + file);
            var message = (error.message || error) + "";
            callback && callback(["/* " + message + " */"], error);
        }
        
        context = me.getContext();
        try {
            sassFile = me.getSassFile(file);
            sassFile.invalidate();
            sassFile.onReady(function () {
                Fashion.log("Fashion build starting for " + file);

                try {
                    var ast = sassFile.getExpandedAst(),
                        converted = context.convert(ast),
                        func = context.compile(converted),
                        css = func(),
                        dynamics = context.dynamicsMap || context.preprocessor.getDynamicsMap();

                    if (vars) {
                        var scope = me.createVarsScope(vars);
                        context.dynamicsMap = dynamics;
                        css = me.rebuildCss(scope);
                    }

                    css.getText(function (cssContent, exportFn) {
                            Fashion.log("Fashion build complete for " + file);
                            callback && callback(cssContent, null, exportFn);
                        },
                        me.compressed,
                        me.indent,
                        me.skipComments,
                        split);

                } catch (error) {
                    onError(error);
                }
            });
        }
        catch (error) {
            onError(error);
        }
    }

    getSassFile(path, relPath, origSource, importer) {
        var scripts = this.scripts,
            isSaveFile = false,
            isUiFile = false,
            sassNamespace = this.sassNamespace,
            sassFileSavePath, sassFileUiPath, script, className;


        path = this.getSassFilePath(path, relPath);
        script = scripts[path];

        if (this.varSavePath) {
            sassFileSavePath = this._sassFileSavePath || this.getSassFilePath(this.varSavePath);
            this._sassFileSavePath = sassFileSavePath;
            isSaveFile = (path === sassFileSavePath);
        }

        if (this.srcSavePath) {
            sassFileUiPath = this._sassFileUiPath || this.getSassFilePath(this.srcSavePath);
            this._sassFileUiPath = sassFileUiPath;
            isUiFile = (path.indexOf(sassFileUiPath) === 0);
        }

        if (isUiFile) {
            className = path.replace(sassFileUiPath, '')
                .replace(/^\//g, '')
                .replace(/\//g, ".")
                .replace(/\.scss$/g, '');

            if (sassNamespace) {
                className = sassNamespace + '.' + className;
            }
        }

        if (!script) {
            script = new SassFile({
                path: path,
                builder: this,
                originalSource: origSource,
                importer: importer,
                isSaveFile: isSaveFile,
                isUiFile: isUiFile,
                jsClassName: className
            });
            scripts[path] = script;
        }
        return script;
    }

    getFsMap() {
        var map = this.fsMap,
            scripts = this.scripts,
            keys = Object.keys(scripts),
            len = keys.length,
            key, k, path, parts, p, plen, part, level, next;

        if (!map) {
            map = {
                files: {},
                root: true
            };
            this.fsMap = map;
        }

        for (k = 0; k < len; k++) {
            key = keys[k];
            path = key.replace(/\\/g, '/');
            parts = path.split('/');
            plen = parts.length;
            level = map;
            for (p = 0; p < plen - 1; p++) {
                part = parts[p];
                next = level.files[part];
                if (!next) {
                    next = {
                        fullName: level.fullName ? [level.fullName, part].join('/') : part,
                        files: {},
                        isDir: true
                    };
                    level.files[part] = next;
                }
                level = next;
            }
            part = parts[plen - 1];
            next = level.files[part];
            if (!next) {
                next = {
                    fullName: level.fullName ? [level.fullName, part].join('/') : part,
                    key: key,
                    isFile: true,
                    sassFile: scripts[key]
                };
                level.files[part] = next;
            }
            level.hasFile = true;
        }

        return map;
    }

    getSassFilePath(path, relPath) {
        if (relPath) {
            path = this.getPath(path, relPath);
        }

        path = path.replace(/\\/g, "/");
        path = this.resolveUrl(path);
        return path;
    }

    getParser() {
        return new Parser();
    }

    getPath(baseFile, relPath) {
        if (relPath) {
            var separatorIndex = relPath.indexOf('/'),
                libraryPaths = this.context.libraries,
                root, libpath;

            if (separatorIndex !== 0) { // not an absolute path
                if (separatorIndex === -1) {
                    // no path separator found e.g. "@import 'compass';"
                    root = relPath;
                } else {
                    // path separator found e.g. "@import 'compass/css3"
                    root = relPath.substring(0,
                        separatorIndex !== -1
                            ? separatorIndex
                            : relPath.length);
                }

                libpath = libraryPaths[root];

                if (libpath) {
                    return this.calcPath(libpath, relPath);
                }
            }
        }

        return this.calcPath(baseFile, relPath);
    }

    calcPath(baseFile, relPath) {
        var sep = baseFile.lastIndexOf("/"),
            path;

        if (sep > -1) {
            path = baseFile.substring(0, sep + 1) + relPath;
        } else {
            path = baseFile + "/" + relPath;
        }

        if (path.indexOf(".scss") === -1 && path.indexOf(".js") === -1) {
            path = path + ".scss";
        }

        return path;
    }

    getResolverEl() {
        if (!this.resolverEl) {
            this.resolverEl = document.createElement("a");
        }
        return this.resolverEl;
    }

    getCanonicalPath(path) {
        var parts = path.split('/'),
            out = [],
            part, p;
        for (p = 0; p < parts.length; p++) {
            part = parts[p];
            if (part == '.') {
                continue;
            } else if (part == '..') {
                if (out.length === 0) {
                    Fashion.raise("bad path for getCanonicalPath : " + path);
                }
                out.pop();
            } else {
                out.push(part);
            }
        }
        return out.join('/');
    }

    resolveUrl(path) {
        // firefox won't automatically convert \ chars to / chars
        // so need to do that here
        path = path.replace(/\\/g, "/");

        if (Env.isBrowser) {
            var resolverEl = this.getResolverEl();
            resolverEl.href = path;
            return resolverEl.href;
        } else {
            path = this.getCanonicalPath(path);
        }
        return path;
    }

    createStyleEl(href, content, before) {
        var head = document.getElementsByTagName('head')[0],
            base = document.createElement('base'),
            styleEl, ieMode;

        Fashion.log("Using base href : " + href);
        base.href = href;

        if (head.firstChild) {
            head.insertBefore(base, head.firstChild);
        } else {
            head.appendChild(base);
        }

        // IE hack to force re-processing of the href
        base.href = base.href;

        styleEl = document.createElement("style");
        styleEl.type = 'text/css';

        ieMode = ('styleSheet' in styleEl);

        if (ieMode) {
            if (before) {
                head.insertBefore(styleEl, before);
            } else {
                head.appendChild(styleEl);
            }
            styleEl.styleSheet.cssText = content;
        } else {
            styleEl.textContent = content;
            if (before) {
                head.insertBefore(styleEl, before);
            } else {
                head.appendChild(styleEl);
            }
        }
        head.removeChild(base);
        return styleEl
    }

    injectCss(cssPath, cssContent) {
        this.lastCssPath = cssPath;

        if (!Array.isArray(cssContent)) {
            cssContent = [cssContent];
        }

        cssPath = this.resolveUrl(cssPath);

        var me = this,
            currEls = me.styleEls || [],
            href = cssPath.substring(0, cssPath.lastIndexOf("/") + 1);

        me.styleEls = [];

        cssContent.forEach((content, idx) => {
            content += "\n/*# sourceURL=" + cssPath + "_" + idx + " */";
            var before = (currEls.length && currEls[0]) || null,
                styleEl = this.createStyleEl(href, content, before);
            me.styleEls.push(styleEl);
        });

        var head = document.getElementsByTagName('head')[0];

        currEls.forEach((el) => {
            head.removeChild(el);
        });

    }
}

Fashion.apply(Builder.prototype, {
    context: undefined,
    varSavePath: undefined,
    srcSavePath: undefined,
    sassNamespace: undefined
});

module.exports = Builder;