import Condition from "./Condition";
import InstructionStyle from "./InstructionStyle";
import MetaStyle from "./MetaStyle";
import PointStyle from "./PointStyle";
import ShapeStyle from "./ShapeStyle";
import ShieldStyle from "./ShieldStyle";
import StyleChooser from "./StyleChooser";
import StyleList from "./StyleList";
import TextStyle from "./TextStyle";

/**
 * RuleSet
 * A whole MapCSS definition, as a set of StyleChoosers
 * 
 * Based on Overpass Turbo implementation
 * @see https://github.com/tyrasd/overpass-turbo
 */
var MapCSSRuleSet = function() {};

MapCSSRuleSet.prototype = {

    choosers: [],		// list of StyleChoosers

    getStyles: function(entity, tags, zoom) {
        // summary:		Find the styles for a given entity.
        var sl=new StyleList();
        for (var i in this.choosers) {
            this.choosers[i].updateStyles(entity, tags, sl, zoom);
        }
        return sl;	// StyleList
    },

    /*loadFromCSS: function(url, callback) {
        // summary:		Load a MapCSS file from a URL, then throw it at the parser when it's loaded.
        this.callback = callback;
        $.ajax({
            url: url,
            success: _.bind(this.parseCSS, this)
        });
    },*/

    parseCSS: function(css) {
        // summary:		Parse a CSS document into a set of StyleChoosers.
        var previous=0;								// what was the previous CSS word?
        var sc = new StyleChooser();	// currently being assembled
        this.choosers = [];
        css = css.replace(/[\r\n]/g,"");				// strip linebreaks because JavaScript doesn't have the /s modifier

        var o = {};
        while (css.length>0) {

            // CSS comment
            if ((o=this.COMMENT.exec(css))) {
                css=css.replace(this.COMMENT,'');

                // Whitespace (probably only at beginning of file)
            } else if ((o=this.WHITESPACE.exec(css))) {
                css=css.replace(this.WHITESPACE,'');

                // Class - .motorway, .builtup, :hover
            } else if ((o=this.CLASS.exec(css))) {
                if (previous===this.oDECLARATION) { this.saveChooser(sc); sc=new StyleChooser(); }

                css=css.replace(this.CLASS,'');
                sc.currentChain().addConditionToLast((new Condition()).init('set',o[1]));
                previous=this.oCONDITION;

                // Not class - !.motorway, !.builtup, !:hover
            } else if ((o=this.NOT_CLASS.exec(css))) {
                if (previous===this.oDECLARATION) { this.saveChooser(sc); sc=new StyleChooser(); }

                css=css.replace(this.NOT_CLASS,'');
                sc.currentChain().addConditionToLast((new Condition()).init('unset',o[1]));
                previous=this.oCONDITION;

                // Zoom
            } else if ((o=this.ZOOM.exec(css))) {
                if (previous!==this.oOBJECT && previous!==this.oCONDITION) { sc.currentChain().addRule(); }

                css=css.replace(this.ZOOM,'');
                var z=this.parseZoom(o[1]);
                sc.currentChain().addZoomToLast(z[0],z[1]);
                sc.zoomSpecific=true;
                previous=this.oZOOM;

                // Grouping - just a comma
            } else if ((o=this.GROUP.exec(css))) {
                css=css.replace(this.GROUP,'');
                sc.newRuleChain();
                previous=this.oGROUP;

                // Condition - [highway=primary]
            } else if ((o=this.CONDITION.exec(css))) {
                if (previous===this.oDECLARATION) { this.saveChooser(sc); sc=new StyleChooser(); }
                if (previous!==this.oOBJECT && previous!==this.oZOOM && previous!==this.oCONDITION) { sc.currentChain().addRule(); }
                css=css.replace(this.CONDITION,'');
                sc.currentChain().addConditionToLast(this.parseCondition(o[1]));
                previous=this.oCONDITION;

                // Object - way, node, relation
            } else if ((o=this.OBJECT.exec(css))) {
                // TODO: raise error if object is none of node|way|relation|line|area|canvas|* ?
                if (previous===this.oDECLARATION) { this.saveChooser(sc); sc=new StyleChooser(); }

                css=css.replace(this.OBJECT,'');
                sc.currentChain().addRule(o[1]);
                previous=this.oOBJECT;

                // Subpart - ::centreline
            } else if ((o=this.SUBPART.exec(css))) {
                if (previous===this.oDECLARATION) { this.saveChooser(sc); sc=new StyleChooser(); }
                css=css.replace(this.SUBPART,'');
                sc.currentChain().setSubpart(o[1]);
                previous=this.oSUBPART;

                // Declaration - {...}
            } else if ((o=this.DECLARATION.exec(css))) {
                css=css.replace(this.DECLARATION,'');
                sc.addStyles(this.parseDeclaration(o[1]));
                previous=this.oDECLARATION;

                // Unknown pattern
            } else if ((o=this.UNKNOWN.exec(css))) {
                css=css.replace(this.UNKNOWN,'');
                // TODO: own error class
                throw new Error("Error while parsing MapCSS at \""+o[1]+(css.length>38?css.substr(0,36)+"...":css)+"\"");
                // console.log("unknown: "+o[1]);
            } else {
                // console.log("choked on "+css);
                throw new Error("MapCSS parsing choked on "+css);
            }
        }
        if (previous===this.oDECLARATION) { this.saveChooser(sc); sc=new StyleChooser(); }
        if (this.callback) { this.callback(); }
    },

    saveChooser:function(sc) {
        this.choosers.push(sc);
    },

    parseDeclaration:function(s) {
        var styles=[];
        var t={};
        var o={};
        var k, v;

        // Create styles
        var ss = new ShapeStyle();
        var ps = new PointStyle();
        var ts = new TextStyle();
        var hs = new ShieldStyle();
        var xs = new InstructionStyle();
        var ms = new MetaStyle();

        var r=s.split(';');
        var isEval={};
        for (var i in r) {
            var a=r[i];
            if ((o=this.ASSIGNMENT_EVAL.exec(a)))   { k=o[1].replace(this.DASH,'_'); t[k]=o[2]; isEval[k]=true; }
            else if ((o=this.ASSIGNMENT.exec(a)))   { k=o[1].replace(this.DASH,'_'); t[k]=o[2]; }
            else if ((o=this.SET_TAG_EVAL.exec(a))) { } // xs.addSetTag(o[1],this.saveEval(o[2]));
            else if ((o=this.SET_TAG.exec(a)))      { xs.addSetTag(o[1],o[2]); }
            else if ((o=this.SET_TAG_TRUE.exec(a))) { xs.addSetTag(o[1],true); }
            else if ((o=this.EXIT.exec(a)))         { xs.setPropertyFromString('breaker',true); }
        }

        /*// Find sublayer
        // TODO: Why the renaming z_index -> sublayer? 
        //       This hardcoded variable isn't used anywhere.
        //       Looks like this was replaced by the "subparts"...
        var sub=5;
        if (t['z_index']) { sub=Number(t['z_index']); delete t['z_index']; }
        ss.sublayer=ps.sublayer=ts.sublayer=hs.sublayer=sub;
        xs.sublayer=10;*/

        /*// Find "interactive" property - it's true unless explicitly set false
        var inter=true;
        if (t['interactive']) { inter=t['interactive'].match(this.FALSE) ? false : true; delete t['interactive']; }
        ss.interactive=ps.interactive=ts.interactive=hs.interactive=xs.interactive=inter;*/

        // Munge special values
        // (we should stop doing this and do it in the style instead)
        // TODO: font_bold??? wtf? -> support propper MapCSS properites!
        if (t['font_weight']    ) { t['font_bold'  ]    = t['font_weight'    ].match(this.BOLD  )    ? true : false; delete t['font_weight']; }
        if (t['font_style']     ) { t['font_italic']    = t['font_style'     ].match(this.ITALIC)    ? true : false; delete t['font_style']; }
        if (t['text_decoration']) { t['font_underline'] = t['text_decoration'].match(this.UNDERLINE) ? true : false; delete t['text_decoration']; }
        if (t['text_position']  ) { t['text_center']    = t['text_position'  ].match(this.CENTER)    ? true : false; delete t['text_position']; }
        if (t['text_transform']) {
            if (t['text_transform'].match(this.CAPS)) { t['font_caps']=true; } else { t['font_caps']=false; }
            delete t['text_transform'];
        }

        // Assign each property to the appropriate style
        for (a in t) {
            // Parse properties
            v = t[a];

            // Set in styles
            if      (ss.has(a)) { ss.setPropertyFromString(a,v,isEval[a]); }
            else if (ps.has(a)) { ps.setPropertyFromString(a,v,isEval[a]); }
            else if (ts.has(a)) { ts.setPropertyFromString(a,v,isEval[a]); }
            else if (hs.has(a)) { hs.setPropertyFromString(a,v,isEval[a]); }
            else if (ms.has(a)) { ms.setPropertyFromString(a,v,isEval[a]); }
            else {
                // console.log(a+" not found");
            }
        }

        // Add each style to list
        if (ss.edited) { styles.push(ss); }
        if (ps.edited) { styles.push(ps); }
        if (ts.edited) { styles.push(ts); }
        if (hs.edited) { styles.push(hs); }
        if (xs.edited) { styles.push(xs); }
        if (ms.edited) { styles.push(ms); }
        return styles;
    },

    parseZoom:function(s) {
        var o={};
        var maxscale =  999; // TODO: hardcoded
        var minscale = -999; // TODO: hardcoded
        if      ((o=this.ZOOM_MINMAX.exec(s))) { return [o[1],o[2]]; }
        else if ((o=this.ZOOM_MIN.exec(s)   )) { return [o[1], maxscale]; }
        else if ((o=this.ZOOM_MAX.exec(s)   )) { return [minscale, o[1]]; }
        else if ((o=this.ZOOM_SINGLE.exec(s))) { return [o[1],o[1]]; }
        return null;
    },

    parseCondition:function(s) {
        var o={};
        if      ((o=this.CONDITION_TRUE.exec(s)))  { return (new Condition()).init('true'	,o[1]); }
        else if ((o=this.CONDITION_FALSE.exec(s))) { return (new Condition()).init('false',o[1]); }
        else if ((o=this.CONDITION_SET.exec(s)))   { return (new Condition()).init('set'	,o[1]); }
        else if ((o=this.CONDITION_UNSET.exec(s))) { return (new Condition()).init('unset',o[1]); }
        else if ((o=this.CONDITION_NE.exec(s)))    { return (new Condition()).init('ne'	,o[1],o[2]); }
        else if ((o=this.CONDITION_GT.exec(s)))    { return (new Condition()).init('>'	,o[1],o[2]); }
        else if ((o=this.CONDITION_GE.exec(s)))    { return (new Condition()).init('>='	,o[1],o[2]); }
        else if ((o=this.CONDITION_LT.exec(s)))    { return (new Condition()).init('<'	,o[1],o[2]); }
        else if ((o=this.CONDITION_LE.exec(s)))    { return (new Condition()).init('<='	,o[1],o[2]); }
        else if ((o=this.CONDITION_REGEX.exec(s))) { return (new Condition()).init('regex',o[1],o[2]); }
        else if ((o=this.CONDITION_EQ.exec(s)))    { return (new Condition()).init('eq'	,o[1],o[2]); }
        return null;
    },

    parseCSSColor:function(colorStr) {
        // todo: this should be done at user (=style consumer) side (if necessary).
        // -> move to a more appropriate location
        colorStr = colorStr.toLowerCase();
        if (this.CSSCOLORS[colorStr]) {
            return this.CSSCOLORS[colorStr];
        } else {
            var match = this.HEX.exec(colorStr);
            if ( match ) { 
                if ( match[1].length === 3) {
                    // repeat digits. #abc => 0xaabbcc
                    return Number("0x"+match[1].charAt(0)+match[1].charAt(0)+
                                  match[1].charAt(1)+match[1].charAt(1)+
                                  match[1].charAt(2)+match[1].charAt(2));
                } else if ( match[1].length === 6) {
                    return Number("0x"+match[1]);
                } else {
                    return Number("0x000000"); //as good as any
                }
            }
        }
        return 0;
    },

    // Regular expression tests and other constants

    WHITESPACE	:/^\s+/,
    COMMENT		:/\/\*.+?\*\/\s*/,
    CLASS		:/^([.:]\w+)\s*/,
    NOT_CLASS	:/^!([.:]\w+)\s*/,
    ZOOM		:/^\|\s*z([\d-]+)\s*/i,
    GROUP		:/^,\s*/i,
    CONDITION	:/^\[(.+?)\]\s*/,
    OBJECT		:/^(\w+|\*)\s*/,
    DECLARATION	:/^\{(.+?)\}\s*/,
    SUBPART		:/^::(\w+)\s*/,
    UNKNOWN		:/^(\S+)\s*/,
    
    ZOOM_MINMAX	:/^(\d+)-(\d+)$/,
    ZOOM_MIN	:/^(\d+)-$/,
    ZOOM_MAX	:/^-(\d+)$/,
    ZOOM_SINGLE	:/^(\d+)$/,
    
    CONDITION_TRUE      :/^\s*([:\w]+)\s*=\s*yes\s*$/i,
    CONDITION_FALSE     :/^\s*([:\w]+)\s*=\s*no\s*$/i,
    CONDITION_SET       :/^\s*([:\w]+)\s*$/,
    CONDITION_UNSET     :/^\s*!([:\w]+)\s*$/,
    CONDITION_EQ        :/^\s*([:\w]+)\s*=\s*(.+)\s*$/,
    CONDITION_NE        :/^\s*([:\w]+)\s*!==\s*(.+)\s*$/,
    CONDITION_GT        :/^\s*([:\w]+)\s*>\s*(.+)\s*$/,
    CONDITION_GE        :/^\s*([:\w]+)\s*>=\s*(.+)\s*$/,
    CONDITION_LT        :/^\s*([:\w]+)\s*<\s*(.+)\s*$/,
    CONDITION_LE        :/^\s*([:\w]+)\s*<=\s*(.+)\s*$/,
    CONDITION_REGEX     :/^\s*([:\w]+)\s*=~\/\s*(.+)\/\s*$/,
    
    ASSIGNMENT_EVAL	:/^\s*(\S+)\s*:\s*eval\s*\(\s*['"](.+?)['"]\s*\)\s*$/i, // TODO: match only two matching quotes
    ASSIGNMENT		:/^\s*(\S+)\s*:\s*(.+?)\s*$/,
    SET_TAG_EVAL	:/^\s*set\s+(\S+)\s*=\s*eval\s*\(\s*'(.+?)'\s*\)\s*$/i,
    SET_TAG			:/^\s*set\s+(\S+)\s*=\s*(.+?)\s*$/i,
    SET_TAG_TRUE	:/^\s*set\s+(\S+)\s*$/i,
    EXIT			:/^\s*exit\s*$/i,
    
    oZOOM: 			2,
    oGROUP: 		3,
    oCONDITION: 	4,
    oOBJECT: 		5,
    oDECLARATION: 	6,
    oSUBPART: 		7,
    
    DASH: 	/-/g,
    COLOR: 	/color$/,
    BOLD: 	/^bold$/i,
    ITALIC: /^italic|oblique$/i,
    UNDERLINE: /^underline$/i,
    CAPS: 	/^uppercase$/i,
    CENTER: /^center$/i,
    FALSE: 	/^(no|false|0)$/i,
    
    HEX: 	/^#([0-9a-f]+)$/i,
    
    CSSCOLORS: {
        aliceblue:0xf0f8ff,
        antiquewhite:0xfaebd7,
        aqua:0x00ffff,
        aquamarine:0x7fffd4,
        azure:0xf0ffff,
        beige:0xf5f5dc,
        bisque:0xffe4c4,
        black:0x000000,
        blanchedalmond:0xffebcd,
        blue:0x0000ff,
        blueviolet:0x8a2be2,
        brown:0xa52a2a,
        burlywood:0xdeb887,
        cadetblue:0x5f9ea0,
        chartreuse:0x7fff00,
        chocolate:0xd2691e,
        coral:0xff7f50,
        cornflowerblue:0x6495ed,
        cornsilk:0xfff8dc,
        crimson:0xdc143c,
        cyan:0x00ffff,
        darkblue:0x00008b,
        darkcyan:0x008b8b,
        darkgoldenrod:0xb8860b,
        darkgray:0xa9a9a9,
        darkgreen:0x006400,
        darkkhaki:0xbdb76b,
        darkmagenta:0x8b008b,
        darkolivegreen:0x556b2f,
        darkorange:0xff8c00,
        darkorchid:0x9932cc,
        darkred:0x8b0000,
        darksalmon:0xe9967a,
        darkseagreen:0x8fbc8f,
        darkslateblue:0x483d8b,
        darkslategray:0x2f4f4f,
        darkturquoise:0x00ced1,
        darkviolet:0x9400d3,
        deeppink:0xff1493,
        deepskyblue:0x00bfff,
        dimgray:0x696969,
        dodgerblue:0x1e90ff,
        firebrick:0xb22222,
        floralwhite:0xfffaf0,
        forestgreen:0x228b22,
        fuchsia:0xff00ff,
        gainsboro:0xdcdcdc,
        ghostwhite:0xf8f8ff,
        gold:0xffd700,
        goldenrod:0xdaa520,
        gray:0x808080,
        green:0x008000,
        greenyellow:0xadff2f,
        honeydew:0xf0fff0,
        hotpink:0xff69b4,
        indianred:0xcd5c5c,
        indigo:0x4b0082,
        ivory:0xfffff0,
        khaki:0xf0e68c,
        lavender:0xe6e6fa,
        lavenderblush:0xfff0f5,
        lawngreen:0x7cfc00,
        lemonchiffon:0xfffacd,
        lightblue:0xadd8e6,
        lightcoral:0xf08080,
        lightcyan:0xe0ffff,
        lightgoldenrodyellow:0xfafad2,
        lightgrey:0xd3d3d3,
        lightgreen:0x90ee90,
        lightpink:0xffb6c1,
        lightsalmon:0xffa07a,
        lightseagreen:0x20b2aa,
        lightskyblue:0x87cefa,
        lightslategray:0x778899,
        lightsteelblue:0xb0c4de,
        lightyellow:0xffffe0,
        lime:0x00ff00,
        limegreen:0x32cd32,
        linen:0xfaf0e6,
        magenta:0xff00ff,
        maroon:0x800000,
        mediumaquamarine:0x66cdaa,
        mediumblue:0x0000cd,
        mediumorchid:0xba55d3,
        mediumpurple:0x9370d8,
        mediumseagreen:0x3cb371,
        mediumslateblue:0x7b68ee,
        mediumspringgreen:0x00fa9a,
        mediumturquoise:0x48d1cc,
        mediumvioletred:0xc71585,
        midnightblue:0x191970,
        mintcream:0xf5fffa,
        mistyrose:0xffe4e1,
        moccasin:0xffe4b5,
        navajowhite:0xffdead,
        navy:0x000080,
        oldlace:0xfdf5e6,
        olive:0x808000,
        olivedrab:0x6b8e23,
        orange:0xffa500,
        orangered:0xff4500,
        orchid:0xda70d6,
        palegoldenrod:0xeee8aa,
        palegreen:0x98fb98,
        paleturquoise:0xafeeee,
        palevioletred:0xd87093,
        papayawhip:0xffefd5,
        peachpuff:0xffdab9,
        peru:0xcd853f,
        pink:0xffc0cb,
        plum:0xdda0dd,
        powderblue:0xb0e0e6,
        purple:0x800080,
        red:0xff0000,
        rosybrown:0xbc8f8f,
        royalblue:0x4169e1,
        saddlebrown:0x8b4513,
        salmon:0xfa8072,
        sandybrown:0xf4a460,
        seagreen:0x2e8b57,
        seashell:0xfff5ee,
        sienna:0xa0522d,
        silver:0xc0c0c0,
        skyblue:0x87ceeb,
        slateblue:0x6a5acd,
        slategray:0x708090,
        snow:0xfffafa,
        springgreen:0x00ff7f,
        steelblue:0x4682b4,
        tan:0xd2b48c,
        teal:0x008080,
        thistle:0xd8bfd8,
        tomato:0xff6347,
        turquoise:0x40e0d0,
        violet:0xee82ee,
        wheat:0xf5deb3,
        white:0xffffff,
        whitesmoke:0xf5f5f5,
        yellow:0xffff00,
        yellowgreen:0x9acd32
    },
    
};

export default MapCSSRuleSet;
