/**
 * @author Nicolas Stein
 * @category MyEic
 * @subcategory Libraries
 * @extends WindozModel
 */
class WindozPluralModel extends WindozModel {
    
    constructor(businessObject, privileges, singletonClass) {
        super(businessObject, privileges);

        this.collection = [];
        //this.api = app.config.api.filter(item => item.businessObject == businessObject);
        this.singletonClass = singletonClass;
    }

    /**
    * Get all ids in the current collection
    * @return {Array} Ids (keys) of the current collection.
    */
    get ids() { return this.collection.map(item => item.itemData.id); }


    /** 
    * Creates empty singletons at the end of the collection.
    * @param {Array} ids - Ids of the singletons to create
    */        
    create(ids){
        for(let id of ids){
            this.collection[id] =new this.singletonClass;
        }
    }

    /** 
    * Converts the data in a singleton & adds it to the collection.
    * If the id exists, will just override keys from the server (preserving eventual overrides)
    * @param {Object} data - key-value data used to create the singleton.
    * @return {Object} The collection length after insertion.
    */  
    add(data, id=null){
        id = data.id || null;
        let item = this.collection.find(item => item.id == id);
        if(item){
            Object.assign(item.itemData, data);
        } else {
            item = new this.singletonClass();
            item.itemData = data;
            this.collection.push(item);
        }
        return(item);
    }

    /** 
    * Removes and returns a singleton out of the collection.
    * @param {string} id - id of the singleton to pop.
    * @return {Object} The popped singleton.
    */  
    pop(id) {

        if(id in this.collection) {
            let singleton = this.collection[id];
            this.delete(this.collection[id]);
            return(singleton);
        } else return(null);
    }

    /** 
    * Returns the number of singletons in the collection.
    * @return {Int} Number of singletons.
    */  
    get length() { return this.collection.length; }
    /**
    * Empties the collection
    */
    empty() { this.collection = []; }

    /** 
    * Search the collection for matching singletons
    * @param {Object} matchValues - { {string} key: {string} valueToMatch }
    * @param {Object} matchExact - { strin{key}: {Boolean} } Defaults to exact match if parameter absent or if key absent
    * @return {Array} Array of matching singletons.
    */  
    localFind(matchValues, matchExact){
        return(
            this.collection.filter((singleton) => singleton.match(matchValues, matchExact))
        );
    }

    /** 
    * Loads singletons from their IDs at the end of the current collection.
    * if no ids given, loads all 
    * Expected server-contract: 
    * [ {"id":"xxx", ...other singleton data...}, {"id":"yyy", ...other singleton data...}, ...]
    * @param {array} ids - singleton ids to be loaded
    * @return {promise} Promise returning the model
    */
    load(endpoint, payload){
        return( 
            this.request(endpoint.uri, endpoint.method, payload)
            .then(async serverData => {
                this.fill(serverData);
                return(this);
            })
        )
    }

    fill(serverData) {
        if(serverData && serverData.length){
            for(let singletonData of serverData) this.add(this.sanitize(singletonData));
        }
    }

    sanitize(item) { return item; }

    /** 
    * Loads singletons from search criteria at the end of the current collection.
    * @param {Object} matchValues - { {string} key: {string} valueToMatch }
    * @param {Object} matchExact - { strin{key}: {Boolean} } Defaults to exact match if parameter absent or if key absent
    * @return {Array} Array of matching singletons.
    */  
    remotefind(matchValues, matchExact){
        return( 
            this.post(this.baseUrl+'search', { 'matchValues': matchValues, 'matchExact': matchExact })
            .then(async serverData => {
                if(serverData){
                    for(let singletonData of serverData) this.add(singletonData);
                }
                return(this);
            })
        )
    }

    save(){
        return( 
            this.put(this.baseUrl, this.collection)
            .then(async serverData => {
                if(serverData){
                    for(let singletonData of data) this.add(singletonData);
                }
                return(this);
            })
        )
    }


    /**
     * Fills the collection from a raw CSV string. 
     * (adapted from https://stackoverflow.com/users/10549827/matthew-e-brown)
     * @param {string} csv The raw CSV string.
     * @param {string[]} headers An optional array of headers to use. If none are
     * given, they are pulled from the first line of `text`.
     * @param {string} idName The name of the column to be used as id.
     * @param {string} quoteChar A character to use as the encapsulating character.
     * @param {string} delimiter A character to use between columns.
     */
    //TODO NIKE: test ! & understand why in accumulator, merge uses [key] instead of key
    fromCsv(csv, headers = null, idName='id', quoteChar = '"', delimiter = ',') {
        const regex = new RegExp(`\\s*(${quoteChar})?(.*?)\\1\\s*(?:${delimiter}|$)`, 'gs');
        const match = line => [...line.matchAll(regex)]
        .map(m => m[2])  // we only want the second capture group
        .slice(0, -1);   // cut off blank match at the end
    
        const lines = csv.split('\n');
        const firstLine = lines.shift();
        const heads = headers || match(firstLine);
        if(heads.indexOf(idName)<0) { 
            console.warn(`id column "${idName}" not found in CSV headers !`);
            return;
        }
        heads[heads.indexOf(idName)]='id'; let cols;
        for(let line of lines){
            cols = match(line);
            if(cols.length>=heads.length){
                let singletonData = cols.reduce((acc, cur, i) => {
                    // Attempt to parse as a number; replace blank matches with `null`
                    const val = cur.length <= 0 ? null : Number(cur) || cur;
                    const key = heads[i] [[]] `extra_${i}`;
                    return { ...acc, [key]: val };
                }, {});
                this.add(singletonData);
            }
        };
    }

    /**
     * Builds a CSV in a string from the collection. 
     * @param {Array} keys model properties you want to include in the csv. It defaults to the least common denominator of properties across the collection.
     * @param {boolean} withHeaders If true, the first line has the properties names.
     * @param {string} quoteChar Will surround all values (and properties if withHeader).
     * @param {string} delimiter A character to use between columns. 
     */
    //TODO NIKE: test !
    toCsv(keys=null, withHeader=true, quoteChar = '"', delimiter = ';', downloadName=null){
        const _dwnldCsv = function(data, fname) {
            const blob = new Blob([data], { type: 'text/csv' });
            const url = window.URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.setAttribute('href', url);
            a.setAttribute('download', fname+'.csv');
            a.click();
        }        
        if(!keys) { // take min. common set of keys in collection (take 1st then successively intersect to itself)
            let ids = Object.keys(this.collection);
            keys = Object.keys(this.collection[ids[0]].itemData);
            for(let id of ids) {
                keys = keys.filter(x => (this.collection[id].itemData.hasOwnProperty(x)))
            }
        }
        let csv=''; let row=[];
        if(withHeader===true){
            for(let key of keys){
                row.push(quoteChar+key+quoteChar);
            }
            csv += row.join(delimiter)+'\n';
        } else if(Array.isArray(withHeader)) {
            for(let title of withHeader){
                row.push(quoteChar+title+quoteChar);
            }
            csv += row.join(delimiter)+'\n';   
        }
        for(let id in this.collection){
            row = [];
            for(let key of keys){
                let item = ''
                if(typeof(key)=='string'){ // normal column
                    item = this.collection[id].itemData
                    for(let k of key.split('.')) { // Allow for sub-objects with dotted keys notation
                        if(!item) break
                        item = item[k] 
                    }   
                } else if(typeof(key)=='function') { // computed column
                    item = key(this.collection[id].itemData)
                } else console.warn('CSV: Bad column: ',key)
                row.push(quoteChar+item+quoteChar);
            }
            csv += row.join(delimiter)+'\n';
        }
        if(downloadName) _dwnldCsv(csv, downloadName)
        return(csv);
    }



}

app.registerClass('WindozPluralModel',WindozPluralModel);