(function(root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        // TODO: remove dependency to Chevrotain
        define(['../vendor/railroad-diagrams'], factory)
    } else if (typeof module === 'object' && module.exports) {
        // Node. Does not work with strict CommonJS, but
        // only CommonJS-like environments that support module.exports,
        // like Node.
        // TODO: remove dependency to Chevrotain
        module.exports = factory(require('../vendor/railroad-diagrams'))
    } else {
        // Browser globals (root is window)
        root.diagrams_builder = factory(root.railroad)
    }
}(this, function(railroad) {

    var Diagram = railroad.Diagram
    var Sequence = railroad.Sequence
    var Choice = railroad.Choice
    var Optional = railroad.Optional
    var OneOrMore = railroad.OneOrMore
    var ZeroOrMore = railroad.ZeroOrMore
    var Terminal = railroad.Terminal
    var NonTerminal = railroad.NonTerminal

    /**
     * @param {chevrotain.gast.ISerializedGast} topRules
     *
     * @returns {string} - The htmlText that will render the diagrams
     */
    function buildSyntaxDiagramsText(topRules) {
        var diagramsHtml = ""

        topRules.forEach(function(production) {
            var currDiagramHtml = convertProductionToDiagram(production, production.name)
            diagramsHtml += '<h2 class="diagramHeader">' + production.name + '</h2>' + currDiagramHtml
        })

        return diagramsHtml
    }

    function definitionsToSubDiagrams(definitions, topRuleName) {
        var subDiagrams = definitions.map(function(subProd) {
            return convertProductionToDiagram(subProd, topRuleName)
        })
        return subDiagrams
    }

    /**
     * @param {chevrotain.gast.ISerializedTerminal} prod
     * @param {string} topRuleName
     * @param {string} dslRuleName
     *
     * @return {RailRoadDiagram.Terminal}
     */
    function createTerminalFromSerializedGast(prod, topRuleName, dslRuleName) {
        // PATTERN static property will not exist when using custom lexers (hand built or other lexer generators)
        var toolTipTitle = prod.pattern
        return railroad.Terminal(prod.label,
            undefined,
            toolTipTitle,
            prod.occurrenceInParent,
            topRuleName,
            dslRuleName,
            prod.name
        )
    }

    /**
     * @param prod
     * @param topRuleName
     *
     * Converts a single Chevrotain Grammar production to a RailRoad Diagram.
     * This is also exported to allow custom logic in the creation of the diagrams.
     * @returns {*}
     */
    function convertProductionToDiagram(prod, topRuleName) {

        if (prod.type === "NonTerminal") {
            // must handle NonTerminal separately from the other AbstractProductions as we do not want to expand the subDefinition
            // of a reference and cause infinite loops
            return NonTerminal(getNonTerminalName(prod), undefined, prod.occurrenceInParent, topRuleName)
        }
        else if (prod.type !== "Terminal") {
            var subDiagrams = definitionsToSubDiagrams(prod.definition, topRuleName)
            if (prod.type === "Rule") {
                return Diagram.apply(this, subDiagrams)
            }
            else if (prod.type === "Flat") {
                return Sequence.apply(this, subDiagrams)
            }
            else if (prod.type === "Option") {
                if (subDiagrams.length > 1) {
                    return Optional(Sequence.apply(this, subDiagrams))
                }
                else if (subDiagrams.length === 1) {
                    return Optional(subDiagrams[0])
                }
                else {
                    throw Error("Empty Optional production, OOPS!")
                }
            }
            else if (prod.type === "Repetition") {
                if (subDiagrams.length > 1) {
                    return ZeroOrMore(Sequence.apply(this, subDiagrams))
                }
                else if (subDiagrams.length === 1) {
                    return ZeroOrMore(subDiagrams[0])
                }
                else {
                    throw Error("Empty Optional production, OOPS!")
                }
            }
            else if (prod.type === "Alternation") {
                // todo: what does the first argument of choice (the index 0 means?)
                return Choice.apply(this, [0].concat(subDiagrams))
            }
            else if (prod.type === "RepetitionMandatory") {
                if (subDiagrams.length > 1) {
                    return OneOrMore(Sequence.apply(this, subDiagrams))
                }
                else if (subDiagrams.length === 1) {
                    return OneOrMore(subDiagrams[0])
                }
                else {
                    throw Error("Empty Optional production, OOPS!")
                }
            }
            else if (prod.type === "RepetitionWithSeparator") {
                if (subDiagrams.length > 0) {
                    // MANY_SEP(separator, definition) === (definition (separator definition)*)?
                    return Optional(Sequence.apply(this, subDiagrams.concat(
                        [ZeroOrMore(Sequence.apply(this,
                            [createTerminalFromSerializedGast(
                                prod.separator,
                                topRuleName,
                                "many_sep")].concat(subDiagrams)))])))
                }
                else {
                    throw Error("Empty Optional production, OOPS!")
                }
            }
            else if (prod.type === "RepetitionMandatoryWithSeparator") {
                if (subDiagrams.length > 0) {
                    // AT_LEAST_ONE_SEP(separator, definition) === definition (separator definition)*
                    return Sequence.apply(this, subDiagrams.concat(
                        [ZeroOrMore(Sequence.apply(this,
                            [createTerminalFromSerializedGast(
                                prod.separator,
                                topRuleName,
                                "at_least_one_sep")].concat(
                                subDiagrams)))]))
                }
                else {
                    throw Error("Empty Optional production, OOPS!")
                }
            }
        }
        else if (prod.type === "Terminal") {
            return createTerminalFromSerializedGast(prod, topRuleName, "consume")
        }
        else {
            throw Error("non exhaustive match")
        }
    }

    function getNonTerminalName(prod) {
        if (prod.nonTerminalName !== undefined) {
            return prod.nonTerminalName
        } else {
            return prod.name
        }
    }

    return {
        buildSyntaxDiagramsText:    buildSyntaxDiagramsText,
        convertProductionToDiagram: convertProductionToDiagram
    }
}))
