/*
 * Copyright 2013, Rimero Solutions
 *
 * 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.
 */
package com.rimerosolutions.ant.git;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.LanguageVersion;
import com.sun.javadoc.MethodDoc;
import com.sun.javadoc.RootDoc;
import com.sun.tools.doclets.standard.Standard;

/**
 * Quick and very dirty doclet for Ant tasks documentation.
 * Self-Contained for now, no template engine or external dependencies.
 *
 * @author Yves Zoundi
 */
public final class AntTaskDoclet extends Standard {
        private static final String ENCODING_UTF_8 = "UTF-8";
        private static final String ANT_TASK_CLASS_NAME = "org.apache.tools.ant.Task";
        private static String destDir;
        private static String doctitle = "Ant tasks documentation";
        private static String header = "Ant tasks documentation";
        private static String windowtitle = "Ant tasks documentation";
        private static String bottom;
        private static String overview;
        private static final Logger LOG = Logger.getLogger(AntTaskDoclet.class.getName());
        private static final List<String> ELEMENT_PREFIXES = Arrays.asList("create", "add", "addConfigured");
        private static final List<String> ATTRIBUTE_PREFIXES = Arrays.asList("set");
        private static final String TAG_ANTDOC_NOTREQUIRED = "antdoc.notrequired";
        private static Map<String, ClassData> classDataMap = new HashMap<String, ClassData>();
        private static final String HTML_INDEX_PAGE = "index.html";
        private static final String HTML_HEADER_PAGE = "header.html";
        private static final String HTML_BODY_PAGE = "body.html";
        private static final String HTML_NAV_PAGE = "nav.html";

        private static final class ElementData {
                String name;
                String description;

                static ElementData fromMethodDoc(MethodDoc methodDoc) {
                        ElementData data = new ElementData();
                        data.name = sanitizeName(methodDoc.name(), ELEMENT_PREFIXES);
                        data.description = methodDoc.commentText();

                        return data;
                }
        }

        private static final class AttributeData {
                String name;
                String description;
                boolean required;

                static AttributeData fromMethodDoc(MethodDoc methodDoc) {
                        AttributeData data = new AttributeData();
                        data.description = methodDoc.commentText();
                        data.required = !(methodDoc.tags(TAG_ANTDOC_NOTREQUIRED).length > 0);
                        data.name = sanitizeName(methodDoc.name(), ATTRIBUTE_PREFIXES);

                        return data;
                }
        }

        private static final class ClassData {
                String qualifiedName;
                String simpleName;
                String description;
                List<String> parentClassDatas = new ArrayList<String>();
                List<AttributeData> attributesList = new ArrayList<AttributeData>();
                List<ElementData> elementsList = new ArrayList<ElementData>();
                boolean hidden;
        }

        static interface WriterCallback {
                void doWithWriter(Writer w) throws IOException;
        }

        private static void copyFile(File sourceFile, File destFile) throws IOException {
                try (InputStream in = new FileInputStream(sourceFile);
                     OutputStream out = new FileOutputStream(destFile)) {
                        byte[] buf = new byte[4096];
                        int len;
                        while ((len = in.read(buf)) > 0) {
                                out.write(buf, 0, len);
                        }
                }
        }

        public static final boolean start(RootDoc root) {
                readDestDirOption(root.options());

                try {
                        writeContents(root.classes());
                } catch (IOException ioe) {
                        LOG.log(Level.SEVERE, "Ant tasks documentation failed", ioe);
                        return false;
                }

                return true;
        }

        public static void readDestDirOption(String options[][]) {
                for (int i = 0; i < options.length; i++) {
                        String[] opt = options[i];
                        if (opt[0].equals("-d")) {
                                destDir = opt[1];
                        } else if (opt[0].equals("-header")) {
                                header = opt[1];
                        } else if (opt[0].equals("-doctitle")) {
                                doctitle = opt[1];
                        } else if (opt[0].equals("-windowtitle")) {
                                windowtitle = opt[1];
                        } else if (opt[0].equals("-overview")) {
                                overview = opt[1];
                        } else if (opt[0].equals("-bottom")) {
                                bottom = opt[1];
                        }
                }
        }

        private static String sanitizeName(String objectName, List<String> prefixes) {
                String newName = objectName;

                for (String prefix : prefixes) {
                        if (newName.startsWith(prefix)) {
                                String methodName = newName.substring(prefix.length() + 1);
                                return (("" + newName.charAt(prefix.length())).toLowerCase() + methodName).toLowerCase();
                        }
                }

                return newName;

        }

        static void withWriter(File f, WriterCallback wc) throws IOException {
                try (Writer w = new OutputStreamWriter(new FileOutputStream(f), ENCODING_UTF_8)) {
                        wc.doWithWriter(w);
                }
        }

        private static void writeIndexPage() throws IOException {
                withWriter(new File(new File(destDir), HTML_INDEX_PAGE), new WriterCallback() {

                                @Override
                                public void doWithWriter(Writer w) throws IOException {
                                        StringBuilder sb = new StringBuilder();

                                        sb.append("<html><head>");
                                        sb.append(htmlElement("title", windowtitle));
                                        sb.append("</head>");
                                        sb.append("<frameset rows=\"15%,*\">");
                                        sb.append(" <frame src=\"");
                                        sb.append(HTML_HEADER_PAGE);
                                        sb.append("\">");
                                        sb.append(" <frameset cols=\"25%,75%\">");
                                        sb.append("<frame src=\"");
                                        sb.append(HTML_NAV_PAGE);
                                        sb.append("\">");
                                        sb.append("<frame name=\"bodycontents\" src=\"");
                                        sb.append(HTML_BODY_PAGE);
                                        sb.append("\">");
                                        sb.append("</frameset></frameset></html>");
                                        sb.append("</html>");

                                        w.write(sb.toString());
                                }
                        });
        }

        private static void writeHeaderPage() throws IOException {
                withWriter(new File(new File(destDir), HTML_HEADER_PAGE), new WriterCallback() {
                                @Override
                                public void doWithWriter(Writer w) throws IOException {
                                        StringBuilder sb = new StringBuilder();

                                        sb.append("<html><head>");
                                        sb.append(htmlElement("title", windowtitle));
                                        sb.append("</head><body>");
                                        sb.append(htmlElement("h1", header));
                                        sb.append("</body></html>");

                                        w.write(sb.toString());
                                }
                        });
        }

        private static void writeBodyPage() throws IOException {
                if (overview == null) {
                        withWriter(new File(new File(destDir), HTML_BODY_PAGE), new WriterCallback() {
                                        @Override
                                        public void doWithWriter(Writer w) throws IOException {
                                                StringBuilder sb = new StringBuilder();

                                                sb.append("<html><head>");
                                                sb.append(htmlElement("title", windowtitle));
                                                sb.append("</head><body>");
                                                sb.append(htmlElement("div", doctitle));
                                                sb.append("</body></html>");

                                                w.write(sb.toString());
                                        }
                                });
                } else {
                        copyFile(new File(overview), new File(new File(destDir), HTML_BODY_PAGE));
                }
        }

        private static void writeNavPage() throws IOException {
                withWriter(new File(new File(destDir), HTML_NAV_PAGE), new WriterCallback() {
                                @Override
                                public void doWithWriter(Writer w) throws IOException {
                                        StringBuilder sb = new StringBuilder();

                                        sb.append("<html><head><title>Nav</title></head><body><div><ul>");

                                        for (Map.Entry<String, ClassData> entry : classDataMap.entrySet()) {
                                                if (!entry.getValue().hidden) {
                                                        sb.append("<li>");
                                                        sb.append("<a target=\"bodycontents\" href=\"");
                                                        sb.append(entry.getKey()).append(".html");
                                                        sb.append("\">");
                                                        sb.append(entry.getValue().simpleName);
                                                        sb.append("</a></li>");
                                                }
                                        }

                                        sb.append("</ul></div></body></html>");
                                        w.write(sb.toString());
                                }
                        });
        }

        private static String htmlElement(String tagName, Object contents) {
                return new StringBuilder().
                        append('<').
                        append(tagName).
                        append('>').
                        append(contents).
                        append("</").
                        append(tagName).
                        append('>').
                        toString();
        }

        private static void writeHtmlTasks() throws IOException {
                Map<String, ClassData> classDataCopy = new HashMap<String, AntTaskDoclet.ClassData>(classDataMap);

                for (Map.Entry<String, ClassData> entry : classDataMap.entrySet()) {
                        String classDocName = entry.getKey();
                        ClassData classData = entry.getValue();

                        if (classData.hidden) {
                                continue;
                        }

                        File outputFile = new File(new File(destDir), classDocName + ".html");
                        OutputStreamWriter w = new OutputStreamWriter(new FileOutputStream(outputFile), ENCODING_UTF_8);
                        StringBuilder header = new StringBuilder();

                        header.append("<html><head><title>classDocName</title><body><div>");
                        w.write(header.toString());

                        header = new StringBuilder();
                        header.append(htmlElement("h1", classData.simpleName));
                        header.append("<hr/>");

                        header.append(htmlElement("h2", "Description"));
                        header.append(htmlElement("div", classData.description));

                        w.write(header.toString());

                        List<AttributeData> attributesCopy = new ArrayList<AntTaskDoclet.AttributeData>(classData.attributesList);
                        List<ElementData> elementsCopy = new ArrayList<AntTaskDoclet.ElementData>(classData.elementsList);

                        if (!classData.parentClassDatas.isEmpty()) {
                                for (String parent : classData.parentClassDatas) {
                                        attributesCopy.addAll(classDataCopy.get(parent).attributesList);
                                        elementsCopy.addAll(classDataCopy.get(parent).elementsList);
                                }
                        }

                        if (!attributesCopy.isEmpty()) {
                                StringBuilder sb = new StringBuilder();

                                sb.append("<h2>Attributes</h2>");
                                sb.append("<table border='1'>");
                                sb.append("<tr>");
                                sb.append("<th>Name</th>").append("<th>Description</th>").append("<th>Required</th>");
                                sb.append("</tr>");

                                for (AttributeData attr : attributesCopy) {
                                        sb.append("<tr>");
                                        sb.append(htmlElement("td", attr.name));
                                        sb.append(htmlElement("td", attr.description));
                                        sb.append(htmlElement("td", attr.required));
                                        sb.append("</tr>");
                                }

                                sb.append("</table>");

                                w.write(sb.toString());
                        }

                        if (!elementsCopy.isEmpty()) {
                                StringBuilder sb = new StringBuilder();

                                sb.append("<h2>Nested elements</h2>");
                                sb.append("<table border='1'>");
                                sb.append("<tr>");
                                sb.append("<th>Name</th>").append("<th>Description</th>");
                                sb.append("</tr>");

                                for (ElementData attr : elementsCopy) {
                                        sb.append("<tr>");
                                        sb.append(htmlElement("td", attr.name));
                                        sb.append(htmlElement("td", attr.description));
                                        sb.append("</tr>");
                                }

                                sb.append("</table>");

                                w.write(sb.toString());
                        }

                        header = new StringBuilder();
                        header.append("<div><p>").append(bottom).append("</p></div>");
                        w.write(header.toString());

                        header = new StringBuilder();
                        header.append("</div></body></html>");
                        w.write(header.toString());

                        w.flush();
                        w.close();
                }
        }

        private static void writeHtmlToOutputDir() throws IOException {
                writeBodyPage();
                writeNavPage();
                writeHeaderPage();
                writeIndexPage();
                writeHtmlTasks();
        }

        private static List<String> collectParentClassesNames(ClassDoc doc) {
                List<String> parents = new ArrayList<String>();
                ClassDoc currentParent = doc.superclass();

                while (currentParent != null) {
                        if (currentParent.qualifiedTypeName().equals(ANT_TASK_CLASS_NAME)) {
                                break;
                        }
                        parents.add(currentParent.qualifiedTypeName());
                        currentParent = currentParent.superclass();
                }

                return parents;
        }

        private static boolean isAntTask(ClassDoc classDoc) {
                ClassDoc currentParent = classDoc.superclass();

                while (currentParent != null) {
                        if (currentParent.qualifiedTypeName().equals(ANT_TASK_CLASS_NAME)) {
                                return true;
                        }

                        currentParent = currentParent.superclass();
                }

                return false;
        }

        private static void registerClassData(ClassDoc doc) {
                ClassData data = new ClassData();

                Scanner sc = new Scanner(doc.commentText());

                data.description = sc.nextLine();
                sc.close();
                data.qualifiedName = doc.qualifiedTypeName();
                data.simpleName = doc.name();
                data.hidden = doc.isAbstract();
                data.parentClassDatas.addAll(collectParentClassesNames(doc));

                for (MethodDoc methodDoc : doc.methods()) {
                        if (!isHiddenMethodDoc(methodDoc)) {
                                if (isTaskAttribute(methodDoc)) {
                                        data.attributesList.add(AttributeData.fromMethodDoc(methodDoc));
                                } else if (isTaskElement(methodDoc)) {
                                        data.elementsList.add(ElementData.fromMethodDoc(methodDoc));
                                }
                        }
                }

                classDataMap.put(data.qualifiedName, data);
        }

        private static boolean isHiddenMethodDoc(MethodDoc methodDoc) {
                return methodDoc.isPrivate() || methodDoc.isProtected() || methodDoc.isAbstract();
        }

        private static boolean namePrefixMatches(String name, List<String> prefixes) {
                for (String prefix : prefixes) {
                        if (name.startsWith(prefix)) {
                                return true;
                        }
                }

                return false;
        }

        private static boolean isTaskAttribute(MethodDoc methodDoc) {
                return namePrefixMatches(methodDoc.name(), ATTRIBUTE_PREFIXES);
        }

        private static boolean isTaskElement(MethodDoc methodDoc) {
                return namePrefixMatches(methodDoc.name(), ELEMENT_PREFIXES);
        }

        private static void writeContents(ClassDoc[] classDocs) throws IOException {
                for (ClassDoc classDoc : classDocs) {
                        if (isAntTask(classDoc)) {
                                registerClassData(classDoc);
                        }
                }

                writeHtmlToOutputDir();
        }

        public static LanguageVersion languageVersion() {
                return LanguageVersion.JAVA_1_5;
        }
}