/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.netbeans.modules.javascript2.jquery;

import java.awt.Color;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.netbeans.modules.javascript2.types.api.DeclarationScope;
import org.netbeans.modules.javascript2.model.api.JsFunction;
import org.netbeans.modules.javascript2.model.api.JsObject;
import org.netbeans.modules.javascript2.model.spi.ModelElementFactory;
import org.netbeans.modules.javascript2.jquery.model.JQueryUtils;
import org.openide.util.Exceptions;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * This class loads all selectors from jquery-api.xml file.
 * 
 * @author Petr Pisl
 */
public class SelectorsLoader extends DefaultHandler {
    public static final String TYPE = "type";   //NOI18N
    public static final String NAME = "name";   //NOI18N
    public static final String SELECTOR = "selector";   //NOI18N
    public static final String METHOD = "method";   //NOI18N
    public static final String RETURN = "return";   //NOI18N
    private static final Color PRE_BACKGROUNDCOLOR = new Color(254, 254, 202); // #fefeca
    private static final Color TITLE_BACKGROUNDCOLOR = new Color(31, 111, 180); // #1f6fb4
    private static final Color TITLE_COLOR = Color.WHITE;
    private static final Color LINK_COLOR = Color.WHITE;
    private static final Color TABLE_BACKGROUNDCOLOR = new Color(209, 209, 209);// #d1d1d1
    private static final Color SIGNATURE_COLOR = new Color(102, 102, 102);// #666666
    private static final Color SIGNATURE_VERSION = new Color(93, 176, 230);// #5DB0E6

    private static final Logger LOGGER = Logger.getLogger(SelectorsLoader.class.getName());

    private SelectorsLoader() {
    }
    
    private static List<SelectorItem> result = new ArrayList<SelectorItem>();
    
    public static Collection<SelectorItem> getSelectors(File file) {
        result.clear();
        try {
            long start = System.currentTimeMillis();
            SAXParserFactory factory = SAXParserFactory.newInstance();
            SAXParser parser = factory.newSAXParser();
            DefaultHandler handler = new SelectorsLoader();
            parser.parse(file, handler);
            long end = System.currentTimeMillis();
            LOGGER.log(Level.FINE, "Loading selectors from API file took {0}ms ",  (end - start)); //NOI18N
         
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        } catch (ParserConfigurationException ex) {
            Exceptions.printStackTrace(ex);
        } catch (SAXException ex) {
            Exceptions.printStackTrace(ex);
        }
        return result;
    }
    
    public static String getDocumentation(File file, String selectorName) {
        DocumentationBuilder documentationBuilder = new DocumentationBuilder(file);
        return documentationBuilder.buildForSelector(selectorName);
    }
    
    public static String getMethodDocumentation(File file, String name) {
        DocumentationBuilder documentationBuilder = new DocumentationBuilder(file);
        return documentationBuilder.buildForMethod(name);
    }

    public static void addToModel(File apiFile, ModelElementFactory elementFactory, JsObject jQuery) {
        JQueryModelBuilder propertiesBuilder = new JQueryModelBuilder(apiFile, jQuery, elementFactory);
        propertiesBuilder.addProperties(jQuery);
    }
    
    private boolean inSelector = false;
    
    private enum Tag {
        added, argument, desc, entry, longdesc, note, sample, signature, notinterested;
    }
    private String name;
    private String sample;
    
    private Tag inTag = Tag.notinterested;
    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        if (inSelector){
          if (qName.equals(Tag.sample.name())){
              inTag = Tag.sample;
          }  
        } else if(qName.equals(Tag.entry.name())) {
            String type = attributes.getValue(TYPE); //NOI18N
            if (type.equals(SELECTOR)) {  //NOI18N
                inSelector = true;
                name = attributes.getValue(NAME); //NOI18N
            }
        }
        
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        if(inSelector && qName.equals(Tag.entry.name())) {
            inSelector = false;
            String template = null;
            if(sample.indexOf('(') > -1) {          //NOI18N
                template = name + "(${cursor})";    //NOI18N
                name = name + "()";                 //NOI18N
            }
            SelectorItem item = new SelectorItem(name, template);
            result.add(item);
        }
    }

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        if (inTag == Tag.sample) {
            sample = new String(ch, start, length);
            inTag = Tag.notinterested;
        }
    }

    /**
     * Copied from org.netbeans.modules.subversion.remote.ui.history.RevisionNode
     * @param c
     * @return 
     */
    private static String getColorString(Color c) {
	return "#" + getHex(c.getRed()) + getHex(c.getGreen()) + getHex(c.getBlue()); // NOI18N
    }

    /**
     * Copied from org.netbeans.modules.subversion.remote.ui.history.RevisionNode
     * @param c
     * @return 
     */
    private static String getHex(int i) {
	String hex = Integer.toHexString(i & 0x000000FF);
	if (hex.length() == 1) {
	    hex = "0" + hex; // NOI18N
	}
	return hex;
    }

    private static class Signature {
        List<Argument> arguments;
        String fromVersion;

        public Signature() {
            this.arguments = new ArrayList<Argument>();
            this.fromVersion = null;
        }
        
    }
    
    private static class Argument {
        final String name;
        final String description;

        public Argument(String name, String type, String description) {
            this.name = name;
            this.description = description;
        }    
    }
    
    private static class DocumentationBuilder extends DefaultHandler {

        private static final String TABLE_STYLE= "style=\"border: 0px; width: 100%;\""; //NOI18N
        private static final String TD_STYLE = "style=\"text-aling:left; border-width: 0px;padding: 1px;padding:3px;\" ";  //NOI18N
        private static final String TD_STYLE_MAX_WIDTH = "style=\"text-aling:left; border-width: 0px;padding: 1px;padding:3px;width:80%;\" ";  //NOI18N
        
        private StringBuilder documentation;
        private boolean inTag;
        private String interestedInTypeTag;
        private String elementName;
        private String elementNameWithPrefix;
        private File file;
        private List<Tag> tagPath;
        
        private String sample;
        private String returns;
        private String description;
        private List<String> notes;
        private String longDescription;
        private String argName;
        private String argType;
        private List<Signature> signatures;
        
        public DocumentationBuilder(File file) {
            inTag = false;
            this.file = file;
            tagPath = new ArrayList<Tag>();
        }
        
        public String buildForMethod(String name) {
            documentation = new StringBuilder();
            try {
                long start = System.currentTimeMillis();
                elementName = name;
                elementNameWithPrefix = JQueryUtils.JQUERY + "." + name; //NOI18N
                interestedInTypeTag = METHOD;
                SAXParserFactory factory = SAXParserFactory.newInstance();
                SAXParser parser = factory.newSAXParser();
                parser.parse(file, this);
                long end = System.currentTimeMillis();
                LOGGER.log(Level.FINE, "Loading selectors from API file took {0}ms ", (end - start)); //NOI18N

            } catch (IOException ex) {
                Exceptions.printStackTrace(ex);
            } catch (ParserConfigurationException ex) {
                Exceptions.printStackTrace(ex);
            } catch (SAXException ex) {
                Exceptions.printStackTrace(ex);
            }
            return documentation.toString();
        }
        
        public String buildForSelector(String name) {
            documentation = new StringBuilder();
            try {
                long start = System.currentTimeMillis();
                elementName = name;
                interestedInTypeTag = SELECTOR;
                SAXParserFactory factory = SAXParserFactory.newInstance();
                SAXParser parser = factory.newSAXParser();
                parser.parse(file, this);
                long end = System.currentTimeMillis();
                LOGGER.log(Level.FINE, "Loading selectors from API file took {0}ms ", (end - start)); //NOI18N

            } catch (IOException ex) {
                Exceptions.printStackTrace(ex);
            } catch (ParserConfigurationException ex) {
                Exceptions.printStackTrace(ex);
            } catch (SAXException ex) {
                Exceptions.printStackTrace(ex);
            }
            return documentation.toString();
        }
        
        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
            if (inTag) {

                Tag current;
                try {
                    current = Tag.valueOf(qName);
                } catch (IllegalArgumentException iae) {
                    current = Tag.notinterested;
                }
                tagPath.add(0, current);
                switch (current) {
                    case argument:
                        argName = attributes.getValue(NAME);
                        argType = attributes.getValue(TYPE);
                        break;
                    case signature:
                        signatures.add(new Signature());
                        break;
                    default:
                        break;
                }
            } else if (qName.equals(Tag.entry.name())) {
                String type = attributes.getValue(TYPE);
                if (type.equals(interestedInTypeTag)) {
                    String name = attributes.getValue(NAME);
                    if (name.equals(elementName) || name.equals(elementNameWithPrefix)) {
                        tagPath.add(Tag.entry);
                        returns = attributes.getValue(RETURN);
                        inTag = true;
                        description = "";
                        longDescription = "";
                        sample = "";
                        longDescription = "";
                        notes = new ArrayList<String>();
                        signatures = new ArrayList<Signature>();
                    }
                }
            }

        }

        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {
            if (inTag) {
                if (tagPath.size() > 0) {
                    Tag removed = tagPath.remove(0);
                    switch (removed) {
                        case entry:
                            inTag = false;
                            if (METHOD.equals(interestedInTypeTag)) {
                                createMethodHtmlDoc();
                            } else {
                                createSelectorHtmlDoc();
                            }
                            break;
                        case signature:
                    }
                }
                if (tagPath.isEmpty() && qName.equals(Tag.entry.name())) {
                    
                }
            }
        }

        @Override
        public void characters(char[] ch, int start, int length) throws SAXException {
            if (inTag && tagPath.size() > 0) {
                switch (tagPath.get(0)) {
                    case added:
                        signatures.get(signatures.size() - 1).fromVersion = new String(ch, start, length);
                        break;
                    case desc:
                        switch (tagPath.get(1)) {
                            case entry:
                                description = new String(ch, start, length);
                                break;
                            case argument:
                                signatures.get(signatures.size() - 1).arguments.add(new Argument(argName, argType, new String(ch, start, length)));
                                break;
                            default:
                                break;
                        }
                        break;
                    case longdesc:
                        longDescription = new String(ch, start, length);
                        break;
                    case note:
                        notes.add(new String(ch, start, length));
                        break;
                    case sample:
                        sample = new String(ch, start, length);
                        break;
                    default:
                        break;
                }
            }
        }
        
        private void createMethodHtmlDoc() {
            htmlDocStart();
            htmlDocMethodTitle();
            htmlDocDescription();
            htmlSignatures();
            htmlDocLongDescription();
            htmlDocEnd();
        }
        
        private void createSelectorHtmlDoc() {
            htmlDocStart();
            htmlDocSelectorTitle();
            htmlDocDescription();
            htmlDocSelectorSignatures();
            htmlDocLongDescription();
            htmlDocNotes();
            htmlDocEnd();
        }
        
        private String createSignatureText(Signature signature, boolean link) {
            StringBuilder sb = new StringBuilder();
            sb.append('.'); // NOI18N
            if(link) {
                sb.append(String.format("<a style='color: %s' href='http://api.jquery.com/%s/'>%s</a>", getColorString(LINK_COLOR), elementName, elementName)); // NOI18N
            } else {
                sb.append(elementName);
            }
            sb.append("( ");                                                    // NOI18N
            if(signature.arguments.isEmpty()) {
                sb.append(")");                                                 // NOI18N
            } else {
                boolean addComma = false;
                for(Argument argument : signature.arguments) {
                    if(addComma) {
                        sb.append(", ");                                        // NOI18N
                    } else {
                        addComma = true;
                    }
                    sb.append(argument.name);
                }
                sb.append(" )");                                                // NOI18N
            }
            return sb.toString();
        }
        
        private String addStyles(String text) {
            String result = text.replace("<pre>", "<pre style='margin: 1em 0px; display: block; font-family: monospace;white-space: pre;background-color: " + getColorString(PRE_BACKGROUNDCOLOR) + "; padding:6px;'>"); // NOI18N
            return result;
        }

        private void htmlDocStart() {
            documentation.append("<html>\n");                                   // NOI18N
            documentation.append("<head>\n");                                   // NOI18N
            documentation.append("</head>\n");                                  // NOI18N
            documentation.append("<body style='font-family: Arial; font-size: 11px'>\n"); // NOI18N      
        }

        private void htmlDocEnd() {
            documentation.append("</body>\n");                                  // NOI18N
            documentation.append("</html>\n");                                  // NOI18N
        }
        private void htmlDocMethodTitle(){
	    documentation.append("<table style='width:100%; background-color:")
		    .append(getColorString(TITLE_BACKGROUNDCOLOR)).append("; color:")
		    .append(getColorString(TITLE_COLOR))
		    .append("; margin:0px; padding:3px;font-weight: bold; font-size: 12px'><tr>\n"); // NOI18N
            String firstSignature;
            if(signatures.isEmpty()) {
                firstSignature = String.format(".<a style='color: %s' href='http://api.jquery.com/%s/'>%s</a>( )", getColorString(LINK_COLOR), elementName, elementName); // NOI18N
            } else {
                firstSignature = createSignatureText(signatures.get(0), true);
            }
            documentation.append("<td>").append(firstSignature).append("</td>\n"); // NOI18N
            documentation.append("<td style='text-align: right;font-style: italic;'>").append(String.format("Returns: <a style='color:%s' href='http://api.jquery.com/Types/#'%s> %s</a>", getColorString(LINK_COLOR), returns, returns)).append("</td>\n"); // NOI18N
            documentation.append("</tr></table>");                              // NOI18N
        }
        
        private void htmlDocSelectorTitle(){
	    documentation.append("<table style='width:100%; background-color:")
		    .append(getColorString(TITLE_BACKGROUNDCOLOR)).append("; color:")
		    .append(getColorString(TITLE_COLOR))
		    .append("; margin:0px; padding:3px;font-weight: bold; font-size: 12px'><tr>\n");// NOI18N
            String title= String.format("<a style='color: %s' href='http://api.jquery.com/%s-selector/'>%s</a> selector", getColorString(LINK_COLOR), elementName, elementName); // NOI18N
            documentation.append("<td>").append(title).append("</td>\n");       // NOI18N
            documentation.append("</tr></table>");                              // NOI18N
        }
             
        private void htmlDocDescription() {
            if (description != null && !description.isEmpty()) {
                documentation.append("<div style='margin: 6px'>\n");            // NOI18N
                documentation.append(description);
                documentation.append("</div> \n");                              // NOI18N
            }
        }
        
        private void htmlDocLongDescription() {
            if (longDescription != null && !longDescription.isEmpty()) {
                documentation.append("<div style='margin: 6px'>\n");            // NOI18N
                documentation.append(addStyles(longDescription));
                documentation.append("</div> \n");                              // NOI18N
            }
        }
        
        private void htmlDocNotes() {
            if (!notes.isEmpty()) {
                documentation.append("<p style='font-size: 12px'>\n");          // NOI18N
                documentation.append("<span style='font-weight: bold'>Additional Notes: </span>\n");// NOI18N
                documentation.append("<ul>\n");                                 // NOI18N
                for (String note : notes) {
                    documentation.append("<li>").append(note).append("</li>\n");    // NOI18N
                }
                documentation.append("</ul>\n");                                // NOI18N
                documentation.append("</p> \n");                                // NOI18N
            }
        }
        
        private void htmlSignatures() {    
            if (!signatures.isEmpty()) {
                documentation.append("<table align='right' style='width:95%; background-color:").append(getColorString(TABLE_BACKGROUNDCOLOR)).append("; padding:12px;margin-right:6px;'><tr><td>"); // NOI18N
                for(Signature signature : signatures) {
                    documentation.append("<table width='100%' style='font-weight: bold;font-size: small; color:").append(getColorString(SIGNATURE_COLOR)).append("'><tr>\n"); // NOI18N
                    documentation.append("<td>");                               // NOI18N
                    documentation.append(createSignatureText(signature, false));
                    documentation.append("</td><td style='vertical-align: bottom; text-align: right;'>"); // NOI18N
                    documentation.append(String.format("version added: <a style='color: %s' href='http://api.jquery.com/category/version/%s/'>%s</a>", getColorString(SIGNATURE_VERSION), signature.fromVersion, signature.fromVersion));// NOI18N
                    documentation.append("</td>");                              // NOI18N    
                    documentation.append("</tr></table>\n");                    // NOI18N
                    documentation.append("<hr style='width: 100%; height: 2px'/>"); // NOI18N
                    for(Argument argument: signature.arguments) {
                        documentation.append("<p style='font-size: small; margin-bottom:6px'>");// NOI18N
                        documentation.append("<b>").append(argument.name).append("</b>: "); // NOI18N
                        documentation.append(argument.description);
                        documentation.append("</p>");                           // NOI18N
                    }
                }
                documentation.append("</td></tr></table>");                     // NOI18N
            }
        }
        
        private void htmlDocSelectorSignatures() {    
            if (!signatures.isEmpty()) {
                documentation.append("<table align='right' style='width:95%; background-color:").append(getColorString(TABLE_BACKGROUNDCOLOR)).append("; padding:12px;margin-right:6px;'><tr><td>"); // NOI18N
                for(Signature signature : signatures) {
                    documentation.append("<table width='100%' style='font-weight: bold;font-size: small; color:").append(getColorString(SIGNATURE_COLOR)).append("'><tr>\n"); // NOI18N
                    documentation.append("<td>");                               // NOI18N
                    documentation.append(String.format("jQuery('%s')", sample));// NOI18N
                    documentation.append("</td><td style='vertical-align: bottom; text-align: right;'>");   // NOI18N
                    documentation.append(String.format("version added: <a style='color: %s' href='http://api.jquery.com/category/version/%s/'>%s</a>", getColorString(SIGNATURE_VERSION), signature.fromVersion, signature.fromVersion));// NOI18N
                    documentation.append("</td>");                              // NOI18N
                    documentation.append("</tr></table>\n");                    // NOI18N
                    documentation.append("<hr style='width: 100%; height: 2px'/>"); // NOI18N
                    for(Argument argument: signature.arguments) {
                        documentation.append("<p style='font-size: small; margin-bottom:6px'>"); // NOI18N
                        documentation.append("<b>").append(argument.name).append("</b>: "); // NOI18N
                        documentation.append(argument.description);
                        documentation.append("</p>");                           // NOI18N
                    }
                }
                documentation.append("</td></tr></table>");                     // NOI18N
            }
        }
        
    }
    
    private static class JQueryModelBuilder extends DefaultHandler {
        private final File file;
        private final JsObject jQuery;
        private final ModelElementFactory elementFactory;
        private final List<Tag> tagPath;
        
        
        boolean isMethod;
        boolean isProperty;
        private String name;
        private String returns;
        private String added;
        // Todo the parametrs has to be js objects to assign type
        private final List<String> params;
        
        public JQueryModelBuilder(final File file, final JsObject jQuery, ModelElementFactory elementFactory) {
            this.file = file;
            this.jQuery = jQuery;
            this.elementFactory = elementFactory;
            this.tagPath = new ArrayList<Tag>();
            this.params  = new ArrayList<String>();
        }

        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
            Tag current;
            try {
                current = Tag.valueOf(qName);
            } catch (IllegalArgumentException iae) {
                current = Tag.notinterested;
            }
            tagPath.add(0, current);

            switch (current) {
                case entry:
                    String type = attributes.getValue(TYPE);
                    if (type.equals(METHOD)) {
                        isMethod = true;
                    } else if (type.equals("property")) { // NOI18N
                        isProperty = true;
                    }
                    if (isMethod || isProperty) {
                        name = attributes.getValue(NAME);
                        if (name.startsWith(JQueryUtils.JQUERY + ".")) {
                            name = name.substring(7);
                        }
                        returns = attributes.getValue(RETURN);
                    }
                    break;
                case argument: 
                    if (isMethod) {
                        String paramName = attributes.getValue(NAME);
                        params.add(paramName);
                    }
                    break;
                default:
                    break;
            }    
        }
        
        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {
            if (tagPath.size() > 0) {
                Tag current = tagPath.remove(0);
                if (isMethod){
                    switch (current) {
                        case signature:
                            if (name.indexOf('.') == -1) {
                                JsFunction function = elementFactory.newFunction((DeclarationScope) jQuery, jQuery, name, params);
                                function.addReturnType(elementFactory.newType(returns, -1, true));
                                jQuery.addProperty(name + "#" + added, function);
                                params.clear();
                            }
                            break;
                        case entry:
                            isMethod = false;
                            params.clear();
                            break;
                        default:
                            break;
                    }
                }
            }
        }

        @Override
        public void characters(char[] ch, int start, int length) throws SAXException {
            if (tagPath.get(0) == Tag.added) {
                if (tagPath.size() > 1 && tagPath.get(1) == Tag.signature) {
                    added = new String(ch, start, length);
                }
            }
        }
        
        
        private void addProperties(JsObject global) {
            try {
                long start = System.currentTimeMillis();
                SAXParserFactory factory = SAXParserFactory.newInstance();
                SAXParser parser = factory.newSAXParser();
                parser.parse(file, this);
                long end = System.currentTimeMillis();
                LOGGER.log(Level.FINE, "Collecting properties from jQuery API file took {0}ms ", (end - start)); //NOI18N

            } catch (IOException ex) {
                Exceptions.printStackTrace(ex);
            } catch (ParserConfigurationException ex) {
                Exceptions.printStackTrace(ex);
            } catch (SAXException ex) {
                Exceptions.printStackTrace(ex);
            }
        }
    }

}
