/*
 * Copyright (c) 2007-2008 Nokia Corporation and/or its subsidiary(-ies).
 * All rights reserved.
 * This component and the accompanying materials are made available
 * under the terms of the License "Eclipse Public License v1.0"
 * which accompanies this distribution, and is available
 * at the URL "http://www.eclipse.org/legal/epl-v10.html".
 *
 * Initial Contributors:
 * Nokia Corporation - initial contribution.
 *
 * Contributors:
 *
 * Description: 
 *
 */

package com.nokia.helium.ant.data;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.tools.ant.Project;
import org.dom4j.Comment;
import org.dom4j.Element;
import org.dom4j.Node;

/**
 * Base class for all Ant Meta objects. Each Ant object is represented by a meta
 * object that provides core and additional data about it.
 */
public class AntObjectMeta {

    public static final Map<String, Integer> SCOPES;

    static {
        Map<String, Integer> tempMap = new HashMap<String, Integer>();
        tempMap.put("public", new Integer(1));
        tempMap.put("protected", new Integer(2));
        tempMap.put("private", new Integer(3));
        SCOPES = Collections.unmodifiableMap(tempMap);
    }

    /** The default scope if an element does not have a defined scope. */
    public static final String DEFAULT_SCOPE = "public";

    private static AntComment emptyComment;

    private Project runtimeProject;

    /** The parent meta object. */
    private AntObjectMeta parent;
    /** The dom4j XML Element of the Ant object represented by this meta object. */
    private Node node;
    /** The AntComment of any preceeding comment block. */
    private AntComment comment = emptyComment;

    static {
        emptyComment = new AntComment();
    }

    /**
     * Constructor.
     * 
     * @param parent The parent meta object.
     * @param node The XML node of the Ant object.
     */
    public AntObjectMeta(AntObjectMeta parent, Node node) {
        this.parent = parent;
        this.node = node;
        processComment();
    }

    public Project getRuntimeProject() {
        return runtimeProject;
    }

    public void setRuntimeProject(Project project) {
        this.runtimeProject = project;
    }

    /**
     * Gets an attribute if a value is available, otherwise returns an emtpy
     * string.
     * 
     * @param name Attribute name.
     * @return Attribute value.
     */
    public String getAttr(String name) {
        if (node.getNodeType() == Node.ELEMENT_NODE) {
            String value = ((Element) node).attributeValue(name);
            if (value != null) {
                return value;
            }
        }
        return "";
    }

    protected Node getNode() {
        return node;
    }

    protected AntComment getEmptyComment() {
        return emptyComment;
    }

    /**
     * Returns the meta object of the top-level project for this Ant object.
     */
    public RootAntObjectMeta getRootMeta() {
        if (parent instanceof RootAntObjectMeta) {
            return (RootAntObjectMeta) parent;
        }
        return parent.getRootMeta();
    }

    public AntObjectMeta getParent() {
        return parent;
    }

    /**
     * Returns the Ant file this Ant object is contained in.
     */
    public AntFile getAntFile() {
        return getRootMeta().getAntFile();
    }

    /**
     * Returns the top-level database object.
     */
    public Database getDatabase() {
        return getAntFile().getDatabase();
    }

    /**
     * Returns the name of the object or an empty string.
     * 
     * @return Object name.
     */
    public String getName() {
        String name = getAttr("name");
        if (name.length() == 0) {
            name = getComment().getObjectName();
        }
        return name;
    }

    /**
     * Returns the location path of the object.
     * 
     * @return Location path string.
     */
    public String getLocation() {
        RootAntObjectMeta rootMeta = getRootMeta();
        return rootMeta.getFilePath() + ":" + getLineNumber();
    }

    public Integer getLineNumber() {
        int lineNum = 0;
        if (node != null && node instanceof ElementWithLocation) {
            lineNum = ((ElementWithLocation) node).getLineNumber();
        }
        return lineNum;
    }

    /**
     * Returns the first line summary from a doc comment.
     * 
     * @return The documentation text.
     */
    public String getSummary() {
        return getComment().getSummary();
    }

    /**
     * Returns the documentation block from a associated comment.
     * 
     * @return The documentation text.
     */
    public String getDocumentation() {
        return getComment().getDocumentation();
    }

    /**
     * Returns the scope of the object, or an empty string. This could be
     * public, protected or private.
     * 
     * @return The object scope.
     */
    public String getScope() {
        String scope = getComment().getTagValue("scope");
        if (scope.equals("")) {
            scope = DEFAULT_SCOPE;
        }
        return scope;
    }

    public boolean matchesScope(String scopeFilter) {
        if (!SCOPES.containsKey(scopeFilter)) {
            throw new IllegalArgumentException("Invalid scope filter: " + scopeFilter);
        }
        String scope = getScope();
        if (scope.length() > 0 && !SCOPES.containsKey(scope)) {
            log("Invalid scope: " + scope + ", " + toString(), Project.MSG_WARN);
            return false;
        }
        return SCOPES.get(scope).compareTo(SCOPES.get(scopeFilter)) <= 0;
    }

    /**
     * Returns the deprecated text if the object has been deprecated, or an
     * empty string.
     * 
     * @return Deprecated descripion.
     */
    public String getDeprecated() {
        return comment.getTagValue("deprecated");
    }

    /**
     * Returns the content of the "since" tag that should indicate which release
     * this feature was first added.
     * 
     * @return Since release number.
     */
    public String getSince() {
        return comment.getTagValue("since");
    }

    /**
     * Returns the source XML of the object.
     * 
     * @return The XML string.
     */
    public String getSource() {
        // Add the raw XML content of the element
        String sourceXml = node.asXML();
        // Replace the CDATA end notation to avoid nested CDATA sections
        sourceXml = sourceXml.replace("]]>", "] ]>");
        return sourceXml;
    }

    /**
     * Returns the AntComment that represents a preceeding comment.
     * 
     * @return An Ant comment.
     */
    protected AntComment getComment() {
        return comment;
    }

    protected void setComment(AntComment comment) {
        this.comment = comment;
    }

    private void processComment() {
        Comment commentNode = getCommentNode();
        if (commentNode != null) {
            comment = new AntComment(commentNode);
        }
    }

    @SuppressWarnings("unchecked")
    private Comment getCommentNode() {
        Node commentNode = null;
        if (node.getNodeType() == Node.COMMENT_NODE) {
            commentNode = node;
        } else {
            List<Node> children = node.selectNodes("preceding-sibling::node()");
            if (children.size() > 0) {
                // Scan past the text nodess, which are most likely whitespace
                int index = children.size() - 1;
                Node child = children.get(index);
                while (index > 0 && child.getNodeType() == Node.TEXT_NODE) {
                    index--;
                    child = children.get(index);
                }

                // Check if there is a comment node
                if (child.getNodeType() == Node.COMMENT_NODE) {
                    commentNode = child;
                    log("Node has comment: " + node.getStringValue(), Project.MSG_DEBUG);
                } else {
                    log("Node has no comment: " + node.toString(), Project.MSG_WARN);
                }
            }
        }
        return (Comment) commentNode;
    }

    public void log(String text, int level) {
        Project project = getRuntimeProject();
        if (project != null) {
            project.log(text, level);
        }
    }

    public String toString() {
        return getName();
    }

    public List<TaskMeta> getTasks(String taskType) {
        return getTaskDefinitions(".//" + taskType);
    }

    @SuppressWarnings("unchecked")
    public List<MacroMeta> getScriptDefinitions(String xpathExpression) {
        List<Element> nodes = getNode().selectNodes(xpathExpression);
        List<MacroMeta> scripts = new ArrayList<MacroMeta>();
        for (Element node : nodes) {
            MacroMeta macroMeta = new MacroMeta(this, node);
            macroMeta.setRuntimeProject(getRuntimeProject());
            scripts.add(macroMeta);
        }
        return scripts;
    }

    @SuppressWarnings("unchecked")
    public List<TaskMeta> getTaskDefinitions(String xpathExpression) {
        List<Element> nodes = getNode().selectNodes(xpathExpression);
        List<TaskMeta> tasks = new ArrayList<TaskMeta>();
        for (Element node : nodes) {
            TaskMeta taskMeta = new TaskMeta(this, node);
            taskMeta.setRuntimeProject(getRuntimeProject());
            tasks.add(taskMeta);
        }
        return tasks;
    }
}
