/* global require,console,__dirname */

/* Node script to validate caniuse feature JSONs */
(function () {
    var fs = require('fs');
    var path = __dirname + '/../features-json';
    var sampleData;
    var w3cStatusArr = ['rec', 'pr', 'cr', 'wd'];
    var statusArr = w3cStatusArr.concat(['ls', 'other', 'unoff']);
    var categoryArr = ['HTML5', 'CSS', 'CSS2', 'CSS3', 'SVG', 'PNG', 'JS API', 'Canvas', 'DOM', 'Other', 'JS', 'Security'];
    // Support string MUST have one of these (optionally others)
    var supportValues = ['y', 'a', 'n', 'u', 'p'];

    var validationFn = {
        isString: function (val) {
            return typeof val === 'string';
        },
        isObject: function (val) {
            return typeof val === 'object';
        },
        isArray: function (val) {
            return val instanceof Array;
        },
        isURL: function (val) {
            // Source: https://gist.github.com/dperini/729294
            var pattern = new RegExp(
                "^" +
                // protocol identifier
                "(?:(?:https?|ftp)://)" +
                // user:pass authentication
                "(?:\\S+(?::\\S*)?@)?" +
                "(?:" +
                // IP address exclusion
                // private & local networks
                "(?!10(?:\\.\\d{1,3}){3})" +
                "(?!127(?:\\.\\d{1,3}){3})" +
                "(?!169\\.254(?:\\.\\d{1,3}){2})" +
                "(?!192\\.168(?:\\.\\d{1,3}){2})" +
                "(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})" +
                // IP address dotted notation octets
                // excludes loopback network 0.0.0.0
                // excludes reserved space >= 224.0.0.0
                // excludes network & broadcast addresses
                // (first & last IP address of each class)
                "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" +
                "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" +
                "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +
                "|" +
                // host name
                "(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)" +
                // domain name
                "(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*" +
                // TLD identifier
                "(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))" +
                ")" +
                // port number
                "(?::\\d{2,5})?" +
                // resource path
                "(?:/[^\\s]*)?" +
                "$", "i"
            );
            return pattern.test(val);
        },
        isStatus: function (val) {
            return statusArr.indexOf(val) > -1;
        },
        atLeastOne: function (arr) {
            return arr.length >= 1;
        },
        hasCategories: function (arr) {
            for (var i = 0; i < arr.length; i++) {
                if (categoryArr.indexOf(arr[i]) === -1) {
                    return false;
                }
            }
            return true;
        },
        isNumber: function (val) {
            return typeof val === 'number';
        },
        isBoolean: function (val) {
            return typeof val === 'boolean';
        }
    };

    var Validator = function (type, id, data) {

        this.throwError = function (message) {
            var pre = '[' + id;
            if( this.currentBrowser ) {
                pre += ':' + this.currentBrowser;
            }
            if( this.currentVersion ) {
                pre += ':' + this.currentVersion;
            }
            pre += '] ';
            throw Error(pre + message);
        };
        
        this.warn = function (message) {
            try {
                console.warn('[' + id + '] WARNING: ' + message);
            } catch(e){}
        };

        this.validateArray = function (template, arr) {
            for (var i = 0; i < arr.length; i++) {
                var itemToValidate = arr[i];
                for (var key in template) {
                    var itemRules = template[key];
                    this.validate(key, itemRules, itemToValidate);
                }
                this.validateKeys('array', template, itemToValidate);
            }
        };

        this.validate = function (key, rules, altObject) {
            var object = altObject || data;
            if (!(key in object)) {
                this.throwError('"' + key + '" missing in data');
            }
            var val = object[key];
            if (typeof rules === 'function') {
                var validatorFn = rules;
                validatorFn(val);
                return;
            }
            for (var i = 0; i < rules.length; i++) {
                var rule = rules[i];
                if (typeof rule == 'string') {
                    if (!validationFn[rule](val)) {
                        this.throwError('Failed ' + rule + ' validation on "' + key + '". Got this: ' + val);
                    }
                } else if (rule instanceof Array) {
                    this.validateArray(rule[0], val);
                }
            }
        };

        this.validateToken = function (token) {
            // Must be any of these letters or #1, #2, etc.
            if (!/^(y|a|n|u|p|x|d|(\#\d+))$/.test(token)) {
                this.throwError('Invalid token: ' + token);
            }
        };

        this.validateSupportValue = function (val) {
            if (!validationFn.isString(val)) {
                this.throwError('Expected ' + val + ' to be a string');
            }
            var tokens = val.split(' ');
            var doneTokens = {};
            // Must have exactly one of these
            var gotSupportToken = false;

            for (var i = 0; i < tokens.length; i++) {
                var token = tokens[i];
                if (doneTokens[token]) {
                    this.throwError('Duplicate token: ' + token);
                }
                doneTokens[token] = true;
                this.validateToken(token);

                if (supportValues.indexOf(token) > -1) {
                    if (gotSupportToken) {
                        this.throwError('Duplicate support token: ' + token);
                    }
                    gotSupportToken = true;
                }
            }
            if (!gotSupportToken) {
                this.throwError('No support token found');
            }
        };

        this.validateStatusOfSpec = function () {
            if (/^https?:\/\/(?:[^.]+)\.spec\.whatwg\.org\//.test(data.spec)) {
                if (w3cStatusArr.indexOf(data.status) > -1) {
                    this.throwError('W3C status "' + data.status + '" not valid for WHATWG spec "' + data.spec + '"')
                }
            }
        };

        // Ensures that there are no dangling notes with either a missing description or missing references
        this.validateNoteCoherence = function () {
            var statReferences = new Set();
            for (var browserStats of Object.values(data.stats)) {
                for (var versionStat of Object.values(browserStats)) {
                    // This regex assumes that the value is syntactically correct
                    var versionRefs = versionStat.matchAll(/ #(?<number>\d+)/gu);
                    for (var reference of versionRefs) {
                        statReferences.add(reference.groups.number);
                    }
                }
            }
            var notes = new Set(Object.keys(data.notes_by_num));

            var allReferences = new Set([...statReferences, ...notes]);
            for(var reference of allReferences) {
                if(!statReferences.has(reference)){
                    this.throwError('Note #' + reference + ' is never referenced in stats, and will therefore not be shown');
                }
                if(!notes.has(reference)){
                    this.throwError('Found a reference to note #' + reference + ', but that note does not exist');
                }
            }
        };

        this.validateKeys = function(parentKey, refObject, object) {
            for( var key in object ) {
                if( !(key in refObject) ) {
                    this.throwError('Extra key found in ' + parentKey + ': "' + key + '"');
                }
            }
        };

        this.validateSupportData = function () {
            var sampleStats = sampleData.stats;
            var stats = data.stats;
            for (var browserId in sampleStats) {
                this.currentBrowser = browserId;
                // Check if browser exists
                if (!(browserId in stats)) {
                    this.throwError('No data found for browser "' + browserId + '"');
                }
                var sampleSupportByVersion = sampleStats[browserId];
                var supportByVersion = stats[browserId];
                for (var version in sampleSupportByVersion) {
                    this.currentVersion = version;
                    if (!(version in supportByVersion)) {
                        this.throwError('Browser version missing: ' + browserId + ' ' + version);
                    }
                    var support = supportByVersion[version];
                    this.currentVersion = null;
                    this.validateSupportValue(support);
                }
                this.validateKeys(browserId, sampleSupportByVersion, supportByVersion);
            }
            this.currentBrowser = null;
            this.currentVersion = null;
            this.validateKeys('stats', sampleStats, stats);
            this.validateNoteCoherence();
        };

        this.validateFeature = function () {
            this.validate('title', ['isString']);
            this.validate('description', ['isString']);
            this.validate('spec', ['isString', 'isURL']);
            this.validate('status', ['isString', 'isStatus']);
            this.validateStatusOfSpec();
            this.validate('links', ['isArray', [{
                        url: ['isString', 'isURL'],
                        title: ['isString']
            }]]);
            this.validate('bugs', ['isArray', [{
                        description: ['isString']
            }]]);
            this.validate('categories', ['isArray', 'hasCategories']);
            this.validate('notes', ['isString']);
            this.validate('notes_by_num', ['isObject']);
            this.validate('usage_perc_y', ['isNumber']);
            this.validate('ucprefix', ['isBoolean']);
            this.validate('parent', ['isString']);
            this.validate('parent', function(featureid) {
            	// TODO: Check if existing feature
                if (featureid === 'parentfeatureid' && id !== 'sample-data') {
                    this.throwError('"parent" value is invalid, got: ' + featureid);
                }
            }.bind(this));
            this.validate('keywords', ['isString']);
            this.validate('chrome_id', ['isString']);
            this.validate('shown', ['isBoolean']);
            this.validateSupportData();
        };
        
        this.validateUsage = function () {
            this.validate('id', ['isString']);
            this.validate('name', ['isString']);
            this.validate('month', ['isString']);
            this.validate('month', function(month) {
                if (!/^\d{4}-\d{2}$/.test(month)) {
                    this.throwError('Format for month is invalid, got: ' + month);
                }
            }.bind(this));
            this.validate('access_date', ['isString']);
            this.validate('access_date', function(date) {
                if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
                    this.throwError('Format for access_date is invalid, got: ' + date);
                }
            }.bind(this));
            this.validate('data', ['isObject']);
            this.validate('total', ['isNumber']);
            this.validate('total', function(total) {
                // Total amount should be 100 - untracked usage
                // If total is too low (especially for US) something is probably wrong
                if (total < 80) {
                    this.warn('Expected total usage to be > 80, was ' + total);
                }
                if (id === 'US.json' && total < 95) {
                    this.warn('Expected US total usage to be > 95, was ' + total);
                }
            }.bind(this));
        };
        
        switch(type) {
            case 'feature':
                this.validateFeature();
                break;
            case 'usage':
                this.validateUsage();
                break;
        }
        
    };

    var processFile = function (type, error, data, fileName) {
        if (error) {
            throw Error('Error: ' + error);
        }
        try {
            data = JSON.parse(data);
        } catch(e) {
            throw Error('Error in file "' + fileName + '": ' + e);
        }
        if (type === 'feature') {
            var matches = fileName.match(/([a-z0-9-]+)\.json$/);
            if( !matches || matches.length < 2  ) {
                console.log('Skipping file: ' + fileName);
                return;
            }
            var featureId = matches[1];
            new Validator('feature', featureId, data);
        } else {
            var id = /[^\/]+\.json/.exec(fileName)[0];
            new Validator(type, id, data);
        }
        
    };

    var readFile = function (file, type) {
        fs.readFile(file, function (error, data) {
            processFile(type, error, data, file);
        });
    };

    fs.readFile(__dirname + '/../sample-data.json', function (error, data) {
        if (error) {
            throw Error('Error: ' + error);
        }
        sampleData = JSON.parse(data);
        var files = fs.readdirSync(path);
        for (var i = 0; i < files.length; i++) {
            var file = files[i];
            if (/[.]json$/.test(file)) {
                readFile(path + '/' + file, 'feature');
            } else {
                throw Error('File "' + file + '" does not have ".json" extension');
            }
        }
    });
	
    var regionPath = __dirname + '/../region-usage-json';
    var regionFiles = fs.readdirSync(regionPath);
    for (var i = 0; i < regionFiles.length; i++) {
        var file = regionFiles[i];
        if (file.indexOf('.json') > -1) {
            readFile(regionPath + '/' + file, 'usage');
        }
    }
}());
