/*
 * 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;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.emptyToNull;
import static com.google.common.collect.Multimaps.forMap;
import static org.jclouds.http.utils.Queries.buildQueryLine;
import static org.jclouds.http.utils.Queries.queryParser;
import static org.jclouds.util.Strings2.urlDecode;
import static org.jclouds.util.Strings2.urlEncode;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Map;

import org.jclouds.http.utils.QueryValue;
import org.jclouds.javax.annotation.Nullable;

import com.google.common.annotations.Beta;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;

/**
 * Functions on {@code String}s and {@link URI}s. Strings can be level 1 <a
 * href="http://tools.ietf.org/html/rfc6570">RFC6570</a> form.
 *
 * ex.
 *
 * <pre>
 *  https://api.github.com/repos/{user}
 * </pre>
 *
 * <h4>Reminder</h4>
 *
 * Unresolved <a href="http://tools.ietf.org/html/rfc6570">RFC6570</a> templates are not supported by
 * {@link URI#create(String)} and result in an {@link IllegalArgumentException}.
 *
 * <h4>Limitations</h4>
 *
 * In order to reduce complexity not needed in jclouds, this doesn't support {@link URI#getUserInfo()},
 * {@link URI#getFragment()}, or {@code matrix} params. Matrix params can be achieved via adding {@code ;} refs in the
 * http path directly. Moreover, since jclouds only uses level 1 templates, this doesn't support the additional forms
 * noted in the RFC.
 *
 * @since 1.6
 */
@Beta
public final class Uris {

   /**
    * @param template
    *           URI string that can be in level 1 <a href="http://tools.ietf.org/html/rfc6570">RFC6570</a> form.
    */
   public static UriBuilder uriBuilder(CharSequence template) {
      return new UriBuilder(template);
   }

   /**
    * @param in
    *           uri
    */
   public static UriBuilder uriBuilder(URI uri) {
      return new UriBuilder(uri);
   }

   /**
    * Mutable URI builder that can be in level 1 <a href="http://tools.ietf.org/html/rfc6570">RFC6570</a> template form.
    *
    * ex.
    *
    * <pre>
    *  https://api.github.com/repos/{user}
    * </pre>
    *
    */
   public static final class UriBuilder {
      private static final TransformObjectToQueryValue QUERY_VALUE_TRANSFORMER = new TransformObjectToQueryValue();

      // colon for urns, semicolon & equals for matrix params
      private Iterable<Character> skipPathEncoding = Lists.charactersOf("/:;=");
      private String scheme;
      private String host;
      private Integer port;
      private String path;
      private Multimap<String, Object> query = LinkedHashMultimap.create();

      /**
       * override default of {@code / : ; =}
       * @param scheme
       *           scheme to set or replace
       */
      public UriBuilder skipPathEncoding(Iterable<Character> skipPathEncoding) {
         this.skipPathEncoding = ImmutableSet.copyOf(checkNotNull(skipPathEncoding, "skipPathEncoding"));
         return this;
      }

      /**
       * @param scheme
       *           scheme to set or replace
       */
      public UriBuilder scheme(String scheme) {
         this.scheme = checkNotNull(scheme, "scheme");
         return this;
      }

      /**
       * @param host
       *           host to set or replace
       * @return replaced value
       */
      public UriBuilder host(String host) {
         this.host = checkNotNull(host, "host");
         return this;
      }

      public UriBuilder path(@Nullable String path) {
         path = emptyToNull(path);
         if (path == null)
            this.path = null;
         else
            this.path = prefixIfNeeded(urlDecode(path));
         return this;
      }

      public UriBuilder appendPath(String path) {
         if (this.path == null) {
            path(path);
         } else {
            path(slash(this.path, path));
         }
         return this;
      }

      public UriBuilder query(@Nullable String queryLine) {
         if (query == null)
            return clearQuery();
         return query(queryParser().apply(queryLine));
      }

      public UriBuilder clearQuery() {
         query.clear();
         return this;
      }

      public UriBuilder query(Multimap<String, ?> parameters) {
         Multimap<String, QueryValue> queryValueMultimap = Multimaps.transformValues(
               checkNotNull(parameters, "parameters"), QUERY_VALUE_TRANSFORMER);
         query.clear();
         query.putAll(queryValueMultimap);
         return this;
      }

      public UriBuilder addQuery(String name, Iterable<?> values) {
         query.putAll(checkNotNull(name, "name"), Iterables.transform(checkNotNull(values, "values of %s", name),
               QUERY_VALUE_TRANSFORMER));
         return this;
      }

      public UriBuilder addQuery(String name, String... values) {
         return addQuery(name, Arrays.asList(checkNotNull(values, "values of %s", name)));
      }

      public UriBuilder addQuery(Multimap<String, ?> parameters) {
         Multimap<String, QueryValue> queryValueMultimap = Multimaps.transformValues(
               checkNotNull(parameters, "parameters"), QUERY_VALUE_TRANSFORMER);
         query.putAll(queryValueMultimap);
         return this;
      }

      public UriBuilder replaceQuery(String name, Iterable<?> values) {
         Iterable<QueryValue> queryValues = Iterables.transform(checkNotNull(values, "values of %s", name),
               QUERY_VALUE_TRANSFORMER);
         query.replaceValues(checkNotNull(name, "name"), queryValues);
         return this;
      }

      public UriBuilder replaceQuery(String name, String... values) {
         return replaceQuery(name, Arrays.asList(checkNotNull(values, "values of %s", name)));
      }

      public UriBuilder replaceQuery(Map<String, ?> parameters) {
         return replaceQuery(forMap(parameters));
      }

      public UriBuilder replaceQuery(Multimap<String, ?> parameters) {
         for (String key : checkNotNull(parameters, "parameters").keySet())
            replaceQuery(key, parameters.get(key));
         return this;
      }

      /**
       * <a href="http://tools.ietf.org/html/rfc6570">RFC6570</a> templates have variables defined in curly braces.
       * Curly brace characters are unparsable via {@link URI#create} and result in an {@link IllegalArgumentException}.
       *
       * This implementation temporarily replaces curly braces with double parenthesis so that it can reuse
       * {@link URI#create}.
       *
       * @param uri
       *           template which may have template parameters inside
       */
      private UriBuilder(CharSequence uri) {
         this(URI.create(escapeSpecialChars(checkNotNull(uri, "uri"))));
      }

      private static String escapeSpecialChars(CharSequence uri) {
         // skip encoding if there's no valid variables set. ex. {a} is the left valid
         if (uri.length() < 3)
            return uri.toString();

         // duplicates memory even if there are no special characters, however only requires a single scan.
         StringBuilder builder = new StringBuilder();
         for (char c : Lists.charactersOf(uri)) {
            switch (c) {
            case '{':
               builder.append("((");
               break;
            case '}':
               builder.append("))");
               break;
            default:
               builder.append(c);
            }
         }
         return builder.toString();
      }

      private static String unescapeSpecialChars(CharSequence uri) {
         if (uri.length() < 5) // skip encoding if there's no valid variables set. ex. ((a)) is the left valid
            return uri.toString();

         char last = uri.charAt(0);  // duplicates even if there are no special characters, but only requires 1 scan
         StringBuilder builder = new StringBuilder();
         for (char c : Lists.charactersOf(uri)) {
            switch (c) {
            case '(':
               if (last == '(') {
                  builder.setCharAt(builder.length() - 1, '{');
               } else {
                  builder.append('(');
               }
               break;
            case ')':
               if (last == ')') {
                  builder.setCharAt(builder.length() - 1, '}');
               } else {
                  builder.append(')');
               }
               break;
            default:
               builder.append(c);
            }
            last = c;
         }
         return builder.toString();
      }

      private UriBuilder(URI uri) {
         checkNotNull(uri, "uri");
         this.scheme = uri.getScheme();
         this.host = uri.getHost();
         this.port = uri.getPort() == -1 ? null : uri.getPort();
         if (uri.getRawPath() != null)
            // path decodes the string, so we need to get at the raw (encoded) string
            path(unescapeSpecialChars(uri.getRawPath()));
         if (uri.getRawQuery() != null)
            // The query parser decodes the strings that are passed to it; we should pass raw (encoded) queries
            query(queryParser().apply(unescapeSpecialChars(uri.getRawQuery())));
      }

      public URI build() {
         return build(ImmutableMap.<String, Object> of());
      }

      public URI build(Map<String, ?> variables, boolean encodePath) {
         try {
            return new URI(expand(variables, encodePath));
         } catch (URISyntaxException e) {
            throw new IllegalArgumentException(e);
         }
      }

      /**
       * @throws IllegalArgumentException
       *            if there's a problem parsing the URI
       */
      public URI build(Map<String, ?> variables) {
         try {
            return new URI(expand(variables, true));
         } catch (URISyntaxException e) {
            throw new IllegalArgumentException(e);
         }
      }

      private String expand(Map<String, ?> variables, boolean encodePath) {
         StringBuilder b = new StringBuilder();
         if (scheme != null)
            b.append(scheme).append("://");
         if (host != null)
            b.append(UriTemplates.expand(host, variables));
         if (port != null)
            b.append(':').append(port);
         if (path != null) {
            if (encodePath) {
               b.append(urlEncode(UriTemplates.expand(path, variables), skipPathEncoding));
            } else {
               b.append(UriTemplates.expand(path, variables));
            }
         }
         if (!query.isEmpty()) {
            b.append('?').append(buildQueryLine(query));
         }
         return b.toString();
      }

      /**
       * returns template expression without url encoding
       */
      @Override
      public String toString() {
         StringBuilder b = new StringBuilder();
         if (scheme != null)
            b.append(scheme).append("://");
         if (host != null)
            b.append(host);
         if (port != null)
            b.append(':').append(port);
         if (path != null)
            b.append(path);
         if (!query.isEmpty())
            b.append('?').append(buildQueryLine(query));
         return b.toString();
      }
   }

   private static String slash(CharSequence left, CharSequence right) {
      return delimit(left, right, '/');
   }

   private static String delimit(CharSequence left, CharSequence right, char token) {
      if (left.length() == 0)
         return right.toString();
      if (right.length() == 0)
         return left.toString();
      StringBuilder builder = new StringBuilder(left);
      if (lastChar(left) == token) {
         if (firstChar(right) == token) // left/ + /right
            return builder.append(right.subSequence(1, right.length())).toString();
         return builder.append(right).toString(); // left/ + right
      } else if (firstChar(right) == token) {
         return builder.append(right).toString(); // left + /right
      } // left + / + right
      return new StringBuilder(left).append(token).append(right).toString();
   }

   public static boolean lastCharIsToken(CharSequence left, char token) {
      return lastChar(left) == token;
   }

   public static char lastChar(CharSequence in) {
      return in.charAt(in.length() - 1);
   }

   public static char firstChar(CharSequence in) {
      return in.charAt(0);
   }

   public static boolean isToken(CharSequence right, char token) {
      return right.length() == 1 && right.charAt(0) == token;
   }

   private static String prefixIfNeeded(String in) {
      if (in != null && in.charAt(0) != '/')
         return new StringBuilder().append('/').append(in).toString();
      return in;
   }

   private static class TransformObjectToQueryValue implements Function<Object, QueryValue> {
      @Override
      public QueryValue apply(Object o) {
         if (o == null) {
            return null;
         }
         if (o instanceof QueryValue) {
            return (QueryValue) o;
         }
         return new QueryValue(o.toString(), false);
      }
   }
}
