// #ifdef UNDEFINED
/*
 Copyright 2013 Daniel Wirtz <dcode@dcode.io>

 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at

 http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 */
// #endif
/**
 * @alias ProtoBuf.Builder
 * @expose
 */
ProtoBuf.Builder = (function(ProtoBuf, Lang, Reflect) {
    "use strict";
    
    /**
     * Constructs a new Builder.
     * @exports ProtoBuf.Builder
     * @class Provides the functionality to build protocol messages.
     * @constructor
     */
    var Builder = function() {

        /**
         * Namespace.
         * @type {ProtoBuf.Reflect.Namespace}
         * @expose
         */
        this.ns = new Reflect.Namespace(null, ""); // Global namespace

        /**
         * Namespace pointer.
         * @type {ProtoBuf.Reflect.T}
         * @expose
         */
        this.ptr = this.ns;

        /**
         * Resolved flag.
         * @type {boolean}
         * @expose
         */
        this.resolved = false;

        /**
         * The current building result.
         * @type {Object.<string,ProtoBuf.Builder.Message|Object>|null}
         * @expose
         */
        this.result = null;

        /**
         * Imported files.
         * @type {Array.<string>}
         * @expose
         */
        this.files = {};

        /**
         * Import root override.
         * @type {?string}
         * @expose
         */
        this.importRoot = null;
    };

    /**
     * Resets the pointer to the global namespace.
     * @expose
     */
    Builder.prototype.reset = function() {
        this.ptr = this.ns;
    };

    /**
     * Defines a package on top of the current pointer position and places the pointer on it.
     * @param {string} pkg
     * @param {Object.<string,*>=} options
     * @return {ProtoBuf.Builder} this
     * @throws {Error} If the package name is invalid
     * @expose
     */
    Builder.prototype.define = function(pkg, options) {
        if (typeof pkg !== 'string' || !Lang.TYPEREF.test(pkg)) {
            throw(new Error("Illegal package name: "+pkg));
        }
        var part = pkg.split("."), i;
        for (i=0; i<part.length; i++) { // To be absolutely sure
            if (!Lang.NAME.test(part[i])) {
                throw(new Error("Illegal package name: "+part[i]));
            }
        }
        for (i=0; i<part.length; i++) {
            if (!this.ptr.hasChild(part[i])) { // Keep existing namespace
                this.ptr.addChild(new Reflect.Namespace(this.ptr, part[i], options));
            }
            this.ptr = this.ptr.getChild(part[i]);
        }
        return this;
    };

    /**
     * Tests if a definition is a valid message definition.
     * @param {Object.<string,*>} def Definition
     * @return {boolean} true if valid, else false
     * @expose
     */
    Builder.isValidMessage = function(def) {
        // Messages require a string name
        if (typeof def["name"] !== 'string' || !Lang.NAME.test(def["name"])) {
            return false;
        }
        // Messages must not contain values (that'd be an enum) or methods (that'd be a service)
        if (typeof def["values"] !== 'undefined' || typeof def["rpc"] !== 'undefined') {
            return false;
        }
        // Fields, enums and messages are arrays if provided
        var i;
        if (typeof def["fields"] !== 'undefined') {
            if (!ProtoBuf.Util.isArray(def["fields"])) {
                return false;
            }
            var ids = [], id; // IDs must be unique
            for (i=0; i<def["fields"].length; i++) {
                if (!Builder.isValidMessageField(def["fields"][i])) {
                    return false;
                }
                id = parseInt(def["id"], 10);
                if (ids.indexOf(id) >= 0) {
                    return false;
                }
                ids.push(id);
            }
            ids = null;
        }
        if (typeof def["enums"] !== 'undefined') {
            if (!ProtoBuf.Util.isArray(def["enums"])) {
                return false;
            }
            for (i=0; i<def["enums"].length; i++) {
                if (!Builder.isValidEnum(def["enums"][i])) {
                    return false;
                }
            }
        }
        if (typeof def["messages"] !== 'undefined') {
            if (!ProtoBuf.Util.isArray(def["messages"])) {
                return false;
            }
            for (i=0; i<def["messages"].length; i++) {
                if (!Builder.isValidMessage(def["messages"][i]) && !Builder.isValidExtend(def["messages"][i])) {
                    return false;
                }
            }
        }
        if (typeof def["extensions"] !== 'undefined') {
            if (!ProtoBuf.Util.isArray(def["extensions"]) || def["extensions"].length !== 2 || typeof def["extensions"][0] !== 'number' || typeof def["extensions"][1] !== 'number') {
                return false;
            }
        }
        return true;
    };

    /**
     * Tests if a definition is a valid message field definition.
     * @param {Object} def Definition
     * @return {boolean} true if valid, else false
     * @expose
     */
    Builder.isValidMessageField = function(def) {
        // Message fields require a string rule, name and type and an id
        if (typeof def["rule"] !== 'string' || typeof def["name"] !== 'string' || typeof def["type"] !== 'string' || typeof def["id"] === 'undefined') {
            return false;
        }
        if (!Lang.RULE.test(def["rule"]) || !Lang.NAME.test(def["name"]) || !Lang.TYPEREF.test(def["type"]) || !Lang.ID.test(""+def["id"])) {
            return false;
        }
        if (typeof def["options"] != 'undefined') {
            // Options are objects
            if (typeof def["options"] != 'object') {
                return false;
            }
            // Options are <string,*>
            var keys = Object.keys(def["options"]);
            for (var i=0; i<keys.length; i++) {
                if (!Lang.OPTNAME.test(keys[i]) || (typeof def["options"][keys[i]] !== 'string' && typeof def["options"][keys[i]] !== 'number' && typeof def["options"][keys[i]] !== 'boolean')) {
                    return false;
                }
            }
        }
        return true;
    };

    /**
     * Tests if a definition is a valid enum definition.
     * @param {Object} def Definition
     * @return {boolean} true if valid, else false
     * @expose
     */
    Builder.isValidEnum = function(def) {
        // Enums require a string name
        if (typeof def["name"] !== 'string' || !Lang.NAME.test(def["name"])) {
            return false;
        }
        // Enums require at least one value
        if (typeof def["values"] === 'undefined' || !ProtoBuf.Util.isArray(def["values"]) || def["values"].length == 0) {
            return false;
        }
        for (var i=0; i<def["values"].length; i++) {
            // Values are objects
            if (typeof def["values"][i] != "object") {
                return false;
            }
            // Values require a string name and an id
            if (typeof def["values"][i]["name"] !== 'string' || typeof def["values"][i]["id"] === 'undefined') {
                return false;
            }
            if (!Lang.NAME.test(def["values"][i]["name"]) || !Lang.NEGID.test(""+def["values"][i]["id"])) {
                return false;
            }
        }
        // It's not important if there are other fields because ["values"] is already unique
        return true;
    };

    /**
     * Creates ths specified protocol types at the current pointer position.
     * @param {Array.<Object.<string,*>>} defs Messages, enums or services to create
     * @return {ProtoBuf.Builder} this
     * @throws {Error} If a message definition is invalid
     * @expose
     */
    Builder.prototype.create = function(defs) {
        if (!defs) return; // Nothing to create
        if (!ProtoBuf.Util.isArray(defs)) {
            defs = [defs];
        }
        if (defs.length == 0) return;
        
        // It's quite hard to keep track of scopes and memory here, so let's do this iteratively.
        var stack = [], def, obj, subObj, i, j;
        stack.push(defs); // One level [a, b, c]
        while (stack.length > 0) {
            defs = stack.pop();
            if (ProtoBuf.Util.isArray(defs)) { // Stack always contains entire namespaces
                while (defs.length > 0) {
                    def = defs.shift(); // Namespace always contains an array of messages, enums and services
                    if (Builder.isValidMessage(def)) {
                        obj = new Reflect.Message(this.ptr, def["name"], def["options"]);
                        // Create fields
                        if (def["fields"] && def["fields"].length > 0) {
                            for (i=0; i<def["fields"].length; i++) { // i=Fields
                                if (obj.hasChild(def['fields'][i]['id'])) {
                                    throw(new Error("Duplicate field id in message "+obj.name+": "+def['fields'][i]['id']));
                                }
                                if (def["fields"][i]["options"]) {
                                    subObj = Object.keys(def["fields"][i]["options"]);
                                    for (j=0; j<subObj.length; j++) { // j=Option names
                                        if (!Lang.OPTNAME.test(subObj[j])) {
                                            throw(new Error("Illegal field option name in message "+obj.name+"#"+def["fields"][i]["name"]+": "+subObj[j]));
                                        }
                                        if (typeof def["fields"][i]["options"][subObj[j]] !== 'string' && typeof def["fields"][i]["options"][subObj[j]] !== 'number' && typeof def["fields"][i]["options"][subObj[j]] !== 'boolean') {
                                            throw(new Error("Illegal field option value in message "+obj.name+"#"+def["fields"][i]["name"]+"#"+subObj[j]+": "+def["fields"][i]["options"][subObj[j]]));
                                        }
                                    }
                                    subObj = null;
                                }
                                obj.addChild(new Reflect.Message.Field(obj, def["fields"][i]["rule"], def["fields"][i]["type"], def["fields"][i]["name"], def["fields"][i]["id"], def["fields"][i]["options"]));
                            }
                        }
                        // Push enums and messages to stack
                        subObj = [];
                        if (typeof def["enums"] !== 'undefined' && def['enums'].length > 0) {
                            for (i=0; i<def["enums"].length; i++) {
                                subObj.push(def["enums"][i]);
                            }
                        }
                        if (def["messages"] && def["messages"].length > 0) {
                            for (i=0; i<def["messages"].length; i++) {
                                subObj.push(def["messages"][i]);
                            }
                        }
                        // Set extension range
                        if (def["extensions"]) {
                            obj.extensions = def["extensions"];
                            if (obj.extensions[0] < ProtoBuf.Lang.ID_MIN) {
                                obj.extensions[0] = ProtoBuf.Lang.ID_MIN;
                            }
                            if (obj.extensions[1] > ProtoBuf.Lang.ID_MAX) {
                                obj.extensions[1] = ProtoBuf.Lang.ID_MAX;
                            }
                        }
                        this.ptr.addChild(obj); // Add to current namespace
                        if (subObj.length > 0) {
                            stack.push(defs); // Push the current level back
                            defs = subObj; // Continue processing sub level
                            subObj = null;
                            this.ptr = obj; // And move the pointer to this namespace
                            obj = null;
                            continue;
                        }
                        subObj = null;
                        obj = null;
                    } else if (Builder.isValidEnum(def)) {
                        obj = new Reflect.Enum(this.ptr, def["name"], def["options"]);
                        for (i=0; i<def["values"].length; i++) {
                            obj.addChild(new Reflect.Enum.Value(obj, def["values"][i]["name"], def["values"][i]["id"]));
                        }
                        this.ptr.addChild(obj);
                        obj = null;
                    } else if (Builder.isValidService(def)) {
                        obj = new Reflect.Service(this.ptr, def["name"], def["options"]);
                        for (i in def["rpc"]) {
                            if (def["rpc"].hasOwnProperty(i)) {
                                obj.addChild(new Reflect.Service.RPCMethod(obj, i, def["rpc"][i]["request"], def["rpc"][i]["response"], def["rpc"][i]["options"]));
                            }
                        }
                        this.ptr.addChild(obj);
                        obj = null;
                    } else if (Builder.isValidExtend(def)) {
                        obj = this.ptr.resolve(def["ref"]);
                        if (obj) {
                            for (i=0; i<def["fields"].length; i++) { // i=Fields
                                if (obj.hasChild(def['fields'][i]['id'])) {
                                    throw(new Error("Duplicate extended field id in message "+obj.name+": "+def['fields'][i]['id']));
                                }
                                if (def['fields'][i]['id'] < obj.extensions[0] || def['fields'][i]['id'] > obj.extensions[1]) {
                                    throw(new Error("Illegal extended field id in message "+obj.name+": "+def['fields'][i]['id']+" ("+obj.extensions.join(' to ')+" expected)"));
                                }
                                obj.addChild(new Reflect.Message.Field(obj, def["fields"][i]["rule"], def["fields"][i]["type"], def["fields"][i]["name"], def["fields"][i]["id"], def["fields"][i]["options"]));
                            }
                            /* if (this.ptr instanceof Reflect.Message) {
                                this.ptr.addChild(obj); // Reference the extended message here to enable proper lookups
                            } */
                        } else {
                            if (!/\.?google\.protobuf\./.test(def["ref"])) { // Silently skip internal extensions
                                throw(new Error("Extended message "+def["ref"]+" is not defined"));
                            }
                        }
                    } else {
                        throw(new Error("Not a valid message, enum, service or extend definition: "+JSON.stringify(def)));
                    }
                    def = null;
                }
                // Break goes here
            } else {
                throw(new Error("Not a valid namespace definition: "+JSON.stringify(defs)));
            }
            defs = null;
            this.ptr = this.ptr.parent; // This namespace is s done
        }
        this.resolved = false; // Require re-resolve
        this.result = null; // Require re-build
        return this;
    };

    /**
     * Tests if the specified file is a valid import.
     * @param {string} filename
     * @returns {boolean} true if valid, false if it should be skipped
     * @expose
     */
    Builder.isValidImport = function(filename) {
        // Ignore google/protobuf/descriptor.proto (for example) as it makes use of low-level
        // bootstrapping directives that are not required and therefore cannot be parsed by ProtoBuf.js.
        return !(/google\/protobuf\//.test(filename));
    };

    /**
     * Imports another definition into this builder.
     * @param {Object.<string,*>} json Parsed import
     * @param {(string|{root: string, file: string})=} filename Imported file name
     * @return {ProtoBuf.Builder} this
     * @throws {Error} If the definition or file cannot be imported
     * @expose
     */
    Builder.prototype["import"] = function(json, filename) {
        if (typeof filename === 'string') {
            if (ProtoBuf.Util.IS_NODE) {
                var path = require("path");
                filename = path.resolve(filename);
            }
            if (!!this.files[filename]) {
                this.reset();
                return this; // Skip duplicate imports
            }
            this.files[filename] = true;
        }
        if (!!json['imports'] && json['imports'].length > 0) {
            var importRoot, delim = '/', resetRoot = false;
            if (typeof filename === 'object') { // If an import root is specified, override
                this.importRoot = filename["root"]; resetRoot = true; // ... and reset afterwards
                importRoot = this.importRoot;
                filename = filename["file"];
                if (importRoot.indexOf("\\") >= 0 || filename.indexOf("\\") >= 0) delim = '\\';
            } else if (typeof filename === 'string') {
                if (this.importRoot) { // If import root is overridden, use it
                    importRoot = this.importRoot;
                } else { // Otherwise compute from filename
                    if (filename.indexOf("/") >= 0) { // Unix
                        importRoot = filename.replace(/\/[^\/]*$/, "");
                        if (/* /file.proto */ importRoot === "") importRoot = "/";
                    } else if (filename.indexOf("\\") >= 0) { // Windows
                        importRoot = filename.replace(/\\[^\\]*$/, ""); delim = '\\';
                    } else {
                        importRoot = ".";
                    }
                }
            } else {
                importRoot = null;
            }

            for (var i=0; i<json['imports'].length; i++) {
                if (typeof json['imports'][i] === 'string') { // Import file
                    if (!importRoot) {
                        throw(new Error("Cannot determine import root: File name is unknown"));
                    }
                    var importFilename = importRoot+delim+json['imports'][i];
                    if (!Builder.isValidImport(importFilename)) continue; // e.g. google/protobuf/*
                    if (/\.proto$/i.test(importFilename) && !ProtoBuf.DotProto) {     // If this is a NOPARSE build
                        importFilename = importFilename.replace(/\.proto$/, ".json"); // always load the JSON file
                    }
                    var contents = ProtoBuf.Util.fetch(importFilename);
                    if (contents === null) {
                        throw(new Error("Failed to import '"+importFilename+"' in '"+filename+"': File not found"));
                    }
                    if (/\.json$/i.test(importFilename)) { // Always possible
                        this["import"](JSON.parse(contents+""), importFilename); // May throw
                    } else {
                        this["import"]((new ProtoBuf.DotProto.Parser(contents+"")).parse(), importFilename); // May throw
                    }
                } else { // Import structure
                    if (!filename) {
                        this["import"](json['imports'][i]);
                    } else if (/\.(\w+)$/.test(filename)) { // With extension: Append _importN to the name portion to make it unique
                        this["import"](json['imports'][i], filename.replace(/^(.+)\.(\w+)$/, function($0, $1, $2) { return $1+"_import"+i+"."+$2; }));
                    } else { // Without extension: Append _importN to make it unique
                        this["import"](json['imports'][i], filename+"_import"+i);
                    }
                }
            }
            if (resetRoot) { // Reset import root override when all imports are done
                this.importRoot = null;
            }
        }
        if (!!json['messages']) {
            if (!!json['package']) this.define(json['package'], json["options"]);
            this.create(json['messages']);
            this.reset();
        }
        if (!!json['enums']) {
            if (!!json['package']) this.define(json['package'], json["options"]);
            this.create(json['enums']);
            this.reset();
        }
        if (!!json['services']) {
            if (!!json['package']) this.define(json['package'], json["options"]);
            this.create(json['services']);
            this.reset();
        }
        if (!!json['extends']) {
            if (!!json['package']) this.define(json['package'], json["options"]);
            this.create(json['extends']);
            this.reset();
        }
        return this;
    };

    /**
     * Tests if a definition is a valid service definition.
     * @param {Object} def Definition
     * @return {boolean} true if valid, else false
     * @expose
     */
    Builder.isValidService = function(def) {
        // Services require a string name
        if (typeof def["name"] !== 'string' || !Lang.NAME.test(def["name"]) || typeof def["rpc"] !== 'object') {
            return false;
        }
        return true;
    };

    /**
     * Tests if a definition is a valid extension.
     * @param {Object} def Definition
     * @returns {boolean} true if valid, else false
     * @expose
    */
    Builder.isValidExtend = function(def) {
        if (typeof def["ref"] !== 'string' || !Lang.TYPEREF.test(def["name"])) {
            return false;
        }
        var i;
        if (typeof def["fields"] !== 'undefined') {
            if (!ProtoBuf.Util.isArray(def["fields"])) {
                return false;
            }
            var ids = [], id; // IDs must be unique (does not yet test for the extended message's ids)
            for (i=0; i<def["fields"].length; i++) {
                if (!Builder.isValidMessageField(def["fields"][i])) {
                    return false;
                }
                id = parseInt(def["id"], 10);
                if (ids.indexOf(id) >= 0) {
                    return false;
                }
                ids.push(id);
            }
            ids = null;
        }
        return true;
    };

    /**
     * Resolves all namespace objects.
     * @throws {Error} If a type cannot be resolved
     * @expose
     */
    Builder.prototype.resolveAll = function() {
        // Resolve all reflected objects
        var res;
        if (this.ptr == null || typeof this.ptr.type === 'object') return; // Done (already resolved)
        if (this.ptr instanceof Reflect.Namespace) {
            // Build all children
            var children = this.ptr.getChildren();
            for (var i=0; i<children.length; i++) {
                this.ptr = children[i];
                this.resolveAll();
            }
        } else if (this.ptr instanceof Reflect.Message.Field) {
            if (!Lang.TYPE.test(this.ptr.type)) { // Resolve type...
                if (!Lang.TYPEREF.test(this.ptr.type)) {
                    throw(new Error("Illegal type reference in "+this.ptr.toString(true)+": "+this.ptr.type));
                }
                res = this.ptr.parent.resolve(this.ptr.type, true);
                if (!res) {
                    throw(new Error("Unresolvable type reference in "+this.ptr.toString(true)+": "+this.ptr.type));
                }
                this.ptr.resolvedType = res;
                if (res instanceof Reflect.Enum) {
                    this.ptr.type = ProtoBuf.TYPES["enum"];
                } else if (res instanceof Reflect.Message) {
                    this.ptr.type = ProtoBuf.TYPES["message"];
                } else {
                    throw(new Error("Illegal type reference in "+this.ptr.toString(true)+": "+this.ptr.type));
                }
            } else {
                this.ptr.type = ProtoBuf.TYPES[this.ptr.type];
            }
        } else if (this.ptr instanceof ProtoBuf.Reflect.Enum.Value) {
            // No need to build enum values (built in enum)
        } else if (this.ptr instanceof ProtoBuf.Reflect.Service.Method) {
            if (this.ptr instanceof ProtoBuf.Reflect.Service.RPCMethod) {
                res = this.ptr.parent.resolve(this.ptr.requestName);
                if (!res || !(res instanceof ProtoBuf.Reflect.Message)) {
                    throw(new Error("Illegal request type reference in "+this.ptr.toString(true)+": "+this.ptr.requestName));
                }
                this.ptr.resolvedRequestType = res;
                res = this.ptr.parent.resolve(this.ptr.responseName);
                if (!res || !(res instanceof ProtoBuf.Reflect.Message)) {
                    throw(new Error("Illegal response type reference in "+this.ptr.toString(true)+": "+this.ptr.responseName));
                }
                this.ptr.resolvedResponseType = res;
            } else {
                // Should not happen as nothing else is implemented
                throw(new Error("Illegal service method type in "+this.ptr.toString(true)));
            }
        } else {
            throw(new Error("Illegal object type in namespace: "+typeof(this.ptr)+":"+this.ptr));
        }
        this.reset();
    };

    /**
     * Builds the protocol. This will first try to resolve all definitions and, if this has been successful,
     * return the built package.
     * @param {string=} path Specifies what to return. If omitted, the entire namespace will be returned.
     * @return {ProtoBuf.Builder.Message|Object.<string,*>}
     * @throws {Error} If a type could not be resolved
     * @expose
     */
    Builder.prototype.build = function(path) {
        this.reset();
        if (!this.resolved) {
            this.resolveAll();
            this.resolved = true;
            this.result = null; // Require re-build
        }
        if (this.result == null) { // (Re-)Build
            this.result = this.ns.build();
        }
        if (!path) {
            return this.result;
        } else {
            var part = path.split(".");
            var ptr = this.result; // Build namespace pointer (no hasChild etc.)
            for (var i=0; i<part.length; i++) {
                if (ptr[part[i]]) {
                    ptr = ptr[part[i]];
                } else {
                    ptr = null;
                    break;
                }
            }
            return ptr;
        }
    };

    /**
     * Similar to {@link ProtoBuf.Builder#build}, but looks up the internal reflection descriptor.
     * @param {string=} path Specifies what to return. If omitted, the entire namespace wiil be returned.
     * @return {ProtoBuf.Reflect.T} Reflection descriptor or `null` if not found
     */
    Builder.prototype.lookup = function(path) {
        return path ? this.ns.resolve(path) : this.ns;
    };

    /**
     * Returns a string representation of this object.
     * @return {string} String representation as of "Builder"
     * @expose
     */
    Builder.prototype.toString = function() {
        return "Builder";
    };

    // Pseudo types documented in Reflect.js.
    // Exist for the sole purpose of being able to "... instanceof ProtoBuf.Builder.Message" etc.
    Builder.Message = function() {};
    Builder.Service = function() {};
    
    return Builder;
    
})(ProtoBuf, ProtoBuf.Lang, ProtoBuf.Reflect);
