/*
 * 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.rest.config;

import static com.google.common.base.Optional.fromNullable;
import static com.google.common.collect.Maps.transformValues;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.jclouds.Constants.PROPERTY_TIMEOUTS_PREFIX;
import static org.jclouds.util.Maps2.transformKeys;
import static org.jclouds.util.Predicates2.startsWith;

import java.util.Map;
import java.util.regex.Pattern;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;

import org.jclouds.reflect.Invocation;
import org.jclouds.rest.annotations.Fallback;

import com.google.common.annotations.Beta;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.reflect.Invokable;
import com.google.inject.Injector;

@Beta
@Singleton
public class ReadAnnotationsAndProperties implements InvocationConfig {
   private final Injector injector;
   private final org.jclouds.Fallback<Object> defaultFallback;
   private final Map<String, Long> timeouts;

   @Inject
   ReadAnnotationsAndProperties(Injector injector,
         Function<Predicate<String>, Map<String, String>> filterStringsBoundByName,
         org.jclouds.Fallback<Object> defaultFallback) {
      this.injector = injector;
      this.defaultFallback = defaultFallback;
      this.timeouts = timeouts(filterStringsBoundByName);
   }

   @Override
   public Optional<Long> getTimeoutNanos(Invocation in) {
      String commandName = getCommandName(in);
      Optional<Long> defaultMillis = fromNullable(timeouts.get("default"));
      Optional<Long> timeoutMillis = fromNullable(timeouts.get(commandName));
      Invokable<?, ?> invoked = in.getInvokable();
      if (invoked.isAnnotationPresent(Named.class)) {
         timeoutMillis = timeoutMillis.or(defaultMillis);
      } else {
         // TODO: remove old logic once Named annotations are on all methods
         String className = invoked.getOwnerType().getRawType().getSimpleName();
         timeoutMillis = timeoutMillis.or(fromNullable(timeouts.get(className))).or(defaultMillis);
      }
      if (timeoutMillis.isPresent())
         return Optional.of(MILLISECONDS.toNanos(timeoutMillis.get()));
      return Optional.absent();
   }

   @Override
   public String getCommandName(Invocation invocation) {
      Invokable<?, ?> invoked = invocation.getInvokable();
      if (invoked.isAnnotationPresent(Named.class)) {
         return invoked.getAnnotation(Named.class).value();
      } else {
         // TODO: remove old logic once Named annotations are on all methods
         String className = invoked.getOwnerType().getRawType().getSimpleName();
         return className + "." + invoked.getName();
      }
   }

   @Override
   public org.jclouds.Fallback<?> getFallback(Invocation invocation) {
      Fallback fallback = invocation.getInvokable().getAnnotation(Fallback.class);
      if (fallback != null) {
         return injector.getInstance(fallback.value());
      }
      return defaultFallback;
   }

   /**
    * override timeout by values configured in properties(in ms)
    */
   static Map<String, Long> timeouts(Function<Predicate<String>, Map<String, String>> filterStringsBoundByName) {
      Map<String, String> stringBoundWithTimeoutPrefix = filterStringsBoundByName
            .apply(startsWith(PROPERTY_TIMEOUTS_PREFIX));
      Map<String, Long> longsByName = transformValues(stringBoundWithTimeoutPrefix, new Function<String, Long>() {
         public Long apply(String input) {
            return Long.valueOf(String.valueOf(input));
         }
      });
      return transformKeys(longsByName, new Function<String, String>() {
         public String apply(String input) {
            return input.replaceFirst(Pattern.quote(PROPERTY_TIMEOUTS_PREFIX), "");
         }
      });
   }
}
