/**
 * @classdesc The main Snaptobus class
 * @author Nike
 * @version 1.0
 */

class Snaptobus{
    constructor(options){
        this.spriteDefs = options.spriteDefs
        this._curBusConfig = []
        this._stagedBusConfig = options.busConfig
        this.snap = options.snap
        this.commitConfig()
    }

    get busConfig() { return this._stagedBusConfig }
    get liveBusConfig() { return this._curBusConfig }
    set busConfig(newConfig) { this._stagedBusConfig = newConfig }

    async commitConfig(){
        const chansToAdd = []
        const chansToKeep = []
        for(const chanObj of this._stagedBusConfig){
            console.log('staged chan:',chanObj.chan,' current ones:', this._curBusConfig.map(item => item.chan))
            if(this._curBusConfig.map(item => item.chan).includes(chanObj.chan)) chansToKeep.push(chanObj)
            else chansToAdd.push(chanObj)
        }
        const chansToDel = this._curBusConfig.filter(item => (!chansToKeep.map(c=>c.chan).includes(item.chan) && !chansToAdd.map(c=>c.chan).includes(item.chan)))
        await app.MessageBus.subscribe(chansToAdd.map(item => item.chan))
        await app.MessageBus.unSubscribe(chansToDel.map(item => item.chan))
        // console.log('subscribe:', chansToAdd.map(item => item.chan))
        // console.log('unSubscribe:', chansToDel)
  
        const eventsToAdd = chansToAdd.flatMap(item => item.events.map(ev => ({ chan:item.chan, eventName:ev.eventName })))
        let eventsToDel = []//= chansToDel.flatMap(item => item.events.map(ev => ({ chan:item.chan, eventName:ev.eventName })))
        for(const oldChan of this._curBusConfig){
            for(const oldEvent of oldChan.events){
                for(const keepChan of chansToKeep){
                    if(!keepChan.events.map(item=>item.eventName).includes(oldEvent.eventName)) eventsToDel.push({chan: oldChan.chan, eventName: oldEvent.eventName})
                }
            }
        }
        
        // console.log('eventsToAdd:', eventsToAdd)
        // console.log('eventsToDel:', eventsToDel)
        for(const eventToAdd of eventsToAdd){
            app.MessageBus.addBusListener(eventToAdd.eventName, [eventToAdd.chan], this.processBusEvent.bind(this, eventToAdd.eventName,), 'snaptobus')
        }
        for(const eventToDel of eventsToDel){
            app.MessageBus.removeBusListener(eventToDel.eventName, this.processBusEvent.bind(this, eventToDel.eventName), 'snaptobus')
        }

        this._curBusConfig = this.deepClone(this._stagedBusConfig)
    }
    
    deepClone(obj) { // Needed because structuredClone doesn't take functions (and we have transformers)
        if (obj === null || typeof obj !== 'object') {
            return obj
        }
        if (Array.isArray(obj)) {
            return obj.map((el => this.deepClone(el)))
        }
        const clone = {}
        for (const key in obj) {
            clone[key] = this.deepClone(obj[key])
        }
        return clone
    }    


    observeObject(obj, onChange) {
        const wrap = (value) =>
            (value && typeof value === 'object')
        ? this.observeObject(value, onChange)
        : value
        
        const handler = {
            set: (target, prop, value) => {
                const oldValue = target[prop]
                target[prop] = wrap(value)
                onChange(prop, value, oldValue, target)
                return true
            },
            deleteProperty: (target, prop) => {
                const oldValue = target[prop]
                delete target[prop]
                onChange(prop, undefined, oldValue, target)
                return true
            }
        }
        
        // Walk initial keys/elements (construction time)
        for (const key of Object.keys(obj)) {
            obj[key] = wrap(obj[key])
        }

        return new Proxy(obj, handler)
    }


    processBusEvent(eventType, chan, payload, userId, x){
        const chanObj = this._curBusConfig.find(item => item.chan==chan)
        if(!chanObj) return
        const eventObj = chanObj.events.find(item => item.eventName==eventType)
        if(!eventObj) return
        // assign attributes in payload to all snap objects for that eventType
        // each snap is an assignation of attributes via a selector
        for(const snapDef of eventObj.snaps){
            const selector = snapDef.selector.replace(/\$\{(\w+)\}/g, (_, key) => this.getValueByPath(payload, key))
            console.log('Will look for snap:', selector)
            const snaps = this.snap.selectAll(selector)
            console.log(`found ${snaps.length} snaps`)
            snaps.forEach(snapEl => {
                const newAttr = this.assignFromConfig(payload, snapDef.assign)
                if(snapDef.animate){
                    snapEl.animate(
                        newAttr,
                        400,
                         mina.linear
                    )
                } else {
                    snapEl.attr(newAttr)
                }
            })
        }
    }

    assignFromConfig(data, replaceDef) {
        console.log('assignFromConfig', data, replaceDef)
        const result = {}        
        for (const [key, rule] of Object.entries(replaceDef)) {
            if (typeof rule === 'string') { // plain path                
                result[key] = this.getValueByPath(data, rule)
            } else if((typeof(rule) == 'object') && (typeof(rule.transformer) == 'function')) { // transformer
                const fnargs = (rule.arguments || []).map(arg => this.getValueByPath(data,arg))
                result[key] = rule.transformer(...fnargs)
            }
        }        
        return result
    }

    getValueByPath(obj, path) {
        return(path.split('.').reduce((acc, key) => acc?.[key], obj))
    }
        
}
/*
const s2bConfig = [
    {   chan: 'gps:agents', // What to subscribe to
        events: [  // What to select on this chan
            {   eventName: 'moving',
                snaps: [
                    {
                        // selector will be used as css selector for a snap element / group, 
                        // with LAST MINUTE template resolving of event properties (with eventual dots)
                        selector: '#${aid}', 
                        assign: {
                            x: 'coords.x',  // type string: event property, eventual dots to go down object
                            y: 'coords.y',
                        },
                        animate: true
                    }
                ]
            },
            {   eventName: 'rotating',
                snaps: [
                    {
                        selector: '#${aid}', 
                        assign: {
                            r: 'rotangle'
                        },
                        animate: true
                    }
                ]
            },            
        ]
    },
    {   chan: 'agent:*', // wildcards allowed
        events: [
            {   eventName: 'aging',
                snaps: [
                    {
                        selector: '#{aid}',
                        assign: {
                            fill: { // transformer function
                                arguments: [ 'age' ], // What to give from the event as function's params
                                transformer: i => `rgb(${Math.round(255 * i / 10)},0,${Math.round(255 * (1 - i / 10))})`
                            },
                        },
                    }
                ]
            },
        ]
    },
]
*/

/*
payload = 
{}
   eventType
*/
