/**
 * 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.apache.hadoop.gateway.dispatch;

import org.apache.hadoop.gateway.SpiGatewayMessages;
import org.apache.hadoop.gateway.filter.AbstractGatewayFilter;
import org.apache.hadoop.gateway.i18n.messages.MessagesFactory;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.CloseableHttpClient;

import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import static org.apache.hadoop.gateway.config.ConfigurationInjectorBuilder.configuration;

public class GatewayDispatchFilter extends AbstractGatewayFilter {

  private static Map<String, Adapter> METHOD_ADAPTERS = createMethodAdapters();

  protected static SpiGatewayMessages LOG = MessagesFactory.get(SpiGatewayMessages.class);

  private Dispatch dispatch;

  private HttpClient httpClient;

  private static Map<String, Adapter> createMethodAdapters() {
    Map<String, Adapter> map = new HashMap<>();
    map.put("GET", new GetAdapter());
    map.put("POST", new PostAdapter());
    map.put("PUT", new PutAdapter());
    map.put("DELETE", new DeleteAdapter());
    map.put("OPTIONS", new OptionsAdapter());
    return Collections.unmodifiableMap(map);
  }

  @Override
  public void init(FilterConfig filterConfig) throws ServletException {
    super.init(filterConfig);
    if (dispatch == null) {
      String dispatchImpl = filterConfig.getInitParameter("dispatch-impl");
      dispatch = newInstanceFromName(dispatchImpl);
    }
    configuration().target(dispatch).source(filterConfig).inject();
    HttpClientFactory httpClientFactory;
    String httpClientFactoryClass = filterConfig.getInitParameter("httpClientFactory");
    if (httpClientFactoryClass != null) {
      httpClientFactory = newInstanceFromName(httpClientFactoryClass);
    } else {
      httpClientFactory = new DefaultHttpClientFactory();
    }
    httpClient = httpClientFactory.createHttpClient(filterConfig);
    dispatch.setHttpClient(httpClient);
    dispatch.init();
  }

  @Override
  public void destroy() {
    dispatch.destroy();
    try {
      if (httpClient instanceof  CloseableHttpClient) {
        ((CloseableHttpClient) httpClient).close();
      }
    } catch ( IOException e ) {
      LOG.errorClosingHttpClient(e);
    }
  }

  public Dispatch getDispatch() {
    return dispatch;
  }

  public void setDispatch(Dispatch dispatch) {
    this.dispatch = dispatch;
  }

  @Override
  protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
    String method = request.getMethod().toUpperCase();
    Adapter adapter = METHOD_ADAPTERS.get(method);
    if ( adapter != null ) {
      try {
        adapter.doMethod(dispatch, request, response);
      } catch ( URISyntaxException e ) {
        throw new ServletException(e);
      }
    } else {
      response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
    }
  }

  protected static URI getDispatchUrl(HttpServletRequest request) {
    StringBuffer str = request.getRequestURL();
    String query = request.getQueryString();
    if ( query != null ) {
      str.append('?');
      str.append(query);
    }
    URI url = URI.create(str.toString());
    return url;
  }

  private interface Adapter {
    public void doMethod(Dispatch dispatch, HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException, URISyntaxException;
  }

  private static class GetAdapter implements Adapter {
    public void doMethod(Dispatch dispatch, HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException, URISyntaxException {
      dispatch.doGet(getDispatchUrl(request), request, response);
    }
  }

  private static class PostAdapter implements Adapter {
    public void doMethod(Dispatch dispatch, HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException, URISyntaxException {
      dispatch.doPost(getDispatchUrl(request), request, response);
    }
  }

  private static class PutAdapter implements Adapter {
    public void doMethod(Dispatch dispatch, HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException, URISyntaxException {
      dispatch.doPut(getDispatchUrl(request), request, response);
    }
  }

  private static class DeleteAdapter implements Adapter {
    public void doMethod(Dispatch dispatch, HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException, URISyntaxException {
      dispatch.doDelete(getDispatchUrl(request), request, response);
    }
  }

  private static class OptionsAdapter implements Adapter {
    public void doMethod(Dispatch dispatch, HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException, URISyntaxException {
      dispatch.doOptions(getDispatchUrl(request), request, response);
    }
  }

  private <T> T newInstanceFromName(String dispatchImpl) throws ServletException {
    try {
      Class<T> clazz = loadClass(dispatchImpl);
      return clazz.newInstance();
    } catch ( Exception e ) {
      throw new ServletException(e);
    }
  }

  private <T> Class<T> loadClass(String className) throws ClassNotFoundException {
      ClassLoader loader = Thread.currentThread().getContextClassLoader();
      if ( loader == null ) {
        loader = this.getClass().getClassLoader();
      }
      return (Class<T>) loader.loadClass(className);
  }
}
