package org.apache.maven.html2xdoc;

/* ====================================================================
 *   Copyright 2001-2004 The Apache Software Foundation.
 *
 *   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.
 * ====================================================================
 */

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dom4j.CharacterData;
import org.dom4j.Comment;
import org.dom4j.Document;
import org.dom4j.DocumentFactory;
import org.dom4j.Element;
import org.dom4j.Node;

/**
 * A simple bean for converting a HTML document into an XDoc compliant XML
 * document.
 * This could be done via XSLT but is a little more complex than it might first
 * appear so its done via Java code instead.
 *
 * @author <a href="mailto:jstrachan@apache.org">James Strachan</a>
 */
public class Html2XdocBean {

    /** The Log to which logging calls will be made. */
    private static final Log log = LogFactory.getLog(Html2XdocBean.class);

    /**
     * Used to create the output document
     */
    private DocumentFactory factory = new DocumentFactory();

    /**
     * The current node to attach the sub-nodes.
     */
    private Element currentNode = null;

    /**
     * The current 'root' section node. This is used to keep
     * track of the root section so that when a subsection is
     * found it can be associated correctly.
     */
    private Element currentSectionNode = null;

    /**
     * The current section heading level. If a subsequent level
     * lower or equal, then create a new section.
     */
    private int currentSectionHeadingLevel = Integer.MIN_VALUE;

    /**
     * The current paragraph node. This is used to associate text
     * and formatting nodes to the current paragraph node.
     */
    private Element currentParaNode = null;

    /**
     * Converts the given HTML document into the corresponding XDoc format
     * of XML
     *
     * @param html the input html document
     * @return Document
     */
    public Document convert(Document html) {
        Document doc = factory.createDocument();
        Element root = doc.addElement("document");
        Element properties = root.addElement("properties");
        Element title = properties.addElement("title");
        title.setText(html.valueOf("/html/head/title"));

        Element body = root.addElement("body");

        Element htmlContent = (Element) html.selectSingleNode("/html/body");
        if (htmlContent == null) {
            log.info("No body element found for HTML document: "
                + html.asXML());
        } else {
            addSections(body, htmlContent);
        }
        return doc;
    }

    /**
     * Iterates thorugh the given body looking for h1, h2, h3 nodes and
     * creating the associated section elements. Any text nodes
     * contained inside the body are wrapped in a &lt;p&gt; element
     *
     * @param output the output destination
     * @param body the block of HTML markup to convert
     */
    protected void addSections(Element output, Element body) {
        List content = getBodyContent(body.content());

        for (Iterator iter = content.iterator(); iter.hasNext();) {
            Node node = (Node) iter.next();
            if (isHeading(node)) {
                makeSection(output, node);
            } else {
                guaranteeHasSection(output);
                processNode(node);
            }
        }

    }

    /**
     * main algorithm which represents the iteration contract.
     * Use the protected methods to change the behavior.
     *
     * @param node the node to process
     */
    private void processNode(Node node) {
        if (isCharacterData(node)) {
            addTextNode(node);
        } else if (isTextFormatting(node)) {
            addFormattingNode(node);
        } else {
            addNode(node);
        }
    }

    /**
     * Specifies whether the node is a text modifying construct that should be
     * passed as is to the resultant html. Such as an anchor '&lt;a&gt;'.
     *
     * @param node the node to check
     * @return true if the node is used to modify the formatting of the
     *         text; otherwise, false
     */
    protected boolean isTextFormatting(Node node) {
        // Ultimately this needs bold, italic, and so on
        return node.getName() != null && node.getName().equals("a");
    }

    /**
     * Specifies whether the node is character data and should be passed as
     * straight text to the resultant html.
     *
     * @param node the node to check
     * @return true if the node is a text node; otherwise, false.
     */
    protected boolean isCharacterData(Node node) {
        return node instanceof CharacterData
            && (node instanceof Comment) == false;
    }

    /**
     * Specifies whether the node is a heading node.
     *
     * @param node the node to check
     * @return true if the given node is a heading element
     *         (h1, h2, h3 etc); otherwise, false
     */
    protected boolean isHeading(Node node) {
        String name = node.getName();
        return name != null && name.startsWith("h");
    }

    /**
     * Determines the heading level of the node.
     *
     * @param node the node to check
     * @return the integer level of the heading
     */
    protected int determineHeadingLevel(Node node) {
        try {
            String name = node.getName().substring(1);
            return Integer.parseInt(name);
        } catch (NumberFormatException nfe) {
            return 1;
        }
    }

    /**
     * Creates a section or subsection as necessary based on the node
     * for the output document.
     *
     * @param output the output document to attach the section
     * @param node the node to base making a section on
     */
    protected void makeSection(Element output, Node node) {
        int level = determineHeadingLevel(node);
        if (needsNewSection(node)) {
            currentNode = output.addElement("section");
            currentSectionHeadingLevel = level;
            currentSectionNode = currentNode;
        } else {
            currentNode = currentSectionNode.addElement("subsection");
        }
        currentNode.addAttribute("name", getSectionText(node));
        currentParaNode = null;
    }

    /**
     * @return the section text for the given node. If the node
     * contains an embedded element (such as an &lt;a&gt; element)
     * then return its text
     */
    protected String getSectionText(Node node) {
        String text = node.getText().trim();
        if (text.length() <= 0 && node instanceof Element) {
            Element element = (Element) node;

            // maybe we contain a hypertext link
            List childElements = element.elements();
            if (! childElements.isEmpty()) {
                Node child = (Node) childElements.get(0);
                return child.getText();
            }
        }
        return text;
    }

    /**
     * Determines if a new section is needed which is based on whether
     * the node's a heading level and equal to or less than the current
     * section's heading level.
     *
     * @param node the node to check
     * @return true if the current node's information means for a new
     *         section; otherwise, false
     */
    protected boolean needsNewSection(Node node) {
        int level = determineHeadingLevel(node);
        return level <= currentSectionHeadingLevel
            || currentSectionNode == null;
    }

    /**
     * Determines if a paragraph node is needed.
     */
    private void guaranteeHasParaNode() {
        if (currentParaNode == null) {
            currentParaNode = currentNode.addElement("p");
        }
    }

    /**
     * Makes sure the current node is section, if necessary.
     * @param output the output element to add the section to
     */
    private void guaranteeHasSection(Element output) {
        if (currentNode == null) {
            // we have a section with no name
            // should we default it to be the same as the document title?
            currentNode = output.addElement("section");
        }
    }

    /**
     * Add the node to the current node.
     * @param node the node to add
     */
    private void addNode(Node node) {
        if (currentParaNode != null && ! shouldBreakPara(node)) {
            currentParaNode.add(cloneNode(node));
        }
        else {
            currentNode.add(cloneNode(node));
            currentParaNode = null;
        }
    }

    /**
     * @return true if the paragraph should be split, such as for a br or p
     * tag
     */
    protected boolean shouldBreakPara(Node node) {
        String name = node.getName();
        return "p".equals(name) || "br".equals(name);
    }

    /**
     * Adds the text of the node to the current paragraph.
     * @param node the node to add
     */
    private void addTextNode(Node node) {
        guaranteeHasParaNode();
        currentParaNode.addText(node.getText());
    }

    /**
     * Adds the node to the current paragraph.
     * @param node the node to add
     */
    private void addFormattingNode(Node node) {
        guaranteeHasParaNode();
        currentParaNode.add(cloneNode(node));
    }

    /**
     * Returns a copy of the body content, removing any whitespace from
     * the beginning and end.
     *
     * @param content the content node list to obtain body content from
     * @return List
     */
    protected List getBodyContent(List content) {
        // lets turn <pre> into <source> and concatenate consective entries
        Element lastPre = null;
        LinkedList list = new LinkedList();
        boolean lastWasElement = true;
        for (Iterator iter = content.iterator(); iter.hasNext();) {
            Node node = (Node) iter.next();

            if (isPre(node)) {
                if (lastPre == null) {
                    lastPre = factory.createElement("source");
                    list.add(lastPre);
                }
                lastPre.addText(node.getText());
            } else {
                if (isWhitespace(node) && lastWasElement) {
                    if (lastPre != null) {
                        lastPre.addText(node.getText());
                    }
                } else {
                    lastWasElement = node instanceof Element;
                    if (lastWasElement) {
                        lastPre = null;
                    }
                    list.add(node);
                }
            }
        }

        if (list.size() == 0) return list;

        // now lets remove any whitespace text nodes at the beginning and end
        while (true) {
            Node node = (Node) list.getFirst();
            if (isWhitespace(node)) {
                list.removeFirst();
                continue;
            }
            break;
        }
        while (true) {
            Node node = (Node) list.getLast();
            if (isWhitespace(node)) {
                list.removeLast();
                continue;
            }
            break;
        }
        return list;
    }

    /**
     * @param node the node to check
     * @return true if the node is a pre tag; otherwise false.
     */
    protected boolean isPre(Node node) {
        if (node instanceof Element) {
            Element element = (Element) node;
            return element.getName().equals("pre");
        }
        return false;
    }

    /**
     * @param node the node to check
     * @return true if the given node is a whitespace text node
     */
    protected boolean isWhitespace(Node node) {
        if (node instanceof CharacterData) {
            String text = node.getText();
            return text.trim().length() <= 0;
        }
//        if (node instanceof Element) {
//            String name = node.getName();
//            if (name.equals("p")) {
//                String text = node.getText();
//                return text.trim().length() <= 0;
//            }
//            if (name.equals("br")) {
//                return true;
//            }
//        }
        return false;
    }

    /**
     * Normalizes the whitespace of any Elements
     *
     * @param node the node to clone
     * @return Node the cloned node
     */
    protected Node cloneNode(Node node) {
        Node answer = (Node) node.clone();
        if (answer instanceof Element) {
            Element element = (Element) answer;
            element.normalize();
        }
        return answer;
    }

}
