const fs = require('fs')

const Reps = require('./reps.js')

const JSUnit = require('./lib/jsunit.js')
let isStateKey = JSUnit.isStateKey

function newProgram(name) {
    var program = {
        description: {
            name: name,
            id: name
        },
        modules: {}
    }

    return program
}

function loadModuleFromSource(program, path, id) {
    // source -> heap
    if (fs.existsSync(path)) {
        var src = require(path)
        var mod = new src()

        // wants unique module id's 
        if (program.description.counter == null) {
            program.description.counter = 0
        } else {
            program.description.counter++
        }

        // make unique name 
        if (id) {
            mod.description.id = id
        } else {
            mod.description.id = mod.description.name + '-' + program.description.counter
        }
        mod.description.path = path

        /* ---------------------------------- */
        // WARN! Corner Case should Go Away or Improve at next spiral 
        if(mod.description.isLink){
            for (mdlName in program.modules) {
                if (program.modules[mdlName].description.isLink) {
                    console.log("PRGMEM ONLY BIG ENOUGH FOR ONE LINK")
                    process.exit()
                }
            }
        }
        // end corner case code 
        /* ---------------------------------- */

        // add to program object 
        program.modules[mod.description.id] = mod

        // input need references for later hookup
        for (key in mod.inputs) {
            mod.inputs[key].parentId = mod.description.id
            mod.inputs[key].key = key
        }

        // state updating, begs for update 
        for (key in mod.state) {
            if (key == 'onChange' | key == 'emitChange' | key == 'emitters') {
                //console.log('rolling past change fn')
            } else {
                mod.state['_' + key] = mod.state[key]
                mod.state[key] = {}
                writeStateObject(mod, key)
            }
        }

        if (program.description.id == null) {
            if (program.description.name == null) {
                if (program.description == null) {
                    program.description = {}
                }
                program.description.name = 'unnamed program'
            }
            program.description.id = program.description.name + '-' + 0
        }

        console.log('ADDING', mod.description.id, 'to', program.description.id)

        /* ---------------------------------- */
        // WARN! Corner Case should Go Away or Improve at next spiral 
        // hardware corner case is hardware corner case
        // here's what we'll do
        // if it's hardware, and we're not loading from some saved program (no id)
        // 
        if (mod.description.isHardware && !mod.description.isLink && id == null) {
            // make sure we haven't already done this, thx 
            var lnk = null 
            for (mdlName in program.modules) {
                if (program.modules[mdlName].description.isLink) {
                    lnk = mdlName
                }
            }
            if (lnk) {
                console.log('CORNER CASE: LOADING HW MODULE, LINKING TO LINK')
                program.modules[lnk].attach(mod.route)
            } else {
                console.log('CORNER CASE: LOADING HW MODULE, ALSO LOADING LINK')
                var link = loadModuleFromSource(program, './modules/hardware/atkseriallink.js')
                // hook it up auto-like 
                link.attach(mod.route)
            }
        }

        // also return it so that we can write programs without the UI 
        return mod
    } else {
        console.log('ERR no module found at ', path)
    }
}

function writeStateObject(mod, key) {
    Object.defineProperty(mod.state, key, {
        set: function(x) {
            // update internal value 
            this['_' + key] = x
            //console.log('SET', key, this['_' + key])
            // push to internal state change handler
            // let's call emitChange from the server-side ...
            // so that we don't get into any heavy VIR
            // when we change it within the module 
            // this.emitChange(key)
            // push to external view
            if (socket) {
                pushState(mod, key)
            }
        }
    })
    Object.defineProperty(mod.state, key, {
        get: function() {
            //console.log('GET', key, this['_' + key])
            return this['_' + key]
        }
    })
}

function removeModuleFromProgram(program, id) {
    // this simple? 
    delete program.modules[id]

    for (key in program.modules) {
        var mdl = program.modules[key]
        for (otKey in mdl.outputs) {
            mdl.outputs[otKey].checkLinks(id)
        }
    }
}

/*

EXTERNAL HOOKS

*/

function assignSocket(sckt) {
    socket = sckt
}

function pushState(mod, key) {
    var data = {
        id: mod.description.id,
        key: key,
        val: mod.state[key]
    }
    socket.send('put state change', data)
}


function saveProgram(prgmem, path) {
    // ok, and we're interested in just copying the relevant things ... 
    var svprgmem = {
        description: {
            name: prgmem.description.name,
            counter: prgmem.description.counter
        },
        modules: {}
    }

    var mdls = prgmem.modules

    for (key in mdls) {
        var mdl = mdls[key]
        var og = Reps.makeFromModule(mdl)
        svprgmem.modules[mdl.description.id] = og
    }

    fs.writeFileSync(path, JSON.stringify(svprgmem, null, 2), 'utf8')

    console.log('PROGRAM SAVED AT', path)
}


function openProgram(path) {
    var program = {}

    // get the .json file as an object 
    var prgRep = JSON.parse(fs.readFileSync(path, 'utf8'))
    console.log('OPENING THIS PRGRAM REP', prgRep)

    // copy-pasta the program descro, 
    program.description = {
        name: prgRep.description.name,
        counter: 0,
        id: prgRep.description.name + 1, // in another world, we count
        path: path
    }

    // gonna get those modules from source
    program.modules = {}

    for (key in prgRep.modules) {
        var mdlRep = prgRep.modules[key]
        loadModuleFromSource(program, mdlRep.description.path, mdlRep.description.id)
    }

    // restore saved state and links 
    for (modName in prgRep.modules) {
        // keys should be identical for rep and heap
        // this is the representation that we're opening up from 
        var mdlRep = prgRep.modules[modName]

        // this is the actual object, having functions and whatnot 
        var mdl = program.modules[modName]

        if (mdl == null) {
            console.log('NULL')
            console.log('prgRep modules', prgRep.modules)
            console.log('program modules', program.modules)
        }

        // hooking outputs -> inputs 
        for (outName in mdlRep.outputs) {
            var outRep = mdlRep.outputs[outName]
            // each has some caller ids 
            for (nestedInputRep in outRep.calls) {
                // conn from tl program -> this hookup 
                var nIRParent = outRep.calls[nestedInputRep].parentId
                var nIRKey = outRep.calls[nestedInputRep].key
                var nI = program.modules[nIRParent].inputs[nIRKey]
                console.log("ATTACHING", nIRKey, 'to', outName)
                mdl.outputs[outName].attach(nI)
            }
        }

        // restoring state 
        for (key in mdlRep.state) {
            if (isStateKey(key)) {
                // I think this is OK?
                // would prefer to do this before we write getters and setters
                // for now we walk-around to secret key ... 
                mdl.state['_' + key] = mdlRep.state[key]
            }
        }

        console.log('mdlRep', mdlRep)
        console.log('mdl', mdl)
        // restore position / UI state
        if (mdlRep.description.position != null) {
            // ??
            mdl.description.position = {}
            mdl.description.position.left = mdlRep.description.position.left
            mdl.description.position.top = mdlRep.description.position.top
        }
    }

    // once modules exist, link inputs / outputs / copy state ? 

    return program
}

module.exports = {
    new: newProgram,
    open: openProgram,
    save: saveProgram,
    loadModuleFromSource: loadModuleFromSource,
    removeModule: removeModuleFromProgram,
    assignSocket: assignSocket
}