/*
 * 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.client.core.communication.request.batch;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.AbstractMap;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.io.IOUtils;
import org.apache.commons.io.LineIterator;
import org.apache.commons.lang3.StringUtils;
import org.apache.olingo.client.api.ODataBatchConstants;
import org.apache.olingo.client.api.communication.request.batch.ODataBatchLineIterator;
import org.apache.olingo.commons.api.Constants;
import org.apache.olingo.commons.api.format.ContentType;
import org.apache.olingo.commons.api.http.HttpHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Utility class for batch requests and responses.
 */
public class ODataBatchUtilities {

  private static final byte[] CRLF = {13, 10};
  public static enum BatchItemType {
    NONE,
    CHANGESET,
    RETRIEVE
  }

  /**
   * Logger.
   */
  private static final Logger LOG = LoggerFactory.getLogger(ODataBatchUtilities.class);

  /**
   * Response line syntax.
   */
  private static final Pattern RESPONSE_PATTERN =
          Pattern.compile("HTTP/\\d\\.\\d (\\d+) (.*)", Pattern.CASE_INSENSITIVE);

  /**
   * Reads batch part taking source and delimiter (boundary) from given batch controller.
   * <p>
   * Usually used to consume/discard useless lines.
   *
   * @param batchController batch controller.
   * @param checkCurrent if 'TRUE' the current line will be included into the delimiter verification.
   * @return latest read line.
   */
  public static String readBatchPart(final ODataBatchController batchController, final boolean checkCurrent) {
    return readBatchPart(batchController, null, -1, checkCurrent);
  }

  /**
   * Reads the given number of line from the given batch wrapped into the batch controller.
   * <p>
   * Usually used to consume/discard useless lines.
   *
   * @param batchController batch controller.
   * @param count number of batch line to be read.
   * @return latest read line.
   */
  public static String readBatchPart(final ODataBatchController batchController, final int count) {
    return readBatchPart(batchController, null, count, true);
  }

  /**
   * Reads batch part taking source and delimiter (boundary) from given batch controller.
   * <p>
   * Usually used to read an entire batch part.
   *
   * @param controller batch controller.
   * @param os destination stream of batch part (null to discard).
   * @param checkCurrent if 'TRUE' the current line will be included into the delimiter verification.
   * @return latest read line.
   */
  public static String readBatchPart(
          final ODataBatchController controller, final OutputStream os, final boolean checkCurrent) {

    return readBatchPart(controller, os, -1, checkCurrent);
  }

  /**
   * Reads batch part taking source and delimiter (boundary) from given batch controller.
   * <p>
   * Usually used to read an entire batch part.
   *
   * @param controller batch controller.
   * @param os destination stream of batch part (null to discard).
   * @param count number of batch line to be read.
   * @param checkCurrent if 'TRUE' the current line will be included into the delimiter verification.
   * @return latest read line.
   */
  public static String readBatchPart(
          final ODataBatchController controller, final OutputStream os, final int count, final boolean checkCurrent) {

    String currentLine;

    synchronized (controller.getBatchLineIterator()) {
      currentLine = checkCurrent ? controller.getBatchLineIterator().getCurrent() : null;

      if (count < 0) {
        try {

          boolean notEndLine = isNotEndLine(controller, currentLine);

          while (controller.isValidBatch() && notEndLine && controller.getBatchLineIterator().hasNext()) {

            currentLine = controller.getBatchLineIterator().nextLine();
            LOG.debug("Read line '{}' (end-line '{}')", currentLine, controller.getBoundary());

            notEndLine = isNotEndLine(controller, currentLine);

            if (notEndLine && os != null && currentLine!=null) {
              os.write(currentLine.getBytes(Constants.UTF8));
              os.write(CRLF);
            }
          }

        } catch (IOException e) {
          LOG.error("Error reading batch part", e);
          throw new IllegalStateException(e);
        }

      } else {
        for (int i = 0; controller.isValidBatch() && controller.getBatchLineIterator().hasNext() && i < count; i++) {
          currentLine = controller.getBatchLineIterator().nextLine();
        }
      }
    }

    return currentLine;
  }

  /**
   * Reads headers from the batch starting from the given position.
   *
   * @param iterator batch iterator.
   * @return Map of header name in header values.
   */
  public static Map<String, Collection<String>> readHeaders(final ODataBatchLineIterator iterator) {
    final Map<String, Collection<String>> target =
            new TreeMap<>(String.CASE_INSENSITIVE_ORDER);

    readHeaders(iterator, target);
    return target;
  }

  /**
   * Reads headers from the batch starting from the given position.
   * <p>
   * Retrieved headers will be added to the map given by target parameter.
   *
   * @param iterator batch iterator.
   * @param target destination of the retrieved headers.
   */
  public static void readHeaders(
          final ODataBatchLineIterator iterator, final Map<String, Collection<String>> target) {

    try {
      final ByteArrayOutputStream baos = new ByteArrayOutputStream();
      readBatchPart(new ODataBatchController(iterator, null), baos, true);

      final LineIterator headers = IOUtils.lineIterator(new ByteArrayInputStream(baos.toByteArray()), Constants.UTF8);
      while (headers.hasNext()) {
        final String line = headers.nextLine().trim();
        if (StringUtils.isNotBlank(line)) {
          addHeaderLine(line, target);
        }
      }
    } catch (Exception e) {
      LOG.error("Error retrieving headers", e);
      throw new IllegalStateException(e);
    }
  }

  /**
   * Parses and adds the given header line to the given target map.
   *
   * @param headerLine header line to be added.
   * @param targetMap target map.
   */
  public static void addHeaderLine(final String headerLine, final Map<String, Collection<String>> targetMap) {
    final int sep = headerLine.indexOf(':');
    if (sep > 0 && sep < headerLine.length() - 1) {
      final String key = headerLine.substring(0, sep).trim();
      final Collection<String> value;
      if (targetMap.containsKey(key)) {
        value = targetMap.get(key);
      } else {
        value = new HashSet<>();
        targetMap.put(key, value);
      }
      value.add(headerLine.substring(sep + 1, headerLine.length()).trim());
    }
  }

  /**
   * Retrieved batch boundary from the given content-type header values.
   *
   * @param contentType content-types.
   * @return batch boundary.
   */
  public static String getBoundaryFromHeader(final Collection<String> contentType) {
    final String boundaryKey = ODataBatchConstants.BOUNDARY + "=";

    if (contentType == null || contentType.isEmpty() || !contentType.toString().contains(boundaryKey)) {
      throw new IllegalArgumentException("Invalid content type");
    }

    final String headerValue = contentType.toString();

    final int start = headerValue.indexOf(boundaryKey) + boundaryKey.length();
    int end = headerValue.indexOf(';', start);

    if (end < 0) {
      end = headerValue.indexOf(']', start);
    }

    final String res = headerValue.substring(start, end);
    return res.startsWith("--") ? res : "--" + res;
  }

  /**
   * Retrieves response line from the given position.
   *
   * @param iterator batch iterator.
   * @return retrieved response line.
   */
  public static Map.Entry<Integer, String> readResponseLine(final ODataBatchLineIterator iterator) {
    final String line = readBatchPart(new ODataBatchController(iterator, null), 1);
    LOG.debug("Response line '{}'", line);
    
    if(line !=null){

      final Matcher matcher = RESPONSE_PATTERN.matcher(line.trim());

      if (matcher.matches()) {
        return new AbstractMap.SimpleEntry<>(Integer.valueOf(matcher.group(1)), matcher.group(2));
      }
    }

    throw new IllegalArgumentException("Invalid response line '" + line + "'");
  }

  /**
   * Retrieves headers of the next batch item.
   *
   * @param iterator batch line iterator.
   * @param boundary batch boundary.
   * @return batch item headers.
   */
  public static Map<String, Collection<String>> nextItemHeaders(
          final ODataBatchLineIterator iterator, final String boundary) {

    final Map<String, Collection<String>> headers =
            new TreeMap<>(String.CASE_INSENSITIVE_ORDER);

    final String line = ODataBatchUtilities.readBatchPart(new ODataBatchController(iterator, boundary), true);

    if (line != null && line.trim().equals(boundary)) {
      ODataBatchUtilities.readHeaders(iterator, headers);
    }

    LOG.debug("Retrieved batch item headers {}", headers);
    return headers;
  }

  /**
   * Retrieves item type from item headers.
   *
   * @param headers batch item headers.
   * @return batch item type.
   */
  public static BatchItemType getItemType(final Map<String, Collection<String>> headers) {

    final BatchItemType nextItemType;

    final String contentType = headers.containsKey(HttpHeader.CONTENT_TYPE) ?
        headers.get(HttpHeader.CONTENT_TYPE).toString() :
        StringUtils.EMPTY;

    if (contentType.contains(ContentType.MULTIPART_MIXED.toContentTypeString())) {
      nextItemType = BatchItemType.CHANGESET;
    } else if (contentType.contains(ODataBatchConstants.ITEM_CONTENT_TYPE)) {
      nextItemType = BatchItemType.RETRIEVE;
    } else {
      nextItemType = BatchItemType.NONE;
    }

    LOG.debug("Retrieved next item type {}", nextItemType);
    return nextItemType;
  }

  /**
   * Checks if the given line is the expected end-line.
   *
   * @param controller batch controller.
   * @param line line to be checked.
   * @return 'TRUE' if the line is not the end-line; 'FALSE' otherwise.
   */
  private static boolean isNotEndLine(final ODataBatchController controller, final String line) {
    return line == null
            || (StringUtils.isBlank(controller.getBoundary()) && StringUtils.isNotBlank(line))
            || (StringUtils.isNotBlank(controller.getBoundary()) && !line.startsWith(controller.getBoundary()));
  }
}
