
define([
    "backbone", "js-yaml-parser", "underscore"
], function (Backbone, JsYamlParser, _) {

    var BrooklynYamlCompletionProposals = {};

    var Catalog = Backbone.Collection.extend({
        initialize: function(models, options) {
            this.name = options["name"];
            var that = this; 
            var model = this.model.extend({
              url: function() {
                return "/v1/" + that.name + "/" + this.id.split(":").join("/");
              }
            });
            _.bindAll(this);
            this.model = model;
        },
        url: function() {
            return "/v1/" + this.name;
        }
    });
    // currently populates catalog on startup, then refreshes on each search
    // (but only renders later)
    // ideally should be shared model refreshed periodically in background
    var catalogE = new Catalog(undefined, { name: "catalog/entities" });
    var catalogA = new Catalog(undefined, { name: "catalog/applications" });
    var catalogL = new Catalog(undefined, { name: "locations" });
    // if browser opened while server is starting this will fail on first completion
    catalogA.fetch();
    catalogE.fetch();
    catalogL.fetch();

    function findParseNodeAt(parse, position) {
      if (parse.start<=position && parse.end>=position) {
        for (var ci in parse.children) {
          var c = parse.children[ci];
          var result = findParseNodeAt(c, position);
          if (result) return result;
        }
        return parse;
      }
      return null;
    }
    
    /** adds additional fields to a parse node:
     *  * type: map, list, primitive, null
     *  * role: key, value, entry, root; or primitive if we are a primitive in a primitive (due to how it is parsed) 
     *  * key: if we are role 'value', what is our key
     *  * index: if parent is map or list, what is our position in that as a list
     *  and returns a list of the containing parse nodes, root first
     */
    function findContexts(n, position) {
        if (!n || n.role) return;
        
        if (n.result === null) {
            n.type = 'null';
        } else if (n.kind == 'sequence') {
            if (typeof n.result.length === 'undefined') {
                console.log("WARN: mismatch expected list but had no length", n);
            }
            n.type = 'list';
        } else if (n.kind == 'mapping') {
            if (typeof n.result.length !== 'undefined') {
                console.log("WARN: mismatch expected map but had length", n);
            }
            n.type = 'map';
        } else {
            if (n.kind != 'scalar') {
                console.log("WARN: mismatch expected scalar/primitive", n);
            }
            n.type = 'primitive';
        }
        
        if (!n.parent) {
            n.role = 'root';
            n.depth = 0;
            return [n];
        }
        var result = findContexts(n.parent, position);
        n.depth = n.parent.depth+1;
        result.push(n);
        
        if (n.parent.type == 'map') {
            var prev;            
            for (var ci in n.parent.children) {
                var c = n.parent.children[ci];
                if (c === n || c.start > position) {
                    n.role = (ci%2==0 ? 'key' : 'value');
                    n.index = (ci - (ci%2))/2;
                    if (n.role === 'value') {
                      n.key = prev;
                    }
                    return result;
                }
                prev = c;
            }
            console.log("not found",n,"in",n.parent.children);
            throw "did not find parse node in parent's children";
        }
        
        if (n.parent.type == 'list') {
            n.role = 'entry';
            for (var ci in n.children) {
                var c = n.parent.children[ci];
                if (c === n || c.start > position) {
                    n.index = c;
                    return result;
                }
            }
            console.log("not found",n,"in",n.parent.children);
            throw "did not find parse node in parent's children";
        }
        
        n.role = 'primitive';
        return result;
    }
    
    function findContainingParseNode(n, predicate) {
        if (typeof n === 'undefined') return null;
        if (predicate(n)) return n;
        return findContainingParseNode(n.parent, predicate);
    }
    
    function findContainingMapParseNode(n) {
        return findContainingParseNode(n, function() { return n.type === 'map'; });
    }
    
    function indentation(n) {
      var i = n.start;
      while (i>0 && n.doc.charAt(i-1)!='\n') i--;
      return n.start - i;
    }

    function spaces(n) {
      var result = '';
      while (n>0) { result+=' '; n--; }
      return result;
    }
        
    var CatalogProposer = {
        getRootProposals: function() {
          return [
              { displayText: "brooklyn.catalog:", text: "brooklyn.catalog:\n  items:\n  - " }
            ];
        },
        getProposals: function(nn, position, cmPosition) {
            var n = nn[nn.length-1];
            var result = [];
            
            var itemsKey;
            if (n.type=='list' && n.key.result=='items') itemsKey = n.key;
            else if (n.type='entry' && nn.length>2 && nn[nn.length-2].type=='list' && nn[nn.length-2].key.result=='items') itemsKey = nn[nn.length-2].key;
            if (itemsKey) {            
                result = result.concat(_.map(["id","name","itemType"],
                    function(s) { return { displayText: s, text: spaces(indentation(itemsKey)+2)+s+': ' } }));
                result.push({displayText: "item", text: spaces(indentation(itemsKey)+2)+"item"+':\n'+spaces(indentation(n)+2) });
                result = result.concat(_.map(["description","iconUrl"],
                    function(s) { return { displayText: s, text: spaces(indentation(itemsKey)+2)+s+': ' } }));
                result.push({displayText: "items", text: spaces(indentation(itemsKey)+2)+"items"+':\n'+spaces(indentation(n))+"- " });
            }

            if (n.key && n.key.result=='itemType') {
              result.concat(['template','entity','location','policy']);
            }
            
            if (n.depth==1 || (n.depth>1 && cmPosition.ch==0)) {
                result.concat([{displayText: "version", text: "  version:\n"}, 
                    {displayText: "items", text: "  items:\n  - \n"} ]);
            }
            
            return result;
        }
    }
    
    var AppBlueprintProposer = {
        getRootProposals: function(node) {
          var result = [];
          if (!node || !node.name)
            result = result.concat( { displayText: "name:", text: "name: " } );
          if (!node || !node.services)
            result = result.concat( { displayText: "services:", text: "services:\n- type: " } );
          if (!node || !(node.location || node.locations))
            result = result.concat( { displayText: "location:", text: "location:\n  " } );
          
          return result;
        },
        
        getServiceTypes: function(n) {
            var result = [];
            catalogE.fetch();
            catalogA.fetch();
            result = result.concat(_.map(catalogE.models, function(m) { return m.get('symbolicName'); })); 
            result = result.concat(_.map(catalogA.models, function(m) { return m.get('symbolicName'); }));
            return result; 
        },
        getServiceKeys: function(type) {
            t = catalogA.get(type) || catalogE.get(type) ||
                // look for type without ID
                _.find(catalogA.models, function(m) { return m.get('symbolicName') == type; }) || 
                _.find(catalogE.models, function(m) { return m.get('symbolicName') == type; }); 
            if (!t) return [];
            return _.map(t.get('config'), function(c) { return c.name; });
        },
        getServiceKeyProposals: function(type, key) {
            t = catalogA.get(type) || catalogE.get(type) ||
                // look for type without ID
                _.find(catalogA.models, function(m) { return m.get('symbolicName') == type; }) || 
                _.find(catalogE.models, function(m) { return m.get('symbolicName') == type; }); 
            if (!t) return [];
            var c = _.find(t.get('config'), function(c) { return c.name == key; });
            if (!c) return [];
            var ct = c.type;
            if (ct.startsWith("java.lang.") || ct.startsWith("java.util.")) ct = ct.substring(10);
            var result = [ { displayText: ct+": "+c.description, text: '', className: 'summary' } ]; 
            if (c.possibleValues) {
                _.each(c.possibleValues, function(v) { result.push(v.value); });
            }
            return result;
        },
    
        getLocationTypes: function() {
            catalogL.fetch();
            return _.map(catalogL.models, function(m) { return m.get('name'); });
        },    
        
        // TODO would be much nicer to define a yaml schema; see e.g. json-schema.org
        // and various JS implementations
        getProposals: function(nn, position, cmPosition) {
            var n = nn[nn.length-1];
            var result = [];
//            console.log("context at position "+position, nn);
            while (n.role === 'primitive') n = n.parent;
            var fromHereToEndOfOuterBlock;
            if (nn[1].key) {
                fromHereToEndOfOuterBlock = position <= nn[1].end ? nn[1].doc.substring(position, nn[1].end) : nn[1].doc.substring(nn[1].end, position);
                fromHereToEndOfOuterBlock = fromHereToEndOfOuterBlock.trim();
            }
            if (nn[1].key && nn[1].key.result === 'services') {
                // in services block
                var canAddService = true;
                
                if (n.depth == 3 && n.role == 'value' && n.parent.role == 'entry') {
                    // in a block for a particular service
                    canAddService = false;
                    if (n.key.result === 'type') {
                        result = result.concat(_.map(this.getServiceTypes(n.parent), function(t) { return t+'\n'; }));
                    } else if (n.key.result === 'location') {
                        result = result.concat(_.map(this.getLocationTypes(n.parent), function(t) { 
                            return t+'\n'; }));
                    
                    } else {
                        // no assistance for values of other keys atm; show summary if available
                        var type = nn[2].result['type'];
                        result = result.concat(
                            this.getServiceKeyProposals(type, n.key.result) ||
                                [{ displayText: 'No assistance available for key', 
                                   className: 'summary', text: '' }]);
                    }
                }
                
                if (n.depth >= 2 && n.role == 'entry' && nn[2].result['type']) {
                    var type = nn[2].result['type'];
                    result = result.concat(
                        _.map(this.getServiceKeys(type), function(keyname) {
                            return { displayText: keyname, text: spaces(indentation(nn[2]))+keyname+": " };
                        }));
                }
                
                if (n.depth > 2) {
                    // deep in a service, no special assistance currently offered
                }

                if (canAddService && fromHereToEndOfOuterBlock.length==0) {
                    result.push( { displayText: 'Add a service', className: 'summary', text: '\n- type: ' } );
                }
            }
            if (nn[1].key && nn[1].key.result === 'location') {
                if (n.depth <= 2) {
                    result = result.concat(_.map(this.getLocationTypes(n.parent), function(t) { 
                            return t+'\n'; }));
                }
            }
            
            // TODO other blocks
            
            // finally add root proposals
            if (cmPosition.ch==0 && fromHereToEndOfOuterBlock && fromHereToEndOfOuterBlock.length==0) {
                result = result.concat(this.getRootProposals(nn[0] ? nn[0].result : null));
            }
            
            return result;
        }
    }

    // cmPosition is {line: N, ch: N} format
    BrooklynYamlCompletionProposals.getCompletionProposals = function(mode, cm) {
        var proposer = mode === 'catalog' ? CatalogProposer : AppBlueprintProposer;
        var text = cm.getValue();
        var cmPosition = cm.getCursor();
        // absolute position in doc
        var position = cm.getRange({line: 0, ch: 0}, cmPosition).length;
         
        var parse;
        try {
            parse = JsYamlParser.parse(text);
            if (typeof parse.result == 'string') {
                throw "primitive not supported, parse as empty and let completion apply";
            }
        } catch (e) {
            // parse failed -- parse to beginning of line
            try {
                parse = JsYamlParser.parse(text.substring(0, text.length - cmPosition.ch)+spaces(cmPosition.ch));
            } catch (e) {
                console.log('parse failed', e);
                return [];
            }
        }
        try {
            var result;
            if (typeof parse === 'undefined') {
                // editor empty -- return defaults
                result = proposer.getRootProposals();
            } else {
                n = findParseNodeAt(parse, position);
                if (!n) {
                  // shouldn't happen... fall back to returning empty
                  console.log("no parse node containing curpos");
                  result = proposer.getRootProposals();
                } else {
                    var nn = findContexts(n, position);
                    
                    if (n.role === 'root') {
                      result = proposer.getRootProposals(n.result);
                    } else {
                      result = proposer.getProposals(nn, position, cmPosition);
                    }
                }
            }
            var wordSoFar = '';
            var lineSoFar = '';
            var i=0;
            while (position > wordSoFar.length) {
                var c = text.charAt(position-wordSoFar.length-1);
                if (c=='\n' || c==' ' || c=='\t') break;
                wordSoFar = '' + c + wordSoFar;
            }
            while (position > lineSoFar.length) {
                var c = text.charAt(position-lineSoFar.length-1);
                if (c=='\n') break;
                lineSoFar = '' + c + lineSoFar;
            }
            result = _.compact(_.map(result, function(proposal) {
                var proposalObj;
                if (typeof proposal === 'object') {
                    proposalObj = proposal;
                    proposal = proposalObj.text;
                }  else {
                    proposalObj = { text: proposal, displayText: proposal };
                }
                if (proposal[0]==' ') {
                  // proposal should start with 'lineSoFar'
                  if (proposal.lastIndexOf(lineSoFar, 0)!=0) {
                    // also match '  - ' in lieu of '    ',
                    // needed for catalog (for service we always add the 'type' in the previous expansion)
                    var proposalIfListStart = proposal.replace(/( *)  /,'$1- ');
                    if (proposalIfListStart.lastIndexOf(lineSoFar, 0)!=0) {
                      return null;
                    }
                    proposalObj.text = proposalIfListStart;
                  }
                  proposalObj.to = cmPosition;
                  proposalObj.from = { line: cmPosition.line, ch: 0 };
                } else {
                  // proposal should start with 'wordSoFar'
                  if (proposal.lastIndexOf(wordSoFar, 0)!=0) return null;
                  proposalObj.to = cmPosition;
                  proposalObj.from = { line: cmPosition.line, ch: cmPosition.ch - wordSoFar.length };
                }
                return proposalObj;
            }));
//            console.log("proposals:", result);
            return result;
            
        } catch (e) {
            console.log('completion failed', e, e.stack);
            return [];
        }
    }
    
    return BrooklynYamlCompletionProposals;

});