//prints vDOM tree to compare with DOM
//tabs is used as helper to print the DOM in a readable format
const printDOMTree = (node, tabs = "") => {
    if (node.children === undefined) {
        return tabs + node[0].tag
    } else {
        prettyPrint = ""
        node.children.forEach(node => {
            prettyPrint += tabs + node.tag + "\n" + printDOMTree(node.children, tabs + "\t") + "\n";
        })
        return prettyPrint;
    }
}

function updateDOMProperties(node, prevVNode, nextVNode) {
    //if this is a text node, update the text value
    if (prevVNode.tag == "TEXT_ELEMENT" && nextVNode.tag == "TEXT_ELEMENT") {
        //set the data attribute in our DOM node instead of nodeValue for speed and for better error detection
        //(that we should not be setting this value for HTML tags that don't implement the CharacterData interface)
        node.data = nextVNode.nodeValue;
    }
    //add/remove attributes, event listeners 
    //remove attributes
    Object.keys(prevVNode.attributes || [])
                .forEach((key, _) => {
                    node.removeAttribute(key);
        });

    //remove old event listeners 
    Object.keys(prevVNode.events || [])
            .forEach((key, _) => {
                //remove event listener and set the value of the associated key to null
                node.removeEventListener(key, prevVNode.events[key]);
        });       


    //add attributes
    var attributes = nextVNode.attributes || []
    //helper method that sets an attribute 
    const setAttributeHelper = (key, val) => {
        //check if an ISL attribute was already mutated from DOM manipulation, in which case don't set it
        //otherwise may produce unintended DOM side-effects (e.g. changing the value of selectionStart)
        if (key && node[key] === val) {
            return;
        }
        //otherwise modify the attribute if it already exists and set element otherwise
        if (key in node) {
            node[key] = val;
        } else {
            // node[key] = val;
            node.setAttribute(key, val);
        }
    }
    //note if nextVNode is a fully rendered DOM node, .attributes will return a named node map
    //or we have a fully fledged DOM node where .attributes returns a NamedNodeMap
    //check this is a vdom node before applying attributes
    if (!(attributes.length) || attributes.length === 0) {
        //this means nextVNode is a vdom node
        Object.keys(attributes)
                .forEach((key, ) => {
                setAttributeHelper(key, nextVNode.attributes[key]); 
            });
    }
    //add event listeners
    Object.keys(nextVNode.events || [])
            .forEach((key, _) => {
                node.addEventListener(key, nextVNode.events[key]);
        });
}
 
 
const isEvent = key => key.startsWith("on");
const isDOM = node => node.nodeType !== Node.TEXT_NODE;
//instantiate a virtual DOM node to an actual DOM node
const instantiate = (vNode) => {
    if (!vNode.tag) {
        //if no tag, then this is already a rendered DOM node,
        if (vNode.node) {
            return vNode.node
        } 
        return vNode;
    } else {
        const domNode = vNode.tag !== "TEXT_ELEMENT" ? document.createElement(vNode.tag) : document.createTextNode(vNode.nodeValue);
        updateDOMProperties(domNode, normalize(null), vNode);
        //create children
        const childrenV = vNode.children || [];
        const childrenDOM = childrenV.map(instantiate);
        childrenDOM.forEach(child => {
            domNode.appendChild(child);
        });
        return domNode;
    }
}


//Tags
const APPEND = 1;
const DELETE = 2;
const REPLACE= 3;
const UPDATE = 4;

//queue to manage all updates to the DOM
//List of {op: <OP>, details: {}}
const updateQueue = [];

//used to update DOM operations from the queue
const performWork = () => {
    var node = null;
    for (let i = 0; i < updateQueue.length; i++) {
        //removes and returns item at index 0
        const item = updateQueue[i];
        switch (item.op) {
            case APPEND:
                parent = item.details.parent;
                child = item.details.node;
                if (parent) {
                    parent.appendChild(child);
                }
                break;
            case REPLACE:
                dom = item.details.dom
                prev = item.details.previous;
                //note calling instaniate also set DOM properties
                next = instantiate(item.details.node);
                dom.replaceWith(next);
                node = next;
                break;
            case DELETE:
                parent = item.details.parent;
                toRemove = item.details.node;
                parent.removeChild(toRemove);
                break;
            case UPDATE:
                dom = item.details.dom;
                prev = item.details.prev;
                newNode = item.details.new;
                updateDOMProperties(dom, prev, newNode);
                break;
        }
    }
    //reset `updateQueue` now that we've dequeued everything (this will empty the queue)
    updateQueue.length = 0;
    return node;
}

//used to normalize vDOM nodes to prevent consantly checking if nodes are undefined before accessing properties
const normalize = (vNode) => {
    if (!vNode) {
        return {tag: "", children: [], events: {}, attributes: {}};
    } 
    if (!(vNode.children)) {
        vNode.children = [];
    }
    if (!(vNode.events)) {
        vNode.events = {};
    }

    if (!(vNode.attributes)) {
        vNode.attributes = {};
    }
    return vNode;
}
 
//main render method for reconciliation
//newVNode: is new vDOM node to be rendered, 
//prevVNode: is old vDOM node that was previously rendered
//nodeDOM: is the corresponding node in the DOM
const renderVDOM = (newVNode, prevVNode, nodeDOM) => {
    //if have an empty node, return 
    if(!newVNode && !prevVNode) {
        return ;
    }
    const sameType = prevVNode && newVNode && newVNode.tag === prevVNode.tag;
    prevVNode = normalize(prevVNode);
    newVNode = normalize(newVNode);
    var node = normalize(null);
    //same node, only update properties
    if (sameType) {
        //means we have an element loaded in a list node since list nodes hand over fully rendered DOM nodes
        if (newVNode.tag === undefined) {
            updateQueue.push({op: REPLACE, details: {dom: nodeDOM, previous: prevVNode, node: newVNode}});
            node = newVNode;
        } else {
            updateQueue.push({op: UPDATE, details: {dom: nodeDOM, prev: prevVNode, new: newVNode}});  
            //render children
            if (newVNode.children) {
                const count = Math.max(newVNode.children.length, prevVNode.children.length);
                const domChildren = nodeDOM ? nodeDOM.childNodes : [];
                for (let i = 0; i < count; i++) {
                    newChild = newVNode.children[i];
                    prev = prevVNode.children[i]; 
                    //note there are two cases to consider here, either we have a child in our DOM tree (that is domChildren[i] is NOT
                    //undefined) or we don't. If we won't have a DOM child, there are two subcases a) newVNode doesn't exist
                    //or b) prevVnode doesn't exist.
                    domChild = domChildren[i] || true; 
                    child = renderVDOM(newChild, prev, domChild);
                    //only append node if it's new
                    if (child && !prev) {
                        updateQueue.push({op: APPEND, details: {parent: nodeDOM, node: child}});  
                    }
                }
            }
            node = nodeDOM;
            //if we are only updating the value of two text nodes, defer doing the `performWork` operation
            //until the caller finishes. This reduces unwanted side effects when we are diffing deep nested 
            //trees since we don't change the DOM until we've finished looking at all levels of the tree
            //as opposed to altering the DOM one level deep into the tree when we haven't yet looked at 
            //the other levels 
            if (prevVNode.tag === "TEXT_ELEMENT" && newVNode.tag === "TEXT_ELEMENT") {
                return node;
            }
        }
    } else if (newVNode.tag == "") {
        //node is no longer present so remove previous present virtual node
        //note if the DOM node is true (line 179), then that node has already been handled i.e. removed or added in a previous iteration
        if (nodeDOM !== true) {
            updateQueue.push({op: DELETE, details: {parent: nodeDOM.parentNode, node: nodeDOM}});
            //Note we want to to return here (i.e. not perform any work yet) to avoid removing DOM nodes before 
            //we have processed all of the children (to avoid indexing issues at line 168 causing us to skip nodes). This means we defer the 
            //`performWork` operation to be called by the parent. Note there is no scenario where we would encounter
            //an empty newVNode that reaches this block without being called by a parent.
            return node;
        } 
    
    } else if (prevVNode.tag == "") {
        //we have a new node that is currently not in the DOM
        node = instantiate(newVNode);
        if (nodeDOM) {
            //return child, parent will handle the add to the queue
            return node;
        }
        //otherwise adding a node to a currently empty DOM tree
        updateQueue.push({op: APPEND, details: {parent: null, node: node}}); 
    } else {
        //node has changed, so replace
        //note we use a similar heuristic to the React diffing algorithm here - since the nodes are different
        //we rebuild the entire tree at this node
        updateQueue.push({op: REPLACE, details: {dom: nodeDOM, previous: prevVNode, node: newVNode}});
        //if we have operations in our queue (i.e. length is greater than 1 since we just pushed an op) we have not yet performed then we defer 
        //performing the work until we have processed all of the children to reduce side-effects of altering the DOM.
        //Note we don't need to do this if there are no operations we need to perform since if the queue
        //is empty, we can be confident there are no past operations that will introduce side-effects by altering the current state of the DOM
        if (updateQueue.length !== 1) {
            return node;
        }
    }

    //Done diffing so we can now render the updates
    const res = performWork();
    //one edge cases that arises is when we attempt to replace the entire DOM tree (i.e. on first iteration) - we push to the queue 
    //but never assign node which we initialize to `normalize(null)`. This would result in incorrectly updating the DOM to null so we check
    //for this case here
    if (res && node.tag === "") node = res;
    return node;
}
 
 
 
 //This is the internal representation of a vDOM node in Poseidon that we will then render onto the DOM
 //note we don't use the type and props approach of react because we're going to be creating our virtual DOM representation
 const node = {
     //tag i.e. h1, p etc.
     tag: '',
     children: [],
     //any events it's listening to e.g. onclick, onmousedown etc, maps keys of events to listen to to responses
     events: {},
     //map of attributes to values (e.g. {class: "...", id: "../"})
     attributes: {}
 };
 

//pointer to global stylesheet to be used in subsequent reloads
let globalStyleSheet;
//maps components to class-names, used to check if styles for a component have already been delcared
//e.g. when initializing different elements of a list
const CSS_CACHE = new Map();
//global rule index to insert CSS rules sequentially
var ruleIndex = 0;
//helper method user to convert the JSON object the `css` template literal returns into
//a set of styles - this function is recursive and resolves nested JSON CSS objects 
//the logic may seem confusing but we need to wrap a list of nested JSON CSS objects
//and array of CSS rules into a flat structure that resolves the selectors
//To do this, we distinguish between the rules for a given nested selector and nested objects.
//We add rules for a given selector at the end once we've guaranteed there are no more 
//nested JSON objects to parse
const parseCSSJSON = (JSONCSS, containerHash, styleRules, specialTag = false) => {
    const {tag, rules} = JSONCSS;
    //represents the overall text of our CSS
    var text = "";
    var cssTag;
    //boolean variable to mark whether we need to handle the text differently when appending to the
    //stylesheet
    var specialTag = specialTag;
    //if this is a special tag that contains @keyframes or media, we need to remove
    //the inner references to the container component nesting
    if (tag.includes("@keyframes") || tag.includes("@media")) {
        specialTag = true;
        cssTag = tag;
        text += tag + "{\n\n"
    } else {
        //replace references to the container component which was unknown as time of generating 
        //the CSS set of JSON rules
        cssTag = tag.replace("<container>", containerHash);
    }
    var textForCurrentSelector = "";
    //represents the set of rules for the current selector at this level of our tree  
    //only add rules at the current level, if this is not a special tag
    textForCurrentSelector = cssTag + " { \n";
    if (!specialTag) {
    }
    rules.forEach((item, _) => {
        //check if this is a rule or a nested CSS JSON object
        if (item.key) {
            const {key, value} = item;
            textForCurrentSelector += "\t" + key + ":" + value + ";\n";
        } else {
            //then this is a nested JSON tag so we need to recurse
            text += parseCSSJSON(item, containerHash, styleRules, specialTag);
            
        }
    });
    if (specialTag && !text) {
        return textForCurrentSelector + "}";
    }
    //if text is not empty, we are adding a special rule like @media or @keyframes
    if (text) {
        styleRules.push(text + "}");
    } else {
        //add the rules for the current level now that we've finished parsing all of the nested rules
        styleRules.push(textForCurrentSelector + "}");
    }
    return "";
}

const initStyleSheet = (userJSONStyles, name, rules) => {
    const containerHash = CSS_CACHE.get(name);
    //create style tag
    const cssNode = document.createElement('style');
    cssNode.type = 'text/css';
    //identify poseidon set of css rules with a unique data attribute
    cssNode.setAttribute("data-poseidon", "true");
    document.head.appendChild(cssNode);
    globalStyleSheet = cssNode.sheet;
    //add . before class for the css stylesheet
    parseCSSJSON(userJSONStyles, "." + containerHash, rules);
}
const generateUniqueHash = (string) => {
    var hashedString = string;
    // Math.random should be unique because of its seeding algorithm.
    // Convert it to base 36 (numbers + letters), and grab the first 9 characters
    // after the decimal.
    hashedString += Math.random().toString(36).substr(2, 9);
    return hashedString;
}

const injectStyles = (rules) => {
    //add the rules to our stylesheet
    for (const rule of rules) {
        globalStyleSheet.insertRule(rule);
    }
}

//unit of UI
class Component {
    constructor(...args) {
        //initialize stuff
        //vdom from create
        this.vdom = null;
        if (this.init !== undefined) {
            this.init(...args);
        }
        //store object of {source, handler} to remove when taking down a component
        //note, intentionally only store one source and handler for encapsulation
        this.event = {};
        //`this.data` is a reserved property for passing into create to reduce side-effects and allow components to create UI without
        //having to rely on getting the data from elsewhere (can define in it in `init` method of a user-defined component)
        //call render if a component has not already been initialized with a fully-fledged, ready DOM node 
        //(e.g. individual elements in a List)
        if (this.node === undefined) {
            this.render(this.data);
        }
    }
    
    //bind allows us to bind data to listen to and trigger an action when data changes. Similar to useState in React which 
    //triggers a re-render when data changes
    bind(source, handler) {
        if (source instanceof Listening) {
            //if no handler passed in, we assume the callback is just a re-render of the UI because of a change in state
            //handler passed in should be a JS callback that takes data and does something (data = new updated data)
            if (handler === undefined) {
                const defaultHandler = (data) => this.render(data);
                source.addHandler(defaultHandler)
                this.events = {source, defaultHandler};
            } else {
                source.addHandler(handler);
                this.events = {source, handler};
            }
        } else {
            throw 'Attempting to bind to an unknown object!';
        }
    }

    //method for adding inline css styling to components via css template literal, should be added in relevant component
    //by returning a css template literal
    // styles() {
    //     return null;
    // }

    //helper method for adding component-defined styles 
    addStyle(vdom) {
        //call only proceeds if we have custom-defined styles for efficiency
        //obleviates the need for having a separate Styled component - any component
        //that does not implement styles() will not call any of this method's logic
        //and any component can use the styles() API to apply CSS styles on its elements
        if (!this.styles) return ;

        //check if we have a class attribute, otherwise, create one
        if (!vdom.attributes["class"]) {
            vdom.attributes["class"] = "";
        }
        //in order to make sure the styles only get applied to elements in the current component 
        //generate a unique class name - note we don't use a unique ID since we may want to use the same styles
        //for dfferent instances of the same component e.g. different elements of a list
        //first check if the class is not in our CSS_CACHE
        if (!CSS_CACHE.has(this.constructor.name)) {
            const uniqueID = generateUniqueHash(this.constructor.name); 
            vdom.attributes["class"] += " " + uniqueID;
            CSS_CACHE.set(this.constructor.name, uniqueID);
        } else {
            vdom.attributes["class"] += " " + CSS_CACHE.get(this.constructor.name);
        }
        
        //if we don't already have a reference to the globalStyleSheet, we need to create it and populate it with our
        //css rules
        if (!globalStyleSheet) {
            this.regenerateStyleSheet();
        } 
        //note by design we don't check if state has changed and re-generate/re-inject all of the styles
        //Poseidon's API
    }

    //generates a new stylesheet and injects all of the styles into the page. This operation is expensive
    //and should be called infrequently - only if state required to load css changes. As with Poseidon's API
    //any state should be bound to this method to automatically trigger a re-injection when the styles change
    regenerateStyleSheet() {
        const rules = [];
        const name = this.constructor.name;
        //get the JSON object of CSS rules
        const userJSONStyles = this.styles();
        initStyleSheet(userJSONStyles, name, rules);
        injectStyles(rules);
    }

    //performs any cleanup before a component is removed such as invalidating timers, canceling network requests or cleaning any
    //bindings that were made in the init
    remove() {
        //remove handlers of any atomic data defined here
        const {source, handler} = this.events;
        source.remove();
        //reset `this.events` 
        this.events = {};
    }

    //create allows us to compose our unit of component
    //should be deterministic and have no side-effects (i.e. should be rendered declaratively)
    create(data) {
        //eventually will need to do manipulation to convert template string into this format, but start simple for now
        return null;
    }

    //converts internal representation of vDOM to DOM node 
    //used to render a component again if something changes - ONLY if necessary
    render(dataIn) {
        var data = dataIn;
        //only apply render with `this.data` if no parameters passed in, which should take precedence
        if (this.data !== undefined && !data) {
            //if we had set this.data when initializing a component, it should also
            //load the data in a manual call to render
            data = this.data;
        }
        if (data instanceof Atom) {
            data = data.state;
        }         
        //create virtual DOM node
        const newVdom = this.create(data); 
        //TODO: fix this, can't use insertRule if element is not already in the DOM
        //apply any user-defined styles if applicable (do this before we render in case any user-generated styles
        //need to add any properties to the outer vDOM node e.g. a unique id)
        this.addStyle(newVdom);
        //call the reconciliation algorithm to render/diff the changes and render the new DOM tree which we save
        this.node = renderVDOM(newVdom, this.vdom, this.node);
        //return an empty comment if no valid DOM node is returned
        if (!this.node) this.node = document.createComment('');
        this.vdom = newVdom;
        return this.node;
    }
}
//Listening class is used to connect handlers
//to data/models for evented data stores (like in Torus)
class Listening {
    constructor() {
        this.handlers = new Set();
        //represent the current state of the data
        //used to determine when a change has happened and execute the corresponding handler
        this.state = null;
    }
    //return summary of state
    summarize() {
        return null;
    }

    //used to listen to and execute handlers on listening to events
    fire() {
        this.handlers.forEach(handler => {
            //call handler with new state
            //since we pass in the state, this means we have access directly to an atom's data (aka state) in the handler
            //(including a call to render)
            handler(this.state);
        })
    }

    //called when an atom is taken down to remove all subscribed event handlers
    remove() {
        this.handlers.forEach(handler => {
            //remove handler 
            this.removeHandler(handler)
        })
    }
    //add a new handler
    addHandler(handler) {
       this.handlers.add(handler);
       handler(this.state);
    }
    //remove a handler
    removeHandler(handler) {
        this.handlers.delete(handler);
    }
}
 
 
 //atom is smallest unit of data, similar to record in Torus
class Atom extends Listening {
    constructor(object) {
        super();
        super.state = object;
    }

    summarize() {
        return this.state;
    }

    //default comparator should be overrided for custom functionality in atom class
    get comparator() {
        return null;
    }

    //all children of atoms should include a method that returns their type (base implementation provided for general Atom)
    //but should be specific to implementing atom class
    get type() {
        return Atom;
    }
    

    //called to update the state of an atom of data
    //takes in an object of keys to values
    update(object) {
        for (const prop in object){
            this.state[prop] = object[prop];
        }
        //change has been made to data so call handler
        this.fire();

    }

    //used to return a property defined in an atom
    get(key) {
        return this.state[key];
    }

    //convert data to JSON (potentially for persistent store, etc.)
    serialize() {
        return JSON.stringify(this.state); 
    }
 
}
 
//Lists are backed by collection data stores (middle man between database and the UI) to map collections to the UI
class List extends Component {
    //fix constructor with args
    constructor(item, store, remove, ...args) {
        //call super method
        super(...args);
        this.initList(item, store, remove);
        this._atomClass = store.atomClass;
    }

    //helper method which initializes the list nodes 
    initList(item, store, remove) {
        if (!(store instanceof CollectionStore)) throw 'Error unknown data store provided, please use a CollectionStore!'
        this.store = store;
        //check if no remove callback is passed in, in which case we default to using the native `remove` method
        //provided by the store
        if (remove) {
            this.remove = remove;
        } else {
            this.remove = (data) => store.remove(data);
        }
        //domElement is the unit of component that will render each individual element of a list 
        this.domElement = item;
        //backed by Javascript Map since maintains order and implements iterable interface, allowing easy manipulation when looping
        //this items maps atoms as keys to DOM nodes as values. This prevents us having to re-render all DOM list elements, and only
        //re-render the elements that have changed or the ones that need to be added
        this.items = new Map();
        this.nodes = [];
        //will initialize map on first call of itemsChanged() -> binding calls handler the first time
        this.bind(store, () => this.itemsChanged());
    }
    
    itemsChanged() {
        //loop over store and add new elements
        this.store.data.forEach((element) => {
            if (!this.items.has(element)) {
                //pass in the atom to the new initialized component as well as the callback to remove an item from a store
                //so that each component can remove its own atomic data
                //recall that initializing a new element will call render the first time, meaning 
                //we will be able to access the DOM node of this new element below
                const domNode = new this.domElement(element, this.remove);
                //set the value in our items map to an instance of the actual (Poseidon) component
                //This allows us to grab specific components and update them in a higher-order component
                this.items.set(element, domNode);
            } 
        })
        //loop over map and remove old elements
        for (let [key, value] of this.items) {
            if (!this.store.has(key)) {
                this.items.delete(key);
            } 
        }

        //note althought we create an array from instances of our list item components, recall
        //when we instantiated them above, it will have made a call to render so will have access
        //to it's predefined DOM node. In our rendering logic, when we see this, we return the
        //DOM node directly, as opposed to trying to create a DOM node from our vDOM. 
        //This is an important subtelty because if we were to do the latter, we (i.e. a Poseidon component) would not have
        //a reference to the DOM node locally, thus would not be able to update any changes (on the web page)
        //reflected to its state (and a goal of Poseidon is that we have self-managing components 
        //so should be able to display changes to changes in atomic data directly within our own component)
        this.nodes = Array.from(this.items.values())
        this.render(this.nodes);
    }

    get type() {
        return this._atomClass;
    }

    get size() {
        return this.items.size;
    }

    create(data) {
       //default implementation is to return a <ul> of all of the individal nodes, should be overrided if custom rendering
       //needs to be specified
       return html`<ul>
           ${this.nodes}
       </ul>` 
    }

}

function ListOf(itemOf) {
    return class extends List {
        constructor(...args) {
            super(itemOf,...args);
        }
    }; 
}

//middle man between database and the UI. Used to store collections and interface with the UI
//similar to Store in Torus and Collections in Backbone
class CollectionStore extends Listening {
    constructor(data, atomClass) {
        super();
        this._atomClass = atomClass;
        this.setStore(data); 
    }

    //will typically have a fetch and save method to cache data locally from the database to load the UI and save upon rewrites
    //setStore provides a flexible way to intialize a store with data (either via the constructor or e.g. an internal fetch method)
    setStore(data) {
        //4 possible configurations for initalizing a store with data
        //1. Pass in objects with Atom
        //2. Pass in intialized atoms as an array with no type (we're responsible for inferring)
        //3. 1 but via CollectionStoreOf
        //4. 2 but via CollectionStoreOf
        if (data !== undefined && data !== null && data.length > 0) {
            //assume all data is the same type if no atom class is provided (meaning we can infer it directly, since just list of atoms) 
            if (this._atomClass === undefined) {
                this.data = new Set(data);
                //use the first element from the provided list as a heuristic for the type of atomic data of the data source
                this._atomClass = data[0].type;
            } else {
                if (data[0] instanceof Atom) {
                    this.data = new Set(data);
                } else {
                    this.data = new Set(data.map(el => new this._atomClass(el)));
                }
            }       
        } else {
            this.data = new Set();
        } 
        //should emit an event for any handlers to act on
        this.fire();
    }

    summarize() {
        return JSON.stringify(this.data);
    }

    add(newData) {
        if (newData instanceof Atom) {
            this.data.add(newData);
            if (this._atomClass === undefined) {
                this._atomClass = newData.type;
            }
        } else {
            if (!this._atomClass) throw "Error, adding a non-atom object without a defined atom class!"
            this.data.add(new this._atomClass(newData));
        } 
        //trigger any event handlers that are subscribed to the store for an update
        this.fire();
    }

    has(value) {
        return this.data.has(value);
    }

    remove(oldData) {
        //remove atom from the store
        this.data.delete(oldData);
        //call atom's remove to remove all subscribed event handlers
        oldData.remove();
        //trigger any event handlers that are subscribed to the store for an update e.g. a re-render if the store was bound
        //to a component
        this.fire();

    }

    //return JSON serialized data sorted by comparator
    serialize() {
        //creates array with spread syntax, then sorts
        //not cross-compatible with some older versions of browsers like IE11
        const sorted = [...this.data];
        sorted.sort((a , b) => {
            return a.comparator - b.comparator;
        });
        return JSON.stringify(sorted);
    }

    get size() {
        return this.data.size;
    }

    get atomClass() {
        return this._atomClass;
    }

    //define customer iterator interface so we can loop over stores directly
    //take advantage of the iterator values() returns since data is a Javascript set 
    [Symbol.iterator]() {
        return this.data.values();
    }
}

//Higher order component pattern like in Torus for defining a CollectionStore of a specific record
function CollectionStoreOf(classOf) {
    return class extends CollectionStore {
        constructor(data) {
            super(data, classOf);
        }
    };
}

//helper method to convert passed in paths into executable regex values to match against incoming routes
const getRegexFromRouteString = (route) => {
    let match;
    let paramNames = []
    //construct a new regex match by replacing paramnames as defined in the route e.g. /:user
    //with corresponding regex bits to match any possible values
    route = route.replace(/[:*](\w+)/g, (full, paramName, _) => {
        paramNames.push(paramName);
        //replace any paramname with a regex to match any value (since any value can be passed in as a parameter e.g. any user!)
        //matches any character that is not a /
        return '([^\/]+)'
    });
    //may be the end of the route or may there may be more stuff, so add a regex to capture this
    route += '(?:\/|$)'
    return [new RegExp(route), paramNames];
}


//NOTE: this is a client-side router. This means that when the URL changes, it looks at the client-side
//Javascript code to determine what to render. This means that if you're using any kind of web server
//and serving the static content from there, either you must allow ALL routes or the routes you'd like to 
//define on the client-side on the web server. If you don't do this, nothing will be served once you navigate to a route 
//even if you've specified what to render through Poseidon.
class Router {
    //client-side router, match-based router (i.e. builds a routing table)
    //constructor takes an object which maps names of routes to their corresponding path
    //when passing routes, make sure to pass more general routes later since Poseidon will match them
    //in that order
    constructor(numRoutes = 0) {
        this.routes = new Map();        
        //set the pathname of the current route
        this.currentPath = window.location.pathname;
        this.options = {
            context: window,
            startListening: true
        }
        this.numRoutes = numRoutes;
        this.numRegistered = 0;
        this.matchHelper = () => {
            return this.match(window.location.pathname);
        }
        //used to detect when URL changes and execute a handler accordingly
        window.addEventListener('popstate', this.matchHelper); 
    }

    get currentRoute() {
        return this.currentPath;
    }

    //gets the query parameters from a route
    getQueryParameters(queryParameters, routeParams) {
        const urlSearchParams = new URLSearchParams(queryParameters);
        const dictParams = Object.fromEntries(urlSearchParams.entries());
        Object.keys(dictParams).forEach(key => {
            routeParams[key] = dictParams[key];
        });
    }

    //route-matching algorithm
    //listener method for when the URL or hash changes to map to the new appropriate view
    match(route) {
        //match route against dictionary of defined paths to their relevant attributes
        for (let [path, {pathRoute, handler, params}] of this.routes) {
            const match = pathRoute.exec(route);
            //each route will be associated with a handler
            //this handler will handle all of the rendering associated with a new change
            if (match !== null) {
                //remove the first / from the route
                //loop through values and add each value with its associated parameter
                const routeParams = match.slice(1).
                                    reduce((allParams, value, index) => {
                                            allParams[params[index]] = value;
                                            return allParams;
                                            }, {});
                //split parameters using the ?varName=varVal 
                this.currentPath = path;
                //check if we have any query parameters to parse
                if (window.location.search) {
                    this.getQueryParameters(window.location.search, routeParams);
                }
                handler(route, routeParams);
                //we don't want to execute more than route once we've matched one
                //if it can match multiple ones so we break
                break;
            }

        }
    }

    //navigate method provided for convenience in events like button actions
    navigate(path, {replace = false} = {}) {
        if (window.location.pathname != path) {
            if (replace) {
                this.options.context.history.replaceState(null, document.title, path);
            } else {
                //add entry to browser's session history stack (will set the location's hash)
                this.options.context.history.pushState(null, document.title, path);
            }
            this.match(path);
        }
    }

    //used to map paths to handler functions which will get executed when navigated to
    on(...pageRoutes) {
        //increment the number of routes registered
        this.numRegistered += 1;
        for (const {route, handler} of pageRoutes) {
            if (Array.isArray(route)) {
                for (const path of route) {
                    const [regPath, params] = getRegexFromRouteString(path);
                    this.routes.set(path, {pathRoute: regPath, handler: handler, params: params});
                }
            } else {
                const [regPath, params] = getRegexFromRouteString(route);
                this.routes.set(route, {pathRoute: regPath, handler: handler, params: params})
            }
        }
        //route the current url once we've registed all the handlers
        if (this.numRegistered === this.numRoutes) {
            this.match(window.location.pathname);
        }
    } 
}

//jsx like parser written in Javascript for Poseidon's vdom

//Reader class to abstract lexing and scanning of a vdom template string
class Reader {
    constructor(string, specialCharacters) {
        //need to replace all backslashes with double backslash to make sure it's correctly rendered
        this.string = string;
        this.index = 0;
        this.length = string.length;
        //set of special characters to return when getNextWord is called
        this.specialCharacters = new Set(specialCharacters);
    }

    peek() {
        if (this.index < this.length - 1) {
            return this.string[this.index + 1];
        }
        return null;
    }

    //gets the next word, keeps moving forward until until it encounters one of the special tags or a closing '/>'
    //takes a positional parameter that by default will only return values inside of quotes as opposed to
    //the entire string with quotes. Can pass true to get the entire string with quotes to override this
    //behavior
    getNextWord(includeQuotes = false) {
        var currIndex = this.index;
        var finalIndex = currIndex;
        var quoteCount = 0;
        //keep looping while we don't encounter a special character of if we're inside a quote
        while ((this.index < this.length) && (!this.specialCharacters.has(this.currentChar) || (!includeQuotes && quoteCount === 1))) {
            //if we have quotes, skip them
            //TODO: add more robust type checking we have the same type of quote
            if (!includeQuotes && (this.currentChar === '"' || this.currentChar === "'")) {
                //adjust starting point of returned work if we encounter an opening quote
                if (quoteCount === 0) { 
                    quoteCount += 1;
                    currIndex = this.index + 1;
                } else if (quoteCount === 1) {
                    finalIndex = this.index - 1;
                    quoteCount += 1;
                }  
            } else if (this.currentChar === '/') {
                //handle special case where next word might be adjacent to a /> tag so return the word before
                //this tag
                //otherwise, since this is
                if (this.peek() === '>') break
            } else {
                finalIndex = this.index;
            } 
            this.consume();
        }
        if (quoteCount == 1) { 
            throw 'Error parsing quotes as values!';
        }

        //skip any spaces for future
        this.skipSpaces();
        return this.string.substring(currIndex, finalIndex + 1);
    }

    get currentChar() {
        return this.string[this.index];
    }

    //skip all white spaces and new line characters
    skipSpaces() {
        while (this.currentChar === " " || this.currentChar === '\n') {
            this.consume();
        }
    }

    consume() {
        return this.string[this.index++];
    }

    //combination of consume and skipping white space since this pattern crops up frequently
    skipToNextChar() {
        this.consume();
        this.skipSpaces();
    }

    //helper method to keep moving pointer until the current char is the provided one
    getUntilChar(char) {
        const currIndex = this.index;
        var finalIndex = currIndex;
        while (this.currentChar != char && this.index < this.length) {
            this.consume();
            finalIndex = this.index;
        }
        return this.string.substring(currIndex, finalIndex);
    }
    //adapted helper method of above to keep moving pounter until the current word is the provided one
    getUntilWord(word) {
        var found = false;
        //edge case where no spaces betwen '<--' and '-->
        if (this.currentChar === '>') {
            found = true;
        }
        while (!found && this.index < this.length) {
            this.getUntilChar(word[0]);
            //note getUntilChar does not consume the character we pass in, so we start comparing each character of the word
            //at index 0
            for (let i = 1; i < word.length; i++) {
                this.consume();
                if (this.currentChar === word[i]) {
                    found = true;
                } else {
                    //exit for loop and go back to the while loop
                    found = false;
                    break
                }
            }
        }
        this.skipToNextChar();
    }

    //keep moving pointer forward until AFTER we encounter a char (i.e pointer now points to character after matching provided)
    skipPastChar(char) {
        var text = this.getUntilChar(char);
        text += this.consume();
        return text;
    }
}


//recursively loop checking children
const parseChildren = (closingTag, reader, values) => {
    try {
        let children = [];
        //check in the scenario where we have an empty HTML node with no children
        if (foundClosingTag(closingTag, reader)) {
            return children;
        }
        var nextChild = parseTag(reader, values);
        while (nextChild !== CLOSED_TAG) {
            //only append child if it's not null or undefined
            if (nextChild) {
                //check if this is the result of returning an array (e.g. if a map operation is called), in which case, we set children 
                //to the result otherwise introducing nesting which will cause issues when trying to render
                if (Array.isArray(nextChild)) children = nextChild
                else children.push(nextChild);
            }
            if (foundClosingTag(closingTag, reader)) break;
            nextChild = parseTag(reader, values);
        }
        return children;
    } catch (e) {
        throw e;
    }
}


//helper method to check if we've encountered the closing tag of a node
//return true if we have and false if we have not encountered the closing tag
const foundClosingTag = (closingTag, reader) => {
    if (reader.currentChar === '<' && reader.peek() === '/') {
        //if we encounter closing tag i.e. '</' then end parsing of this tag
        reader.skipPastChar('/');
        const nextTag = reader.getNextWord();
        reader.skipPastChar('>');
        if (nextTag !== closingTag) throw 'Error parsing the body of an HTML node!'
        return true;
    }
    return false
}

//method which parses JS expressions in our template literal vdom string
//takes a reader, list of values from the template string, and an optional attribute variable that indicates whether this expression
//should return a node (i.e. call parseTag) or return a value associated with some key (e.g an attribute)
const parseJSExpr = (reader, values, attribute) => {
    //return the Javascript expression
    //What's a cleaner way of doing this
    var val = values.shift();
    //if the value returns null we don't want to render anything
    if (val) {
        //if this is a JSX expression associated with some key, return the value obtained directly instead of parsing it as a HTML node
        if (attribute) {
            reader.skipSpaces();
            //if the val either a function or an object which was generated
            //by a nested vdom template literal, we return it directly
            //otherwise, we cast any other non-string primitives if the returned value is not already a string to prevent unnecessary computations
            if (typeof val === 'function' || typeof val === 'object') return val;
            else if (typeof val !== 'string') val = String(val);
            return val;
        }
        //Not DRY, but the alternative is some hard to understand gymnastics
        if (typeof val === 'object' || typeof val === 'function') {
            reader.skipSpaces();
            //if an anonymous function is passed in as a body execute it
            if (typeof val === 'function') {
                return val();
            } else {
                return val;
            }
        } else if (typeof val !== 'string') {
            val = String(val)
        } 
        //To prevent executing any HTML from Javascript variables which would expose
        //a risk of cross-scriping attacks, if there's any HTML content in our string, we don't parse into HTML nodes
        //but returns it as text instead.
        //Note we only need to check if a string starts with < because if the string starts with any other text
        //then `parseTag` will assume it's a text token and consume characters until it finds an opening < until which it stops
        //This means a string like `test<script>alert("hello")</script>` would not cause any issues because the recursive call
        //would stop as soon as it hits the opening < of the script tag, effectively ignoring any other HTML, and thus malicious content
        if (val.startsWith("<")) {
            console.log('Warning, attempting to inject HTML into the page, this is a security risk and should be avoided. ', val);
            return val;
        }

        //notice this set-up nicely allows for nested vdom expressions (e.g. we can return another vdom template literal based on some
        //Javascript expression within another vdom)
        const readerNewExpression = new Reader(val, reader.specialCharacters);
        return parseTag(readerNewExpression, values); 
    } else {
        return null;
    }
}


//parse a complete HTML node tag 
const parseTag = (reader, values) => {
    //if the current char is not a < tag, then either we've finished parsing valid tags or this is a text node
    if (reader.currentChar !== '<') {
        const word = reader.getUntilChar('<');
        //we've reached the end of parsing
        if (!word) return null;
        //otherwise, we've found a text node!
        return {tag: "TEXT_ELEMENT", nodeValue: word};
    } else if (reader.peek() === '/') {
        //just encountered a '</' indicating a closing tag so return the constant to let the caller method know 
        //note we return this constant (instead of null) to differentiate from null nodes which may not necessarily be the last nodes 
        //left to parse
        return CLOSED_TAG;
    }  
    //skip < tag
    reader.consume();
    const name = reader.getNextWord();
    //check if this is a placeholder for a JS expression or if this is a comment node
    if (name === VDOM_PLACEHOLDER) {
        //skip < tag
        reader.consume();
        return parseJSExpr(reader, values, false);
    } else if (name.startsWith("!--")) {
        //note we don't check for '<!--' because we consume it on line 202
        //skip until '-->' tag
        reader.getUntilWord('-->');
        return parseTag(reader, values);
    } 
    const node = {
        tag: name,
        children: [], 
        attributes: {},
        events: {}
    }
    //boolean variable to handle special self-closing HTML nodes like <img />
    var specialChar = false;
    //Match key-value pairs in initial node definition (i.e. from first < to first > tag, recall closing node tag is </)
    //TODO: fix infinite loop if missing closing tag, need to test
    while (reader.currentChar !== '>' && reader.index < reader.length) {
        const key = reader.getNextWord();
        //handle special self-closing tags like <br/> and <img />
        if (key === '/' && reader.peek() === '>') {
            reader.consume();
            specialChar = true;
            break;
        } 
        //key on its own is still valid, so check if we need to map to a specific value
        if (reader.currentChar !== '=') {
            node.attributes[key] = true;
            continue;
        }
        //skip equal sign
        reader.skipToNextChar();
        //get value associated with this key 
        let value = reader.getNextWord();
        //getNextWord stops at some special characters, one of which is < which is the start of the VDOM_JSX_Node
        //so check if this is a placeholder before parsing the JS expression to get the value associated with this key
        if (value === '<') {
            //skip < tag and check if this is a valid placeholder
            reader.consume();
            if (reader.getNextWord() === VDOM_PLACEHOLDER) value = parseJSExpr(reader, values, true);
            else throw "Error trying to parse the key-value pairs of a node, unexpected < found!"
            //skip closing tag
            reader.skipToNextChar();
        } else {
            //replace any template literals inside the string value if they exist with their corresponding values
            while (value.includes(VDOM_JSX_NODE)) {
                value = value.replace(VDOM_JSX_NODE, parseJSExpr(reader, values, true) );
            }
        }
        //if the key starts with an on, this is an event, so we should save it accordingly
        if (key.startsWith("on")) {
            //note keys of events in JS don't include on, so we ignore this part of the string when assigning it
            node.events[key.substring(2)] = value;
        } else {
            //otherwise, this is an attribute so add it there
            node.attributes[key] = value;
        }
    }
    //skip closing > of node definition and any spaces/new lines
    reader.consume();
    //match actual body of the node if this is not a self-closing HTML tag like <img />
    if (!specialChar) node.children = parseChildren(name, reader, values);
    //return JSON-formatted vdom node
    return node;
}

//Regular expression to match all expressions (or JS codes) inside a dom template string
//This lazily matches (lazily meaning as few as possible) any '${}' characters within a template string
const VDOM_EXPRESSIONS = new RegExp('\${.*?}', 'g');
//use the current Date or time to ensure we have a unique placeholder in our template strings which will replace
//all Javascript expressions (i.e. ${}) that need to be executed which we refer to during the parsing phase
const VDOM_PLACEHOLDER = `__vdomPlaceholder${Date.now()}`;
//we wrap the placeholder in opening and closing tags to avoid checking extra edge cases in our parser which would introduce
//extra, unneccessary computations
const VDOM_JSX_NODE = '<' + VDOM_PLACEHOLDER + ">"
//constant used when parsing children nodes to check whether we've finished parsing all child nodes and have found the closing parent
const CLOSED_TAG = '</';
//a unique string that will be used to map outer-level css rules inside a component (that don't have a user-defined selector)
//when constructing the CSS JSON set of rules to the outer-level node in that component at render-time when adding the CSS rules
//to the page
const CSS_PLACEHOLDER = `__container${Date.now()}`;
//note we wrap the placeholder with a {}, since this is a special character which will let the reader stop at the correct position 
//and not overshoot 
const CSS_JSX_NODE = '{' + CSS_PLACEHOLDER + '}';


//check what to do with outer component for CSS

//take advantage of Javascript template literals which gives us a string and a list of interpolated values
const html = (templates, ...values) => {
    //create string and interpolate all of the ${} expressions with our constructed placeholder node 
    const vdomString = templates.join(VDOM_JSX_NODE, values);
    //HTML parsing
    const reader = new Reader(vdomString, [' ', '=', '<', '>']);
    try {
        reader.skipSpaces();
        const node = parseTag(reader, values);
        return node;
    } catch (e) {
        console.error(e);
        return null;
    }
}

//parses body of the CSS and returns a dictionary of type {tag: `string`, rules: []} with arbitrary nesting of other 
//css objects or {key: value} representing a CSS rule
const parseCSStringToDict = (reader, dict, selector, values) => {
    dict["tag"] = selector;
    dict["rules"] = [];
    while (reader.index < reader.length) {
        var word = reader.getNextWord();
        //to prevent an infinite loop and fail gracefully, check if the word is a special character
        //which we don't check later on
        if (word === ';') { 
            console.log(reader.string.substring(reader.index));
            throw 'Error, unexpected end of expression found!';    
        }
        //check if this is JS expression
        if (word === '{') {
            //consume the { token
            reader.consume();
            const placeholder = reader.getNextWord();
            //found a JS expression which is a function call, likely to be a call to another css template literal
            if (placeholder === CSS_PLACEHOLDER) {
                const res = values.shift();
                //if the value returned from a function call is null, ignore it
                if (res) {
                    //since this is a nested call to the css function, we need to "unwrap it" because a call from css
                    //wraps the outer JSON in an object corresponding to the current component we are in. 
                    //In this case we are already nesting styles in
                    //the component wrapper so we can get rid of it
                    res["rules"].forEach((objectStyles, _) => {
                        //need to append the current selector to any nested rules
                        //note we remove `<container> from the result of the nested css template function
                        //call to prevent duplicates in our selector
                        objectStyles["tag"] = selector + objectStyles["tag"].replace("<container>", "");
                        //add the styles
                        dict["rules"].push(objectStyles);    
                    });
                } 
                reader.skipToNextChar();
                continue
            } else {
                throw 'Invalid curly brace found in css template literal!'
            }
        } 

        //we don't directly use the reader's currentChar variable since there are some edge
        //cases where we need to do some lookahead operations and will need to adjust it on the fly
        //to execute the correct logic
        var char = reader.currentChar;

        //may be a key-value pair or a selector, need to lookahead
        if (reader.currentChar === ':') {
            reader.skipToNextChar();
            //some css selectors have `:` in them e.g :hover or ::before, so we need to check if this is a selector
            //or a key-value pair
            //first check if we have the complete word, or if this is a special :: selector case
            if (reader.currentChar === ':' ) {
                word += "::"; //directly add the ::s, first one at line 341 that we skipped, and the current one
                //consume the second :
                reader.consume(); 
                word += reader.getNextWord();
                reader.skipSpaces();
                //this must be a css selector and not a key-value pair so reset char  
                char = reader.currentChar
            }
            if (char !== '{') { 
                //make sure to get the result with quotes in case any values rely on it 
                //to correctly render CSS e.g. content
                var value = reader.getNextWord(true);
                //check if we have a JS expression as the value for a key
                if (value === '{') {
                    //skip the {
                    reader.consume();
                    const constant = reader.getNextWord();
                    if (constant !== CSS_PLACEHOLDER) throw 'Invalid JS expression while trying to parse the value of a key!';
                    value = values.shift();
                    //skip past the } of the 
                    reader.skipToNextChar();
                } 
                //check if this is a css selector with a specific colon like :before, in which case the reader would be 
                //pointing to a {
                if (reader.currentChar === '{') {
                    word += ":" + value;
                    char = reader.currentChar; //adjust char to a { so we correctly parse it as a selector at line 366
                } else if (reader.currentChar === ':') {
                   //this is a media rule or a css selector with two colons e.g. @media and (min-width: 800px) and (max-width: 800px)
                    reader.consume();
                    const next = reader.getNextWord();
                    //trim for consistency
                    word += ":" + value  + ":" + next.trimStart();
                    char = reader.currentChar;
                } else {
                    //otherwise, we've encountered a key-value pair
                    dict.rules.push({key: word, value: value});
                    //consume ; at the end of a rule and skip any spaces
                    reader.skipToNextChar();
                }
            }         
        } 
        //this is a selector with some associated css rules i.e. <name> {key1: rule1....}
        if (char === '{') {
            reader.skipToNextChar();
            //nested tag, recursive call here
            const nestedTagDict = {};
            dict.rules.push(nestedTagDict);
            //TODO: standarize spacing, necessary?
            var newSelector = selector + " " + word;
            //if the tag, or next selector is a keyframe or media, we don't want to append the previous selector
            //since these are special tags which should be handled differently
            if (word.includes("@keyframes") || word.includes("@media") || 
                dict["tag"].includes("@keyframes") || dict["tag"].includes("@media")) {
                newSelector = word;
            } 
            //note for the new selector, we append the current selector (i.e. child) to the parent
            //to capture all descedants of the parent that correspond to this specific child.
            //This prevents us from having to do this logic ad-hoc when we parse our dict into
            //our eventual stylesheet
            parseCSStringToDict(reader, nestedTagDict, newSelector, values);
            //skip closing } and any spaces
            reader.skipToNextChar();
        }
        //check if we've reached the end of a block-scoped {} of key-value pairs
        if (reader.currentChar === '}') {
            //note we don't consume the '}' since we delegate the responsibility to the caller to do that
            //allows us to more reliably manage our position / prevents inconsistencies with multiple nested tags on the same level 
            break;
        }
    }
    return dict;
}

const css = (templates, ...values) => {
    //create string and interpolate all of the ${} expressions with our constructed placeholder node 
    const cssString= templates.join(CSS_JSX_NODE, values);
    //remove any comments
    const cssCommentsRegex = new RegExp('(\\/\\*[\\s\\S]*?\\*\\/)', 'gi');
    const cssWithoutComments = cssString.replace(cssCommentsRegex, '');
    const reader = new Reader(cssWithoutComments, [';', '{', '}', ':']);
    try {
        reader.skipSpaces();
        const dict = {};
        parseCSStringToDict(reader, dict, "<container>", values);
        return dict;
    } catch (e) {
        console.error(e);
        return null;
    }
}