/*******************************************************************************
 * 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.olingo.odata2.core.rest;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;

import org.apache.olingo.odata2.api.commons.HttpHeaders;
import org.apache.olingo.odata2.api.exception.ODataBadRequestException;
import org.apache.olingo.odata2.api.exception.ODataException;
import org.apache.olingo.odata2.api.exception.ODataNotFoundException;
import org.apache.olingo.odata2.api.exception.ODataUnsupportedMediaTypeException;
import org.apache.olingo.odata2.api.processor.ODataResponse;
import org.apache.olingo.odata2.api.uri.PathSegment;
import org.apache.olingo.odata2.core.ODataPathSegmentImpl;
import org.apache.olingo.odata2.core.PathInfoImpl;
import org.apache.olingo.odata2.core.commons.ContentType;
import org.apache.olingo.odata2.core.commons.Decoder;

/**
 *  
 */
public class RestUtil {
  public static Response convertResponse(final ODataResponse odataResponse) {
    return convertResponse(odataResponse, false);
  }

  public static Response convertResponse(final ODataResponse odataResponse, final boolean omitResponseBody) {
    try {
      ResponseBuilder responseBuilder =
          Response.noContent().status(odataResponse.getStatus().getStatusCode());
      if(!omitResponseBody) {
        responseBuilder.entity(odataResponse.getEntity());
      }

      for (final String name : odataResponse.getHeaderNames()) {
        responseBuilder = responseBuilder.header(name, odataResponse.getHeader(name));
      }

      return responseBuilder.build();
    } catch (RuntimeException e) {
      if (odataResponse != null) {
        try {
          odataResponse.close();
        } catch (IOException inner) {
          // if close throw an exception we ignore these and re-throw our exception
          throw e;
        }
      }
      throw e;
    }
  }

  /**
   * Return http header value.
   * consider first header value only
   * avoid using jax-rs 2.0 (getHeaderString())
   * @param name header name
   * @param headers JAX-RS header map
   * @return header value or null
   */
  private static String getSafeHeader(final String name, final javax.ws.rs.core.HttpHeaders headers) {
    List<String> header = headers.getRequestHeader(name);
    if (header != null && !header.isEmpty()) {
      return header.get(0);
    } else {
      return null;
    }
  }

  public static ContentType extractRequestContentType(final SubLocatorParameter param)
      throws ODataUnsupportedMediaTypeException {

    String contentType = getSafeHeader(HttpHeaders.CONTENT_TYPE, param.getHttpHeaders());
    if (contentType == null || contentType.isEmpty()) {
      // RFC 2616, 7.2.1:
      // "Any HTTP/1.1 message containing an entity-body SHOULD include a
      // Content-Type header field defining the media type of that body. [...]
      // If the media type remains unknown, the recipient SHOULD treat it
      // as type "application/octet-stream"."
      return ContentType.APPLICATION_OCTET_STREAM;
    } else if (ContentType.isParseable(contentType)) {
      return ContentType.create(contentType);
    } else {
      throw new ODataUnsupportedMediaTypeException(
          ODataUnsupportedMediaTypeException.NOT_SUPPORTED_CONTENT_TYPE.addContent(contentType));
    }
  }

  /**
   * Extracts the request content from the servlet as input stream.
   * @param param initialization parameters
   * @return the request content as input stream
   * @throws ODataException
   */
  public static ServletInputStream extractRequestContent(final SubLocatorParameter param) throws ODataException {
    try {
      return param.getServletRequest().getInputStream();
    } catch (final IOException e) {
      throw new ODataException("Error getting request content as ServletInputStream.", e);
    }
  }

  public static <T> InputStream contentAsStream(final T content) throws ODataException {
    if (content == null) {
      throw new ODataBadRequestException(ODataBadRequestException.COMMON);
    }

    InputStream inputStream;
    if (content instanceof InputStream) {
      inputStream = (InputStream) content;
    } else if (content instanceof String) {
      try {
        inputStream = new ByteArrayInputStream(((String) content).getBytes("UTF-8"));
      } catch (final UnsupportedEncodingException e) {
        throw new ODataBadRequestException(ODataBadRequestException.COMMON, e);
      }
    } else {
      throw new ODataBadRequestException(ODataBadRequestException.COMMON);
    }
    return inputStream;
  }

  public static List<String> extractAcceptHeaders(final SubLocatorParameter param) throws ODataBadRequestException {
    List<String> acceptHeaders = param.getHttpHeaders().getRequestHeader(HttpHeaders.ACCEPT);

    List<String> toSort = new LinkedList<String>();
    if (acceptHeaders != null) {
      for (String acceptHeader : acceptHeaders) {
        String[] contentTypes = acceptHeader.split(",");
        for (String contentType : contentTypes) {
          toSort.add(contentType.trim());
        }
      }
    }

    ContentType.sortForQParameter(toSort);
    return toSort;
  }

  public static Map<String, String> extractRequestHeaders(final javax.ws.rs.core.HttpHeaders httpHeaders) {
    final MultivaluedMap<String, String> headers = httpHeaders.getRequestHeaders();
    Map<String, String> headerMap = new HashMap<String, String>();

    for (final String key : headers.keySet()) {
      String value = getSafeHeader(key, httpHeaders);
      if (value != null && !"".equals(value)) {
        headerMap.put(key, value);
      }
    }
    return headerMap;
  }

  public static PathInfoImpl buildODataPathInfo(final SubLocatorParameter param) throws ODataException {
    PathInfoImpl pathInfo = splitPath(param);

    pathInfo.setServiceRoot(buildBaseUri(param.getUriInfo(),
        param.getServletRequest(), pathInfo.getPrecedingSegments()));
    pathInfo.setRequestUri(buildRequestUri(param.getServletRequest()));

    return pathInfo;
  }

  private static PathInfoImpl splitPath(final SubLocatorParameter param) throws ODataException {
    PathInfoImpl pathInfo = new PathInfoImpl();

    List<javax.ws.rs.core.PathSegment> precedingPathSegments;
    List<javax.ws.rs.core.PathSegment> pathSegments;

    if (param.getPathSplit() == 0) {
      precedingPathSegments = Collections.emptyList();
      pathSegments = param.getPathSegments();
    } else {
      if (param.getPathSegments().size() < param.getPathSplit()) {
        throw new ODataBadRequestException(ODataBadRequestException.URLTOOSHORT);
      }

      precedingPathSegments = param.getPathSegments().subList(0, param.getPathSplit());
      final int pathSegmentCount = param.getPathSegments().size();
      pathSegments = param.getPathSegments().subList(param.getPathSplit(), pathSegmentCount);
    }

    // Percent-decode only the preceding path segments.
    // The OData path segments are decoded during URI parsing.
    pathInfo.setPrecedingPathSegment(convertPathSegmentList(precedingPathSegments));

    List<PathSegment> odataSegments = new ArrayList<PathSegment>();
    for (final javax.ws.rs.core.PathSegment segment : pathSegments) {
      if (segment.getMatrixParameters() == null || segment.getMatrixParameters().isEmpty()) {
        odataSegments.add(new ODataPathSegmentImpl(segment.getPath(), null));
      } else {
        // post condition: we do not allow matrix parameters in OData path segments
        throw new ODataNotFoundException(ODataNotFoundException.MATRIX.addContent(segment.getMatrixParameters()
            .keySet(), segment.getPath()));
      }
    }
    pathInfo.setODataPathSegment(odataSegments);

    return pathInfo;
  }

  private static URI buildBaseUri(final UriInfo uriInfo, final HttpServletRequest request,
      final List<PathSegment> precedingPathSegments) throws ODataException {
    try {
      String path = uriInfo.getBaseUri().getPath();
      UriBuilder uriBuilder = UriBuilder.fromUri(path);
      for (final PathSegment ps : precedingPathSegments) {
        uriBuilder = uriBuilder.path(ps.getPath());
        for (final String key : ps.getMatrixParameters().keySet()) {
          final Object[] v = ps.getMatrixParameters().get(key).toArray();
          uriBuilder = uriBuilder.matrixParam(key, v);
        }
      }

      /*
       * workaround because of host name is cached by uriInfo
       */
      uriBuilder.host(request.getServerName()).port(request.getServerPort());
      uriBuilder.scheme(request.getScheme());

      String uriString = uriBuilder.build().toString();
      if (!uriString.endsWith("/")) {
        uriString = uriString + "/";
      }

      return new URI(uriString);
    } catch (final URISyntaxException e) {
      throw new ODataException(e);
    }
  }

  private static URI buildRequestUri(final HttpServletRequest servletRequest) {
    URI requestUri;

    StringBuffer buf = servletRequest.getRequestURL();
    String queryString = servletRequest.getQueryString();

    if (queryString != null) {
      buf.append("?");
      buf.append(queryString);
    }

    String requestUriString = buf.toString();

    requestUri = URI.create(requestUriString);
    return requestUri;
  }

  private static List<PathSegment> convertPathSegmentList(final List<javax.ws.rs.core.PathSegment> pathSegments) {
    ArrayList<PathSegment> converted = new ArrayList<PathSegment>();
    for (final javax.ws.rs.core.PathSegment pathSegment : pathSegments) {
      final PathSegment segment =
          new ODataPathSegmentImpl(Decoder.decode(pathSegment.getPath()), pathSegment.getMatrixParameters());
      converted.add(segment);
    }
    return converted;
  }

  public static Map<String, String> convertToSinglevaluedMap(final MultivaluedMap<String, String> multi) {
    final Map<String, String> single = new HashMap<String, String>();

    for (final String key : multi.keySet()) {
      final String value = multi.getFirst(key);
      single.put(key, value);
    }

    return single;
  }

}
