/*
 * HelpInfo.java
 *
 * Copyright (C) 2009-12 by RStudio, Inc.
 *
 * Unless you have received this program directly from RStudio pursuant
 * to the terms of a commercial license agreement with RStudio, then
 * this program is licensed to you under the terms of version 3 of the
 * GNU Affero General Public License. This program is distributed WITHOUT
 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
 *
 */
package org.rstudio.studio.client.workbench.views.help.model ;

import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.dom.client.*;

import org.rstudio.core.client.dom.DomUtils;
import org.rstudio.core.client.dom.DomUtils.NodePredicate;

import java.util.ArrayList;
import java.util.HashMap;

public class HelpInfo extends JavaScriptObject
{
   protected HelpInfo()
   {

   }

   public final ParsedInfo parse(String defaultSignature)
   {
      HashMap<String, String> values = new HashMap<String, String>() ;
      HashMap<String, String> args = new HashMap<String, String>();
      HashMap<String, String> slots = new HashMap<String, String>();

      String html = getHTML() ;
      if (html != null)
      {
         DivElement div = Document.get().createDivElement() ;
         div.setInnerHTML(html) ;
         
         // disable all links
         NodeList<Element> anchors = div.getElementsByTagName("a") ;
         for (int i = 0; i < anchors.getLength(); i++)
         {
            Element anchor = anchors.getItem(i) ;
            Element parent = anchor.getParentElement() ;
            Node child = anchor.getFirstChild() ;
            while (child != null)
            {
               parent.insertBefore(child, anchor) ;
               child = child.getNextSibling() ;
            }
         }
         
         // parse all description lists
         NodeList<Element> descriptionLists = div.getElementsByTagName("dl");
         for (int i = 0; i < descriptionLists.getLength(); i++)
            parseDescriptionList(args, descriptionLists.getItem(i));
         
         // get all h2 and h3 headings
         NodeList<Element> h2headings = div.getElementsByTagName("h2") ;
         NodeList<Element> h3headings = div.getElementsByTagName("h3") ;
         ArrayList<Element> headings = new ArrayList<Element>();
         for (int i = 0; i < h2headings.getLength(); i++)
            headings.add(h2headings.getItem(i));
         for (int i = 0; i < h3headings.getLength(); i++)
            headings.add(h3headings.getItem(i));
         
         // the first h2 heading is the title -- handle that specially
         if (headings.size() > 0)
         {
            Element titleElement = headings.get(0);
            String title = titleElement.getInnerText();
            values.put("Title", title);
         }
         
         // iterate through them
         for (int i = 1; i < headings.size(); i++)
         {
            Element heading = headings.get(i) ;
            String name = heading.getInnerText() ;
            if (name.equals("Arguments"))
            {
               parseArguments(args, heading);
            }
            if (name.equals("Slots"))
            {
               parseDescriptionList(slots, heading);
            }
            StringBuffer value = new StringBuffer() ;
            Node sibling = heading.getNextSibling() ;
            while (sibling != null
                  && !sibling.getNodeName().toLowerCase().equals("h2")
                  && !sibling.getNodeName().toLowerCase().equals("h3"))
            {
               value.append(DomUtils.getHtml(sibling)) ;
               sibling = sibling.getNextSibling() ;
            }
            values.put(name, value.toString()) ;
         }
      }

      String signature = getSignature();
      if (signature == null)
         signature = defaultSignature;
      return new ParsedInfo(getPackageName(), signature, values, args, slots) ;
   }
   
   private void parseDescriptionList(HashMap<String, String> args,
                                     Element heading)
   {
      Element table = (Element) DomUtils.findNode(heading, true, true, new NodePredicate() {
         
         public boolean test(Node n)
         {
            if (n.getNodeType() != Node.ELEMENT_NODE)
               return false;
            
            Element el = (Element) n;
            
            return el.getTagName().toUpperCase().equals("DL");
         }
      });
      
      if (table == null)
      {
         assert false : "Unexpected slots format: no <dl> entry found";
         return ;
      }
      
      NodeList<Node> children = table.getChildNodes();
      int nChildren = children.getLength();
      for (int i = 0; i < nChildren; i++)
      {
         Element child = (Element) children.getItem(i);
         if (child.getNodeName().toUpperCase().equals("DT"))
         {
            String argName = child.getInnerText().replaceAll(":", "");
            Element nextChild = (Element) children.getItem(i + 1);
            if (nextChild.getNodeName().toUpperCase().equals("DD"))
            {
               String value = nextChild.getInnerHTML();
               args.put(argName, value);
            }
         }
      }
   }

   private void parseArguments(HashMap<String, String> args,
                               Element heading)
   {
      Element table = (Element) DomUtils.findNode(heading, true, true, 
                                                  new NodePredicate() {
         public boolean test(Node n)
         {
            if (n.getNodeType() != Node.ELEMENT_NODE)
               return false ;
            
            Element el = (Element) n ;
            
            return el.getTagName().toUpperCase().equals("TABLE")
               && "R argblock".equals(el.getAttribute("summary")) ;
         }
      });
      
      if (table == null)
      {
         assert false : "Unexpected help format, no argblock table found" ; 
         return;
      }

      TableElement t = (TableElement) table ;
      NodeList<TableRowElement> rows = t.getRows() ;
      for (int i = 0; i < rows.getLength(); i++)
      {
         TableRowElement row = rows.getItem(i) ;
         NodeList<TableCellElement> cells = row.getCells() ;
         
         TableCellElement argNameCell = cells.getItem(0) ;
         TableCellElement argValueCell = cells.getItem(1) ;
         
         String argNameText = argNameCell.getInnerText() ;
         String argValueHtml = argValueCell.getInnerHTML() ;
         
         // argNameCell may be multiple comma-delimited arguments;
         // split them up if necessary (duplicate the help across args)
         String[] argNameTextSplat = argNameText.split("\\s*,\\s*");
         for (int j = 0; j < argNameTextSplat.length; j++)
            args.put(argNameTextSplat[j], argValueHtml);
         
      }
   }

   private final native String getHTML() /*-{
      return this.html ? this.html[0] : null ;
   }-*/;

   private final native String getSignature() /*-{
      return this.signature ? this.signature[0] : null ;
   }-*/;
   
   private final native String getPackageName() /*-{
      return this.pkgname ? this.pkgname[0] : null ;
   }-*/;

   public static class ParsedInfo
   {
      private String pkgName ;
      private String signature ;
      private HashMap<String, String> values ;
      private HashMap<String, String> args ;
      private HashMap<String, String> slots ;

      public ParsedInfo(String pkgName, String signature, HashMap<String, String> values,
            HashMap<String, String> args, HashMap<String, String> slots)
      {
         super() ;
         this.pkgName = pkgName ;
         this.signature = signature ;
         this.values = values != null ? values : new HashMap<String, String>();
         this.args = args ;
         this.slots = slots ;
      }
      
      public String getPackageName()
      {
         return pkgName ;
      }
      
      public String getTitle()
      {
         return values.get("Title") ;
      }
      
      public String getFunctionSignature()
      {
         return signature;
      }

      public String getDescription()
      {
         return values.get("Description") ;
      }

      public String getUsage()
      {
         return values.get("Usage") ;
      }

      public String getDetails()
      {
         return values.get("Details") ;
      }

      /**
       * Returns null if no args section was present in the docs.
       */
      public HashMap<String, String> getArgs()
      {
         return args ;
      }
      
      public HashMap<String, String> getSlots()
      {
         return slots ;
      }

      public boolean hasInfo()
      {
         return signature != null
                || args != null
                || getDescription() != null;
      }
   }
}
