/*
 * RMarkdownChunkHeaderParser.java
 *
 * Copyright (C) 2009-16 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.common.r.knitr;

import java.util.HashMap;
import java.util.Map;

import org.rstudio.core.client.Mutable;
import org.rstudio.core.client.StringUtil;
import org.rstudio.core.client.TextCursor;
import org.rstudio.core.client.regex.Match;
import org.rstudio.core.client.regex.Pattern;

public class RMarkdownChunkHeaderParser
{
   public static Map<String, String> parse(String line)
   {
      Map<String, String> options = new HashMap<String, String>();
      parse(line, options);
      return options;
   }
   
   public static final void parse(String line, Map<String, String> options)
   {
      // set up state
      Mutable<String> key = new Mutable<String>();
      Consumer keyConsumer = new MutableConsumer(key);
      
      Mutable<String> val = new Mutable<String>();
      Consumer valConsumer = new MutableConsumer(val);
      
      line = trimBoundary(line);
      TextCursor cursor = new TextCursor(line);
      
      // force default R engine
      options.put("engine", ensureQuoted("r"));
      
      // consume engine
      if (!consumeEngine(cursor, options))
         return;
      
      // consume whitespace and commas
      if (!cursor.consumeUntilRegex("[^\\s,]"))
         return;
      
      // consume next token -- need to determine
      // whether this is a chunk option name or
      // a label soon after
      if (!consumeKey(cursor, keyConsumer))
         return;
      
      // consume until ',' or '='. if nothing is
      // found, this must have been a label
      if (!cursor.consumeUntilRegex("[,=]"))
      {
         options.put("label", ensureQuoted(key.get().trim()));
         return;
      }
      
      char ch = cursor.peek();
      if (ch == ',')
      {
         // found a comma -- this must have been a label
         options.put("label", ensureQuoted(key.get().trim()));
      }
      else
      {
         // found an '=' -- this was a key for a chunk option
         if (!cursor.consume('='))
            return;
         
         // eat whitespace
         if (!cursor.consumeUntilRegex("\\S"))
            return;
         
         // consume value
         if (!consumeValue(cursor, valConsumer))
            return;
         
         // set option
         options.put(key.get(), val.get());
         
         // move to next comma
         if (!cursor.fwdToCharacter(',', false))
            return;
      }
      
      while (cursor.peek() == ',')
      {
         // eat whitespace and commas
         if (!cursor.consumeUntilRegex("[^\\s,]"))
            return;
         
         // consume key
         if (!consumeKey(cursor, keyConsumer))
            return;
         
         // eat whitespace
         if (!cursor.consumeUntilRegex("\\S"))
            return;
         
         // check '='
         if (!cursor.consume('='))
            return;
         
         // eat whitespace
         if (!cursor.consumeUntilRegex("\\S"))
            return;
         
         // consume value
         if (!consumeValue(cursor, valConsumer))
            return;
         
         // update options
         options.put(
               StringUtil.stringValue(key.get().trim()),
               val.get().trim());
         
         // find next comma
         if (!cursor.consumeUntil(','))
            return;
      }
      
      return;
   }
   
   private static final String trimBoundary(String line)
   {
      return line
            .replaceAll("^\\s*```{\\s*", "")
            .replaceAll("\\s*}\\s*$", "")
            .trim();
   }
   
   private static final boolean consumeEngine(final TextCursor cursor,
                                              final Map<String, String> options)
   {
      Consumer consumer = new Consumer()
      {
         @Override
         public void consume(String value)
         {
            options.put("engine", ensureQuoted(value));
         }
      };
      
      if (consumeUntilRegex(cursor, "(?:$|[\\s,])", consumer))
         return true;
      
      return false;
   }
   
   private static final boolean consumeUntilRegex(TextCursor cursor,
                                                  String regex,
                                                  Consumer consumer)
   {
      Pattern pattern = Pattern.create(regex);
      Match match = pattern.match(cursor.getData(), cursor.getIndex());
      if (match == null)
         return false;
      
      int startIdx = cursor.getIndex();
      int endIdx = match.getIndex();
      if (consumer != null)
      {
         String value = cursor.getData().substring(startIdx, endIdx);
         consumer.consume(value);
      }

      cursor.setIndex(endIdx);
      return true;
   }
   
   private static final boolean consumeQuotedItem(TextCursor cursor, Consumer consumer)
   {
      if (!isQuote(cursor.peek()))
         return false;
      
      int startIndex = cursor.getIndex();
      if (cursor.fwdToMatchingCharacter())
      {
         int endIndex = cursor.getIndex() + 1;
         if (consumer != null)
         {
            String value = cursor.getData().substring(startIndex, endIndex);
            consumer.consume(value);
         }
         cursor.setIndex(endIndex);
         return true;
      }
      return false;
   }
   
   private static final boolean consumeKey(TextCursor cursor, Consumer consumer)
   {
      if (isQuote(cursor.peek()) && consumeQuotedItem(cursor, consumer))
         return true;
      
      return consumeUntilRegex(cursor, "(?:$|[^a-zA-Z0-9_])", consumer);
   }
   
   private static final boolean consumeValue(TextCursor cursor,
                                             Consumer consumer)
   {
      if (consumeQuotedItem(cursor, consumer))
         return true;
      
      int startIdx = cursor.getIndex();
      do
      {
         if (cursor.isLeftBracket() && cursor.fwdToMatchingCharacter())
            continue;
         
         if (cursor.peek() == ',')
            break;
      }
      while (cursor.moveToNextCharacter());
      
      int endIdx = cursor.getIndex();
      if (consumer != null)
      {
         String value = cursor.getData().substring(startIdx, endIdx);
         consumer.consume(value);
      }
      cursor.setIndex(endIdx);
      return true;
   }
   
   private static final boolean isQuote(char ch)
   {
      return ch == '\'' || ch == '"' || ch == '`';
   }
   
   private static final String ensureQuoted(String string)
   {
      String[] quotes = new String[] { "\"", "'", "`" };
      for (String quote : quotes)
         if (string.startsWith(quote) && string.endsWith(quote))
            return string;
      
      return "\"" + string.replaceAll("\"", "\\\\\"") + "\"";
   }
   
   private static interface Consumer
   {
      public void consume(String value);
   }
   
   private static class MutableConsumer implements Consumer
   {
      public MutableConsumer(Mutable<String> value)
      {
         value_ = value;
      }
      
      @Override
      public void consume(String value)
      {
         value_.set(value);
      }
      
      private final Mutable<String> value_;
   }
}

@@ -6101,16 +6101,17 @@
 zA-Z0-9_
+.
 %5D)%22, con

allow '.' in key name for chunk parser