<div>
folder <span id='folder-container'></span>
</div>

<script>
  import { uuid } from "utils"
  import { UNIT_DIR, MAIN_DIR } from './../utils/dir.js';
  import { getDirectoriesIn, selectionForList, getProgramPath, getFileMap } from './code-tools.js';

  import babelDefault from 'systemjs-babel-build';
  const babel = babelDefault.babel;
  const t = babel.types;
  const template = babel.template;

  class ProgramGraph {
    
    static query(query) {
      return lively.query(this.ctx, query)
    }

    static async create(ctx) {
    
      this.ctx = ctx

      var currentDirectory = MAIN_DIR
      var container = this.query("lively-container");
      var graphviz = await (<graphviz-dot></graphviz-dot>)

      const directories = await getDirectoriesIn(UNIT_DIR);

      const urlElement = selectionForList(directories, value => {
        currentDirectory = value
        this.updateTable()
      })
      urlElement.value = currentDirectory
      
      const urlContainer = this.query("span#folder-container")
      urlContainer.prepend(urlElement)

      let edges, nodes, selectedNode;
      let pane, svgNodes

      this.updateTable = async () => {

        details.innerHTML = ""

        edges = new Set()
        nodes = []

const SolidBoxStyle = `shape=box fontsize="8" fontname="helvetica"`;
const DashedBoxStyle = `shape=rectangle style="dashed" fontsize="8" fontcolor="gray" color="gray" fontname="helvetica"`;
const DottedBoxStyle = `shape=rectangle style="dotted" fontsize="8" fontcolor="gray" fontname="helvetica"`;


const allNodes = new Set();

const NodePathPrototype = getProgramPath('').constructor.prototype;
Object.assign(NodePathPrototype, {
  getDetails: function() {
    return this.getSource();
  },
  getStyle: function() {

    if (this.isFunctionDeclaration()) {
      var file = this.myFileInfo();
      var color = ' ';
      if (file) {
        color += `colorscheme=set19 color=${file.colorIndex}`;
      }

      // simple named expression
      var body = this.get('body');
      if(body.isBlockStatement() && body.node.body.length === 1 && body.get('body')[0].isReturnStatement()) {
        return DottedBoxStyle + color;
      }

      function isSave(path) {
        if (!path) { return true; }
        if (Array.isArray(path)) { return path.every(isSave); }
        if (path.isBlockStatement()) { return path.get('body').every(isSave); }
        if (path.isIfStatement()) { return isSave(path.get('consequent')) && (!path.has('alternate') || isSave(path.get('alternate'))); }
        if (path.isVariableDeclaration()) { return true; }
        if (path.isReturnStatement()) { return true; }
        return false;
      }
      if(isSave(body)) {
        return DashedBoxStyle + color;
      }
    }

    return SolidBoxStyle + color;
  },
  myProgram: function() {
    return this.find(p => p.isProgram());
  },
  myFileInfo: function() {
    var prog = this.myProgram();
    return fileMap.get(prog.absoluteFilePath);
  },
  addToGraph: function() {
    if (allNodes.has(this)) { return; }
    allNodes.add(this);
    var file = this.myFileInfo();
    var asNode = key(this) + styles(this.getStyle(), label(this))
    if (file) {
      file.nodes.push(asNode);
    } else {
      nodes.push(asNode);
    
    }
  }
});

        edges.clear();
        nodes.length = 0;
      
        const keys = new Map();
        function key(thing) {
          return keys.getOrCreate(thing, t => '_'+uuid().replace(/-/g, '_'));
        }
        
        function styles(...styles) {
          return '[' + styles.join(' ') + ']';
        }
        function isNodePath(thing) {
          return thing && thing.constructor && thing.constructor.name === "NodePath";
        }
        function calcLabel(thing) {
          if (!thing) { return 'undefined'; }
          if (isNodePath(thing)) {
            if (thing.isFunctionDeclaration()) {
              const namePath = thing.get('id');
              if (namePath && namePath.node && namePath.node.name) {
                return namePath.node.name;
              }
            }
            if (thing.isImportSpecifier()) {
              const namePath = thing.get('local');
              if (namePath && namePath.node && namePath.node.name) {
                return namePath.node.name;
              }
            }
            return thing.node.type
          }
          return thing && thing.name || thing;
        }

        function label(thing) {
          return `label="${calcLabel(thing)}"`;
        }
        function styles(...styles) {
          return '[' + styles.join(' ') + ']';
        }

        var DashedEdgeStyle = `color="gray" style="dashed" arrowhead="open" arrowsize=.7`
        const SolidEdgeStyle = 'color="gray50" arrowhead="open" arrowsize=.7';
        var CallEdgeStyle = `color="blue" style="dashed" arrowhead="open" arrowsize=.7`
        function addGraphEdge(a , b, style="") {
          a.addToGraph();
          b.addToGraph();
          edges.add(key(a)  + " -> " + key(b) + styles(style));
        }

        // FIND ALL FILES
        var fileMap = new Map();
        var colorIndex = 0;
        const heroes = ['orcus', 'loren', 'melissa', 'riesz'];
        const files = heroes.map(hero => currentDirectory + hero + '-program.js');
        var fileMap = await getFileMap(files, (info, filePath) => {
          info.program.absoluteFilePath = filePath;
          Object.assign(info, {
            nodes: [],
            colorIndex: (colorIndex++ % 9) + 1,
          });
        });

        // FIND FUNCTION DECLARATIONS
        for (let [file, { program }] of fileMap.entries()) {
          program.traverse({
            FunctionDeclaration(path) {
              const namePath = path.get('id');
              if (namePath && namePath.node && namePath.node.name) {
                path.addToGraph();

                path.traverse({
                  CallExpression(p) {
                    const callee = p.get('callee');
                    if (!callee.isIdentifier()) { return; }
                    var binding = callee.scope.getBinding(callee.node.name);

                    if (binding) {
                      if (binding.kind === 'module') {
                        var importPath = binding.path.find(p => p.isImportDeclaration())
                        const importedFile = System.normalizeSync(importPath.node.source.value, file);
                        var iFile = fileMap.get(importedFile);
                        if (iFile) {
                          iFile.program.traverse({
                            FunctionDeclaration(p) {
                              const namePath2 = p.get('id');
                              if (namePath2 && namePath2.node && namePath2.node.name ===  callee.node.name) {
                                addGraphEdge(path, p, SolidEdgeStyle);
                              }
                            }
                          })
                        }

                      } else if (binding.kind === 'param') {
                        // do nothing
                      } else {
                        addGraphEdge(path, binding.path, CallEdgeStyle);
                      }


                    }

                  }
                });
              } else {
                lively.warn('some function declarations have no name');
              }
            }
          })
        }

        graphviz.innerHTML = `<script type="graphviz">
        digraph G {
layout=dot;

  ${Array.from(fileMap, ([path, info]) => {
  const name = path
    .replace(/.*\//g, '')
    .replace('-program.js', '')
    .replace('.js', '')
  	return `subgraph cluster_${name
    } {
    style=filled;
		color=lightgrey;
    ${info.nodes.length > 0 ? info.nodes.join(";")+';' : ''}
		label = "${name}";
    colorscheme=pastel19
		color=${info.colorIndex}
	}`;
  }).join(`
  `)}

          ${Array.from(edges).join(";")} 
          ${nodes.join(";")} 
}<` + `/script>}`;

        await graphviz.updateViz();

        svgNodes = graphviz.shadowRoot.querySelectorAll("g.node")
        lively.warn(svgNodes.length);
        svgNodes.forEach(ea => {
          ea.addEventListener("click", async (evt) => {
            function thingForKey(key) {
              const pair = keys.toPairs().find(([thing, k]) => k === key)
              if (pair) {
                return pair[0]
              }
            }
            var key = ea.querySelector('title').textContent;
            const thing = thingForKey(key);
            if (!thing) { return; }
            
            if (evt.shiftKey) {
              lively.openInspector(thing)
              return
            }

            // hide previous selected node
            if (selectedNode) {
              selectedNode.querySelector("polygon, ellipse").setAttribute("fill", "none")
            }
            // toggle details by clicking it
            if (selectedNode == ea) {
              selectedNode = null
              details.innerHTML = ""
              lively.setClientPosition(details, lively.pt(0,0)) // move out of  the way
              return
            }
            
            selectedNode = ea
            selectedNode.querySelector("polygon, ellipse").setAttribute("fill", "lightgray")

            if (isNodePath(thing)) {
              details.innerHTML = thing.getDetails();
            } else {
              details.innerHTML = thing
            }

            // JSON.stringify(change, undefined, 2)
            lively.setClientPosition(details, lively.getClientBounds(selectedNode).topRight().addPt(lively.pt(10,0)))
          })
        })

        lively.sleep(0).then(() => {
          if (pane) {
            let pos = lively.getClientPosition(_.first(svgNodes))
            let panePos = lively.getClientPosition(pane)        
            let delta = pos.subPt(panePos)
            pane.scrollLeft = delta.x - lively.getExtent(pane).y / 2
            pane.scrollTop = delta.y - 100
            // lively.notify("scroll to: " + delta )

          } else {
            // lively.notify("no pane to scroll into...")
          }
        })        
      }

      var details = <div id="details"></div>
      this.updateTable()

      var style = document.createElement("style")
      style.textContent = `
      td.comment {
        max-width: 300px
      }
      div#root {
        overflow: visible;
        width: 5000px;
        height: 800px;
      }
      div#details {
        position: absolute;
        font-family: monospace;
        white-space: pre;
        font-size: 8pt;
        background-color: lightgray;
        border: 1px solid gray;
        padding: 5px;
      }
      `
      
            
      graphviz.style.display = "inline-block" // so it takes the width of children and not parent
      // z-index: -1;
      pane = <div id="root" style="position: absolute; top: 20px; left: 0px; overflow-x: auto; overflow-y: scroll; width: calc(100% - 0px); height: calc(100% - 20px);">
        {style}
         <div style="height: 20px"></div>
        <h2>Behavior Graph (Functions)</h2>
        {graphviz}
        {details}
      </div>
      
      
      var lastMove
      function onPanningMove(evt) {
        var pos = lively.getPosition(evt)
        var delta = pos.subPt(lastMove)
        pane.scrollTop -= delta.y
        pane.scrollLeft -= delta.x
        lastMove = pos

      }
      
      function onPanningDown(evt) {
        lastMove = lively.getPosition(evt)
        lively.addEventListener("changegraph", document.body.parentElement, "pointermove", evt => onPanningMove(evt))
        lively.addEventListener("changegraph", document.body.parentElement, "pointerup", evt => {
          lively.removeEventListener("changegraph", document.body.parentElement)
        })
        evt.stopPropagation()
        evt.preventDefault()
      }
      
      // always drag with ctrl pressed
      pane.addEventListener("pointerdown", evt => {
        if (evt.ctrlKey) {
          onPanningDown(evt)          
        }
      }, true)
      
      // but if nothing else... normal drag will do
      pane.addEventListener("pointerdown", evt => {
        var element = _.first(evt.composedPath())
        lively.notify("element " + element.localName)
        if (element.localName == "polygon") {
          onPanningDown(evt)
        }
      })

      return pane
    }
  }  

  ProgramGraph.create(this)
</script>