/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#ifndef nsXULTemplateBuilder_h__
#define nsXULTemplateBuilder_h__

#include "nsStubDocumentObserver.h"
#include "nsIScriptSecurityManager.h"
#include "nsIObserver.h"
#include "nsIRDFCompositeDataSource.h"
#include "nsIRDFContainer.h"
#include "nsIRDFContainerUtils.h"
#include "nsIRDFDataSource.h"
#include "nsIRDFObserver.h"
#include "nsIRDFService.h"
#include "nsIXULTemplateBuilder.h"

#include "nsCOMArray.h"
#include "nsTArray.h"
#include "nsDataHashtable.h"
#include "nsTemplateRule.h"
#include "nsTemplateMatch.h"
#include "nsIXULTemplateQueryProcessor.h"
#include "nsCycleCollectionParticipant.h"

#include "mozilla/Logging.h"
extern mozilla::LazyLogModule gXULTemplateLog;

class nsIContent;
class nsIObserverService;
class nsIRDFCompositeDataSource;

/**
 * An object that translates an RDF graph into a presentation using a
 * set of rules.
 */
class nsXULTemplateBuilder : public nsIXULTemplateBuilder,
                             public nsIObserver,
                             public nsStubDocumentObserver
{
    void CleanUp(bool aIsFinal);
    void DestroyMatchMap();

public:
    nsXULTemplateBuilder();

    nsresult InitGlobals();

    /**
     * Clear the template builder structures. The aIsFinal flag is set to true
     * when the template is going away.
     */
    virtual void Uninit(bool aIsFinal);

    // nsISupports interface
    NS_DECL_CYCLE_COLLECTING_ISUPPORTS
    NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsXULTemplateBuilder,
                                             nsIXULTemplateBuilder)

    // nsIXULTemplateBuilder interface
    NS_DECL_NSIXULTEMPLATEBUILDER

    // nsIObserver Interface
    NS_DECL_NSIOBSERVER

    // nsIMutationObserver
    NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED
    NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
    NS_DECL_NSIMUTATIONOBSERVER_NODEWILLBEDESTROYED

    /**
     * Remove an old result and/or add a new result. This method will retrieve
     * the set of containers where the result could be inserted and either add
     * the new result to those containers, or remove the result from those
     * containers. UpdateResultInContainer is called for each container.
     *
     * @param aOldResult result to remove
     * @param aNewResult result to add
     * @param aQueryNode query node for new result
     */
    nsresult
    UpdateResult(nsIXULTemplateResult* aOldResult,
                 nsIXULTemplateResult* aNewResult,
                 nsIDOMNode* aQueryNode);

    /**
     * Remove an old result and/or add a new result from a specific container.
     *
     * @param aOldResult result to remove
     * @param aNewResult result to add
     * @param aQueryNode queryset for the new result
     * @param aOldId id of old result
     * @param aNewId id of new result
     * @param aInsertionPoint container to remove or add result inside
     */
    nsresult
    UpdateResultInContainer(nsIXULTemplateResult* aOldResult,
                            nsIXULTemplateResult* aNewResult,
                            nsTemplateQuerySet* aQuerySet,
                            nsIRDFResource* aOldId,
                            nsIRDFResource* aNewId,
                            nsIContent* aInsertionPoint);

    nsresult
    ComputeContainmentProperties();

    static bool
    IsTemplateElement(nsIContent* aContent);

    virtual nsresult
    RebuildAll() = 0; // must be implemented by subclasses

    void RunnableRebuild() { Rebuild(); }
    void RunnableLoadAndRebuild() {
      Uninit(false);  // Reset results

      nsCOMPtr<nsIDocument> doc = mRoot ? mRoot->GetComposedDoc() : nullptr;
      if (doc) {
        bool shouldDelay;
        LoadDataSources(doc, &shouldDelay);
        if (!shouldDelay) {
          Rebuild();
        }
      }
    }

    // mRoot should not be cleared until after Uninit is finished so that
    // generated content can be removed during uninitialization.
    void UninitFalse() { Uninit(false); mRoot = nullptr; }
    void UninitTrue() { Uninit(true); mRoot = nullptr; }

    /**
     * Find the <template> tag that applies for this builder
     */
    nsresult
    GetTemplateRoot(nsIContent** aResult);

    /**
     * Compile the template's queries
     */
    nsresult
    CompileQueries();

    /**
     * Compile the template given a <template> in aTemplate. This function
     * is called recursively to handle queries inside a queryset. For the
     * outer pass, aIsQuerySet will be false, while the inner pass this will
     * be true.
     *
     * aCanUseTemplate will be set to true if the template's queries could be
     * compiled, and false otherwise. If false, the entire template is
     * invalid.
     *
     * @param aTemplate <template> to compile
     * @param aQuerySet first queryset
     * @param aIsQuerySet true if 
     * @param aPriority the queryset index, incremented when a new one is added
     * @param aCanUseTemplate true if template is valid
     */
    nsresult
    CompileTemplate(nsIContent* aTemplate,
                    nsTemplateQuerySet* aQuerySet,
                    bool aIsQuerySet,
                    int32_t* aPriority,
                    bool* aCanUseTemplate);

    /**
     * Compile a query using the extended syntax. For backwards compatible RDF
     * syntax where there is no <query>, the <conditions> becomes the query.
     *
     * @param aRuleElement <rule> element
     * @param aActionElement <action> element
     * @param aMemberVariable member variable for the query
     * @param aQuerySet the queryset
     */
    nsresult 
    CompileExtendedQuery(nsIContent* aRuleElement,
                         nsIContent* aActionElement,
                         nsIAtom* aMemberVariable,
                         nsTemplateQuerySet* aQuerySet);

    /**
     * Determine the ref variable and tag from inside a RDF query.
     */
    void DetermineRDFQueryRef(nsIContent* aQueryElement, nsIAtom** tag);

    /**
     * Determine the member variable from inside an action body. It will be
     * the value of the uri attribute on a node.
     */
    already_AddRefed<nsIAtom> DetermineMemberVariable(nsIContent* aElement);

    /**
     * Compile a simple query. A simple query is one that doesn't have a
     * <query> and should use a default query which would normally just return
     * a list of children of the reference point.
     *
     * @param aRuleElement the <rule>
     * @param aQuerySet the query set
     * @param aCanUseTemplate true if the query is valid
     */
    nsresult 
    CompileSimpleQuery(nsIContent* aRuleElement,
                       nsTemplateQuerySet* aQuerySet,
                       bool* aCanUseTemplate);

    /**
     * Compile the <conditions> tag in a rule
     *
     * @param aRule template rule
     * @param aConditions <conditions> element
     */
    nsresult
    CompileConditions(nsTemplateRule* aRule, nsIContent* aConditions);

    /**
     * Compile a <where> tag in a condition. The caller should set
     * *aCurrentCondition to null for the first condition. This value will be
     * updated to point to the new condition before returning. The conditions
     * will be added to the rule aRule by this method.
     *
     * @param aRule template rule
     * @param aCondition <where> element
     * @param aCurrentCondition compiled condition
     */
    nsresult
    CompileWhereCondition(nsTemplateRule* aRule,
                          nsIContent* aCondition,
                          nsTemplateCondition** aCurrentCondition);

    /**
     * Compile the <bindings> for an extended template syntax rule.
     */
    nsresult
    CompileBindings(nsTemplateRule* aRule, nsIContent* aBindings);

    /**
     * Compile a single binding for an extended template syntax rule.
     */
    nsresult
    CompileBinding(nsTemplateRule* aRule, nsIContent* aBinding);

    /**
     * Add automatic bindings for simple rules
     */
    nsresult
    AddSimpleRuleBindings(nsTemplateRule* aRule, nsIContent* aElement);

    static void
    AddBindingsFor(nsXULTemplateBuilder* aSelf,
                   const nsAString& aVariable,
                   void* aClosure);

    /**
     * Load the datasources for the template. shouldDelayBuilding is an out
     * parameter which will be set to true to indicate that content building
     * should not be performed yet as the datasource has not yet loaded. If
     * false, the datasource has already loaded so building can proceed
     * immediately. In the former case, the datasource or query processor
     * should either rebuild the template or update results when the
     * datasource is loaded as needed.
     */
    nsresult
    LoadDataSources(nsIDocument* aDoc, bool* shouldDelayBuilding);

    /**
     * Called by LoadDataSources to load a datasource given a uri list
     * in aDataSource. The list is a set of uris separated by spaces.
     * If aIsRDFQuery is true, then this is for an RDF datasource which
     * causes the method to check for additional flags specific to the
     * RDF processor.
     */
    nsresult
    LoadDataSourceUrls(nsIDocument* aDocument,
                       const nsAString& aDataSources,
                       bool aIsRDFQuery,
                       bool* aShouldDelayBuilding);

    nsresult
    InitHTMLTemplateRoot();

    /**
     * Determine which rule matches a given result. aContainer is used for
     * tag matching and is optional for non-content generating builders.
     * The returned matched rule is always one of the rules owned by the
     * query set aQuerySet.
     *
     * @param aContainer parent where generated content will be inserted
     * @param aResult result to match
     * @param aQuerySet query set to examine the rules of
     * @param aMatchedRule [out] rule that has matched, or null if any.
     * @param aRuleIndex [out] index of the rule
     */
    nsresult
    DetermineMatchedRule(nsIContent* aContainer,
                         nsIXULTemplateResult* aResult,
                         nsTemplateQuerySet* aQuerySet,
                         nsTemplateRule** aMatchedRule,
                         int16_t *aRuleIndex);

    // XXX sigh, the string template foo doesn't mix with
    // operator->*() on egcs-1.1.2, so we'll need to explicitly pass
    // "this" and use good ol' fashioned static callbacks.
    void
    ParseAttribute(const nsAString& aAttributeValue,
                   void (*aVariableCallback)(nsXULTemplateBuilder* aThis, const nsAString&, void*),
                   void (*aTextCallback)(nsXULTemplateBuilder* aThis, const nsAString&, void*),
                   void* aClosure);

    nsresult
    SubstituteText(nsIXULTemplateResult* aMatch,
                   const nsAString& aAttributeValue,
                   nsAString& aResult);

    static void
    SubstituteTextAppendText(nsXULTemplateBuilder* aThis, const nsAString& aText, void* aClosure);

    static void
    SubstituteTextReplaceVariable(nsXULTemplateBuilder* aThis, const nsAString& aVariable, void* aClosure);    

    nsresult 
    IsSystemPrincipal(nsIPrincipal *principal, bool *result);

    /**
     * Convenience method which gets a resource for a result. If a result
     * doesn't have a resource set, it will create one from the result's id.
     */
    nsresult GetResultResource(nsIXULTemplateResult* aResult,
                               nsIRDFResource** aResource);

protected:
    virtual ~nsXULTemplateBuilder();

    nsCOMPtr<nsISupports> mDataSource;
    nsCOMPtr<nsIRDFDataSource> mDB;
    nsCOMPtr<nsIRDFCompositeDataSource> mCompDB;

    /**
     * Circular reference, broken when the document is destroyed.
     */
    nsCOMPtr<nsIContent> mRoot;

    /**
     * The root result, translated from the root element's ref
     */
    nsCOMPtr<nsIXULTemplateResult> mRootResult;

    nsCOMArray<nsIXULBuilderListener> mListeners;

    /**
     * The query processor which generates results
     */
    nsCOMPtr<nsIXULTemplateQueryProcessor> mQueryProcessor;

    /**
     * The list of querysets
     */
    nsTArray<nsTemplateQuerySet *> mQuerySets;

    /**
     * Set to true if the rules have already been compiled
     */
    bool          mQueriesCompiled;

    /**
     * The default reference and member variables.
     */
    nsCOMPtr<nsIAtom> mRefVariable;
    nsCOMPtr<nsIAtom> mMemberVariable;

    /**
     * The match map contains nsTemplateMatch objects, one for each unique
     * match found, keyed by the resource for that match. A particular match
     * will contain a linked list of all of the matches for that unique result
     * id. Only one is active at a time. When a match is retracted, look in
     * the match map, remove it, and apply the next valid match in sequence to
     * make active.
     */
    nsDataHashtable<nsISupportsHashKey, nsTemplateMatch*> mMatchMap;

    // pseudo-constants
    static nsrefcnt gRefCnt;
    static nsIRDFService*            gRDFService;
    static nsIRDFContainerUtils*     gRDFContainerUtils;
    static nsIScriptSecurityManager* gScriptSecurityManager;
    static nsIPrincipal*             gSystemPrincipal;
    static nsIObserverService*       gObserverService;

    enum {
        eDontTestEmpty = (1 << 0),
        eDontRecurse = (1 << 1),
        eLoggingEnabled = (1 << 2)
    };

    int32_t mFlags;

    /**
     * Stack-based helper class to maintain a list of ``activated''
     * resources; i.e., resources for which we are currently building
     * content.
     */
    class ActivationEntry {
    public:
        nsIRDFResource   *mResource;
        ActivationEntry  *mPrevious;
        ActivationEntry **mLink;

        ActivationEntry(nsIRDFResource *aResource, ActivationEntry **aLink)
            : mResource(aResource),
              mPrevious(*aLink),
              mLink(aLink) { *mLink = this; }

        ~ActivationEntry() { *mLink = mPrevious; }
    };

    /**
     * The top of the stack of resources that we're currently building
     * content for.
     */
    ActivationEntry *mTop;

    /**
     * Determine if a resource is currently on the activation stack.
     */
    bool
    IsActivated(nsIRDFResource *aResource);

    /**
     * Returns true if content may be generated for a result, or false if it
     * cannot, for example, if it would be created inside a closed container.
     * Those results will be generated when the container is opened.
     * If false is returned, no content should be generated. Possible 
     * insertion locations may optionally be set for new content, depending on
     * the builder being used. Note that *aLocations or some items within
     * aLocations may be null.
     */
    virtual bool
    GetInsertionLocations(nsIXULTemplateResult* aResult,
                          nsCOMArray<nsIContent>** aLocations) = 0;

    /**
     * Must be implemented by subclasses. Handle removing the generated
     * output for aOldMatch and adding new output for aNewMatch. Either
     * aOldMatch or aNewMatch may be null. aContext is the location returned
     * from the call to MayGenerateResult.
     */
    virtual nsresult
    ReplaceMatch(nsIXULTemplateResult* aOldResult,
                 nsTemplateMatch* aNewMatch,
                 nsTemplateRule* aNewMatchRule,
                 void *aContext) = 0;

    /**
     * Must be implemented by subclasses. Handle change in bound
     * variable values for aResult. aModifiedVars contains the set
     * of variables that have changed.
     * @param aResult the ersult for which variable bindings has changed.
     * @param aModifiedVars the set of variables for which the bindings
     * have changed.
     */
    virtual nsresult
    SynchronizeResult(nsIXULTemplateResult* aResult) = 0;

    /**
     * Output a new match or removed match to the console.
     *
     * @param aId id of the result
     * @param aMatch new or removed match
     * @param aIsNew true for new matched, false for removed matches
     */
    void
    OutputMatchToLog(nsIRDFResource* aId,
                     nsTemplateMatch* aMatch,
                     bool aIsNew);

    virtual void Traverse(nsCycleCollectionTraversalCallback &cb) const
    {
    }

    /**
     * Start observing events from the observer service and the given
     * document.
     *
     * @param aDocument the document to observe
     */
    void StartObserving(nsIDocument* aDocument);

    /**
     * Stop observing events from the observer service and any associated
     * document.
     */
    void StopObserving();

    /**
     * Document that we're observing. Weak ref!
     */
    nsIDocument* mObservedDocument;
};

#endif // nsXULTemplateBuilder_h__