/*
 * 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.jclouds.http.utils;

import static org.jclouds.util.Strings2.urlDecode;
import static org.jclouds.util.Strings2.urlEncode;

import java.util.Map.Entry;

import com.google.common.base.Function;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.Multimap;

public class Queries {

   private static final Function<String, Multimap<String, String>> parseQueryToDecodedMap = new Function<String, Multimap<String, String>>() {
      @Override
      public Multimap<String, String> apply(String in) {
         // some query values are null, which aren't permitted by Immutable*
         Multimap<String, String> map = LinkedListMultimap.create();
         if (in == null) {
         } else if (in.indexOf('&') == -1) {
            if (in.indexOf('=') != -1)
               parseKeyValueFromStringToDecodedMap(in, map);
            else
               map.put(in, null);
         } else {
            for (String part : Splitter.on('&').split(in)) {
               parseKeyValueFromStringToDecodedMap(part, map);
            }
         }
         return map;
      }
   };

   public static Function<String, Multimap<String, String>> queryParser() {
      return parseQueryToDecodedMap;
   }

   private static void parseKeyValueFromStringToDecodedMap(String stringToParse, Multimap<String, String> map) {
      // note that '=' can be a valid part of the value
      int indexOfFirstEquals = stringToParse.indexOf('=');
      String key = indexOfFirstEquals == -1 ? stringToParse : stringToParse.substring(0, indexOfFirstEquals);
      String value = indexOfFirstEquals == -1 ? null : stringToParse.substring(indexOfFirstEquals + 1);
      map.put(urlDecode(key), urlDecode(value));
   }

   /**
    * percent encodes the query parameters, excep {@code /} and {@code ,} characters.
    * 
    * @param queryParams
    * @return percent encoded line or null if no queryParams present
    */
   public static String encodeQueryLine(Multimap<String, ?> queryParams) {
      if (queryParams.isEmpty())
         return null;
      return buildQueryLine(queryParams, new EncodeAndAppendParam());
   }
   
   /**
    * percent encodes the query parameters according except characters specified in the {@code skips} argument.
    * 
    * @param queryParams
    * @return percent encoded line or null if no queryParams present
    */
   public static String encodeQueryLine(Multimap<String, ?> queryParams, Iterable<Character> skips) {
      if (queryParams.isEmpty())
         return null;
      return buildQueryLine(queryParams, new EncodeAndAppendParam(skips));
   }

   public static String buildQueryLine(Multimap<String, ?> queryParams) {
      if (queryParams.isEmpty())
         return null;
      return buildQueryLine(queryParams, new AppendParam());
   }

   private static String buildQueryLine(Multimap<String, ?> queryParams, AppendParam appendParam) {
      StringBuilder queryBuilder = appendParam.b;
      for (Entry<String, ?> pair : queryParams.entries()) {
         queryBuilder.append('&');
         appendParam.appendKey(pair.getKey());
         if (pair.getValue() != null)
            queryBuilder.append('=');
         if (pair.getValue() != null && !pair.getValue().equals("")) {
            appendParam.appendValue(pair.getValue());
         }
      }
      queryBuilder.deleteCharAt(0);
      return queryBuilder.toString();
   }

   private static class AppendParam {
      final StringBuilder b;

      private AppendParam() {
         this.b = new StringBuilder();
      }

      private void appendKey(String key) {
         append(key);
      }

      private void appendValue(Object val) {
         append(val);
      }

      void append(Object in) {
         b.append(in.toString());
      }
   }

   private static class EncodeAndAppendParam extends AppendParam {
      private Iterable<Character> skips;

      private EncodeAndAppendParam() {
         this(ImmutableList.of('/', ','));
      }

      private EncodeAndAppendParam(Iterable<Character> skips) {
         this.skips = skips;
      }

      @Override
      void append(Object in) {
         super.append(urlEncode(in.toString(), skips));
      }
   }

}
