package org.odata4j.consumer;

import java.lang.reflect.Method;
import java.util.Arrays;

import org.odata4j.consumer.behaviors.OClientBehavior;
import org.odata4j.consumer.behaviors.OClientBehaviors;
import org.odata4j.core.Throwables;
import org.odata4j.format.FormatType;

/**
 * <code>ODataConsumer</code> is the client-side interface to an OData service.
 *
 * <p>Use {@link ODataConsumers#create(String)} or {@link ODataConsumers#newBuilder(String)} to connect to an existing OData service.</p>
 *
 * <p>If found, the Jersey implementation of {@link ODataConsumer} is used by default. Otherwise
 * the CXF implementation is loaded. If specified the {@code odata4j.consumerimpl} system property
 * overrules this default behavior. Its value can be either one of the predefined constants {@code
 * jersey} and {@code cxf} or any other concrete full class name, e.g. {@code
 * foo.bar.OtherConsumer}. By convention, {@code foo.bar.OtherConsumer} must implement a factory
 * method {@code public static ODataConsumer.Builder newBuilder(String serviceRootUri)}.</p>
 *
 * @see ODataConsumer
 */
public class ODataConsumers {

  public static final String CONSUMERIMPL_PROPERTY = "odata4j.consumerimpl";
  public static final String JERSEY_CONSUMERIMPL = "jersey";
  public static final String CXF_CONSUMERIMPL = "cxf";
  public static final String JERSEY_CONSUMER_CLASSNAME = "org.odata4j.examples.jersey.consumer.ODataJerseyConsumer";
  public static final String CXF_CONSUMER_CLASSNAME = "org.odata4j.examples.cxf.consumer.ODataCxfConsumer";

  private static ClassLoader classLoader;

  /**
   * Builder for {@link ODataConsumer} objects.
   */
  public static class Builder implements ODataConsumer.Builder {

    private ODataConsumer.Builder consumerBuilder;

    private Builder(String serviceRootUri) {
      if (classLoader == null)
        classLoader = getClass().getClassLoader();

      try {
        String[] classNames = getClassNames();
        Class<?> consumerClass = getConsumerClass(classNames);
        Method newBuilderMethod = consumerClass.getDeclaredMethod("newBuilder", String.class);
        consumerBuilder = (ODataConsumer.Builder) newBuilderMethod.invoke(consumerClass, serviceRootUri);
      } catch (Exception e) {
        Throwables.propagate(e);
      }
    }

    private String[] getClassNames() {
      String impl = System.getProperty(CONSUMERIMPL_PROPERTY);
      if (JERSEY_CONSUMERIMPL.equalsIgnoreCase(impl))
        return new String[] { JERSEY_CONSUMER_CLASSNAME };
      else if (CXF_CONSUMERIMPL.equalsIgnoreCase(impl))
        return new String[] { CXF_CONSUMER_CLASSNAME };
      else if (impl != null && impl.length() > 0)
        return new String[] { impl };
      else
        // default
        return new String[] { JERSEY_CONSUMER_CLASSNAME, CXF_CONSUMER_CLASSNAME };
    }

    private Class<?> getConsumerClass(String[] classNames) throws ClassNotFoundException {
      for (String className : classNames) {
        try {
          Class<?> consumerClass = Class.forName(className, true, classLoader);
          if (consumerClass != null)
            return consumerClass;
        } catch (ClassNotFoundException e) {
          // will be re-thrown at the end of this method (if no class could be loaded)
        }
      }
      throw new ClassNotFoundException("Unable to load ODataConsumer implementation. The following class(es) could not be found: " + Arrays.toString(classNames));
    }

    @Override
    public ODataConsumer.Builder setFormatType(FormatType formatType) {
      return consumerBuilder.setFormatType(formatType);
    }

    @Override
    public ODataConsumer.Builder setClientBehaviors(OClientBehavior... clientBehaviors) {
      return consumerBuilder.setClientBehaviors(clientBehaviors);
    }

    @Override
    public ODataConsumer build() {
      return consumerBuilder.build();
    }
  }

  /**
   * Constructs a new builder for an {@link ODataConsumer} object.
   *
   * @param serviceRootUri  the OData service root uri
   */
  public static ODataConsumer.Builder newBuilder(String serviceRootUri) {
    return new Builder(serviceRootUri);
  }

  /**
   * Creates a new {@link ODataConsumer} for the given OData service root uri.
   *
   * <p>Wrapper for {@code ODataConsumers.newBuilder(serviceRootUri).build()}.
   *
   * @param serviceRootUri  the OData service root uri
   * @return a new OData consumer
   */
  public static ODataConsumer create(String serviceRootUri) {
    return ODataConsumers.newBuilder(serviceRootUri).build();
  }

  /**
   * Creates a new OData consumer for the Azure Table Storage service.
   * 
   * @param account  azure account key
   * @param key  azure secret key
   * @return a new OData consumer for the Azure Table Storage service
   * @see <a href="http://msdn.microsoft.com/en-us/library/dd179423.aspx">[msdn] Table Service API</a>
   */
  public static ODataConsumer azureTables(String account, String key) {
    String url = "http://" + account + ".table.core.windows.net/";

    return ODataConsumers.newBuilder(url).setClientBehaviors(OClientBehaviors.azureTables(account, key)).build();
  }

  /**
   * Creates a new OData consumer for the (now obsolete?) "dallas" service.
   * 
   * @param serviceRootUri  the service uri
   * @param accountKey  dallas account key
   * @param uniqueUserId  dallas user id
   * @return a new OData consumer for the (now obsolete?) "dallas" service
   */
  public static ODataConsumer dallas(String serviceRootUri, String accountKey, String uniqueUserId) {
    // CTP2
    //OClientBehavior dallasAuth = new DallasCtp2AuthenticationBehavior(accountKey, uniqueUserId);
    //OClientBehavior paging = new OldStylePagingBehavior(50, 1);
    //return ODataConsumer.create(serviceRootUri, dallasAuth, paging);

    // CTP3
    OClientBehavior basicAuth = OClientBehaviors.basicAuth("accountKey", accountKey);
    return ODataConsumers.newBuilder(serviceRootUri).setClientBehaviors(basicAuth).build();
  }

  /**
   * Creates a new OData consumer for the Windows Azure DataMarket service.
   * 
   * @param serviceRootUri  the service uri
   * @param accountKey  account key for basic authentication
   * @return a new OData consumer for the Windows Azure DataMarket service
   */
  public static ODataConsumer dataMarket(String serviceRootUri, String accountKey) {
    OClientBehavior basicAuth = OClientBehaviors.basicAuth("accountKey", accountKey);
    return ODataConsumers.newBuilder(serviceRootUri).setClientBehaviors(basicAuth).build();
  }
}
