/*
 * 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.servicecomb.common.accessLog.core.parser.impl;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;

import com.google.common.annotations.VisibleForTesting;
import org.apache.servicecomb.common.accessLog.core.element.AccessLogItem;
import org.apache.servicecomb.common.accessLog.core.element.impl.PlainTextAccessItem;
import org.apache.servicecomb.common.accessLog.core.parser.AccessLogItemMeta;
import org.apache.servicecomb.common.accessLog.core.parser.AccessLogPatternParser;
import org.apache.servicecomb.common.accessLog.core.parser.CompositeVertxRestAccessLogItemMeta;
import org.apache.servicecomb.common.accessLog.core.parser.VertxRestAccessLogItemMeta;
import org.apache.servicecomb.foundation.common.utils.SPIServiceUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.vertx.ext.web.RoutingContext;

/**
 * The parser is used for rest-over-vertx transport.
 */
public class VertxRestAccessLogPatternParser implements AccessLogPatternParser<RoutingContext> {
  private static final Logger LOGGER = LoggerFactory.getLogger(VertxRestAccessLogPatternParser.class);

  public static final Comparator<VertxRestAccessLogItemMeta> accessLogItemMetaComparator = (m1, m2) -> {
    int result = m1.getOrder() - m2.getOrder();
    if (result != 0) {
      return result;
    }

    // one of m1 & m2 has suffix, but the other one doesn't have
    if (m1.getSuffix() == null ^ m2.getSuffix() == null) {
      return m1.getSuffix() == null ? 1 : -1;
    }

    if (null != m1.getSuffix()) {
      result = comparePlaceholderString(m1.getSuffix(), m2.getSuffix());
    }

    return 0 == result ?
        comparePlaceholderString(m1.getPrefix(), m2.getPrefix())
        : result;
  };

  private final List<VertxRestAccessLogItemMeta> metaList = new ArrayList<>();

  public VertxRestAccessLogPatternParser() {
    List<VertxRestAccessLogItemMeta> loadedMeta = loadVertxRestLogItemMeta();
    if (null == loadedMeta || loadedMeta.isEmpty()) {
      LOGGER.error("cannot load AccessLogItemMeta!");
      throw new IllegalStateException("cannot load AccessLogItemMeta!");
    }
    for (VertxRestAccessLogItemMeta meta : loadedMeta) {
      if (CompositeVertxRestAccessLogItemMeta.class.isAssignableFrom(meta.getClass())) {
        this.metaList.addAll(((CompositeVertxRestAccessLogItemMeta) meta).getAccessLogItemMetas());
      } else {
        this.metaList.add(meta);
      }
    }
    sortAccessLogItemMeta(this.metaList);
  }

  @VisibleForTesting
  List<VertxRestAccessLogItemMeta> getMetaList() {
    return metaList;
  }

  @VisibleForTesting
  List<VertxRestAccessLogItemMeta> loadVertxRestLogItemMeta() {
    return SPIServiceUtils.getOrLoadSortedService(VertxRestAccessLogItemMeta.class);
  }

  /**
   * Behavior of this compare:
   * 1. comparePlaceholderString("abc","bbc") &lt; 0
   * 2. comparePlaceholderString("abc","ab") &lt; 0
   * 3. comparePlaceholderString("abc","abc") = 0
   */
  public static int comparePlaceholderString(String s1, String s2) {
    int result = s1.compareTo(s2);
    if (0 == result) {
      return result;
    }

    // there are two possible cases:
    // 1. s1="ab", s2="def"
    // 2. s1="ab", s2="abc"
    // in the case1 just return the result, but int the case2 the result should be reversed
    return result < 0 ?
        (s2.startsWith(s1) ? -result : result)
        : (s1.startsWith(s2) ? -result : result);
  }

  /**
   * Sort all of the {@link AccessLogItemMeta}, the meta that is in front of the others has higher priority.
   *
   * Sort rule(priority decreased):
   * <ol>
   *   <li>compare the {@link AccessLogItemMeta#getOrder()}</li>
   *   <li>compare the {@link AccessLogItemMeta#getSuffix()} in lexicographic order, if one's suffix is start with
   *   the other one's suffix, this one(who's suffix is longer) has higher priority</li>
   *   <li>compare the {@link AccessLogItemMeta#getPrefix()}, compare rule is the same as suffix.</li>
   * </ol>
   *
   * e.g. given a list of {@link AccessLogItemMeta} like below:
   * <ol>
   * <li>(%ac{,}bcd)</li>
   * <li>(%ac{,}bc)</li>
   * <li>(%ac{,}a)</li>
   * <li>(%ac,)</li>
   * <li>(%b,)</li>
   * <li>(%a)</li>
   * <li>(%{,}b)</li>
   * <li>(%{,}bc)</li>
   * </ol>
   * the result is:
   * <ol>
   * <li>(%ac{,}a)</li>
   * <li>(%ac{,}bcd)</li>
   * <li>(%ac{,}bc)</li>
   * <li>(%{,}bc)</li>
   * <li>(%{,}b)</li>
   * <li>(%ac,)</li>
   * <li>(%a)</li>
   * <li>(%b,)</li>
   * </ol>
   */
  public static void sortAccessLogItemMeta(List<VertxRestAccessLogItemMeta> accessLogItemMetaList) {
    accessLogItemMetaList.sort(accessLogItemMetaComparator);
  }

  /**
   * @param rawPattern The access log pattern string specified by users.
   * @return A list of {@linkplain AccessLogItem} which actually generate the content of access log.
   */
  @Override
  public List<AccessLogItem<RoutingContext>> parsePattern(String rawPattern) {
    LOGGER.info("parse the pattern of access log: [{}]", rawPattern);
    List<AccessLogItemLocation> locationList = matchAccessLogItem(rawPattern);
    locationList = fillInPlainTextLocation(rawPattern, locationList);

    return convertToItemList(rawPattern, locationList);
  }

  /**
   * Use the {@link #metaList} to match rawPattern.
   * Return a list of {@link AccessLogItemLocation}.
   * Plain text is ignored.
   */
  private List<AccessLogItemLocation> matchAccessLogItem(String rawPattern) {
    List<AccessLogItemLocation> locationList = new ArrayList<>();
    int cursor = 0;
    while (cursor < rawPattern.length()) {
      AccessLogItemLocation candidate = null;
      for (VertxRestAccessLogItemMeta meta : metaList) {
        if (null != candidate && null == meta.getSuffix()) {
          // TODO:
          // if user define item("%{","}ab") and item("%{_","}abc") and the pattern is "%{_var}ab}abc"
          // currently the result is item("%{","_var","}ab"), plaintext("}abc")
          // is this acceptable?

          // We've gotten an AccessLogItem with suffix, so there is no need to match those without suffix,
          // just break this match loop
          cursor = candidate.tail;
          break;
        }
        if (rawPattern.startsWith(meta.getPrefix(), cursor)) {
          if (null == meta.getSuffix()) {
            // for simple type AccessLogItem, there is no need to try to match the next item.
            candidate = new AccessLogItemLocation(cursor, meta);
            cursor = candidate.tail;
            break;
          }
          // for configurable type, like %{...}i, more check is needed
          // e.g. "%{varName1}o ${varName2}i" should be divided into
          // ResponseHeaderItem with varName="varName1" and RequestHeaderItem with varName="varName2"
          // INSTEAD OF RequestHeaderItem with varName="varName1}o ${varName2"
          int rear = rawPattern.indexOf(meta.getSuffix(), cursor);
          if (rear < 0) {
            continue;
          }
          if (null == candidate || rear < candidate.suffixIndex) {
            candidate = new AccessLogItemLocation(cursor, rear, meta);
          }
          // There is a matched item which is in front of this item, so this item is ignored.
        }
      }

      if (candidate == null) {
        ++cursor;
        continue;
      }
      locationList.add(candidate);
    }

    return locationList;
  }

  /**
   * After processing of {@link #matchAccessLogItem(String)}, all of the placeholders of {@link AccessLogItem} have been
   * picked out. So the rest part of rawPattern should be treated as plain text. Those parts will be located in this
   * method and wrapped as {@link PlainTextAccessItem}.
   * @param rawPattern raw pattern string of access log
   * @param locationList locations picked out by {@link #matchAccessLogItem(String)}
   * @return all of the locations including {@link PlainTextAccessItem}.
   */
  private List<AccessLogItemLocation> fillInPlainTextLocation(String rawPattern,
      List<AccessLogItemLocation> locationList) {
    List<AccessLogItemLocation> resultList = new ArrayList<>();
    if (locationList.isEmpty()) {
      resultList.add(createTextPlainItemLocation(0, rawPattern.length()));
      return resultList;
    }

    Iterator<AccessLogItemLocation> itemLocationIterator = locationList.iterator();
    AccessLogItemLocation previousItemLocation = itemLocationIterator.next();
    if (previousItemLocation.prefixIndex > 0) {
      resultList.add(createTextPlainItemLocation(0, previousItemLocation.prefixIndex));
    }
    resultList.add(previousItemLocation);

    while (itemLocationIterator.hasNext()) {
      AccessLogItemLocation thisItemLocation = itemLocationIterator.next();
      if (previousItemLocation.tail < thisItemLocation.prefixIndex) {
        resultList.add(createTextPlainItemLocation(previousItemLocation.tail, thisItemLocation.prefixIndex));
      }
      previousItemLocation = thisItemLocation;
      resultList.add(previousItemLocation);
    }

    if (previousItemLocation.tail < rawPattern.length()) {
      resultList.add(createTextPlainItemLocation(
          previousItemLocation.tail,
          rawPattern.length()));
    }
    return resultList;
  }

  private AccessLogItemLocation createTextPlainItemLocation(int front, int rear) {
    return new AccessLogItemLocation(front, rear);
  }

  private List<AccessLogItem<RoutingContext>> convertToItemList(String rawPattern,
      List<AccessLogItemLocation> locationList) {
    List<AccessLogItem<RoutingContext>> itemList = new ArrayList<>();

    for (AccessLogItemLocation accessLogItemLocation : locationList) {
      VertxRestAccessLogItemMeta accessLogItemMeta = accessLogItemLocation.accessLogItemMeta;
      if (null == accessLogItemMeta) {
        // a PlainTextItem location
        itemList.add(new PlainTextAccessItem(rawPattern.substring(
            accessLogItemLocation.prefixIndex, accessLogItemLocation.tail
        )));
        continue;
      }

      itemList.add(accessLogItemMeta.getAccessLogItemCreator().createItem(
          getConfigString(rawPattern, accessLogItemLocation))
      );
    }

    return itemList;
  }

  private String getConfigString(String rawPattern, AccessLogItemLocation accessLogItemLocation) {
    if (null == accessLogItemLocation.getSuffix()) {
      // simple AccessLogItem
      return null;
    }

    return rawPattern.substring(
        accessLogItemLocation.prefixIndex + accessLogItemLocation.getPrefix().length(),
        accessLogItemLocation.suffixIndex);
  }

  private static class AccessLogItemLocation {
    /**
     * prefixIndex = rawPattern.indexOf(prefix)
     */
    final int prefixIndex;

    /**
     * suffixIndex = rawPattern.indexOf(suffix)
     */
    final int suffixIndex;

    /**
     * tail = suffixIndex + suffix.length()
     */
    final int tail;

    VertxRestAccessLogItemMeta accessLogItemMeta;

    /**
     * for {@link PlainTextAccessItem} only
     */
    AccessLogItemLocation(int prefixIndex, int suffixIndex) {
      this.prefixIndex = prefixIndex;
      this.suffixIndex = suffixIndex;
      this.tail = suffixIndex;
    }

    /**
     * for configurable type AccessLogItem
     */
    AccessLogItemLocation(int prefixIndex, int suffixIndex, VertxRestAccessLogItemMeta accessLogItemMeta) {
      this.prefixIndex = prefixIndex;
      this.suffixIndex = suffixIndex;
      this.tail = suffixIndex + accessLogItemMeta.getSuffix().length();
      this.accessLogItemMeta = accessLogItemMeta;
    }

    /**
     * for simple type AccessLogItem
     */
    AccessLogItemLocation(int prefixIndex, VertxRestAccessLogItemMeta accessLogItemMeta) {
      this.prefixIndex = prefixIndex;
      this.suffixIndex = prefixIndex + accessLogItemMeta.getPrefix().length();
      this.tail = this.suffixIndex;
      this.accessLogItemMeta = accessLogItemMeta;
    }

    public String getPrefix() {
      if (null == accessLogItemMeta) {
        return null;
      }
      return accessLogItemMeta.getPrefix();
    }

    public String getSuffix() {
      if (null == accessLogItemMeta) {
        return null;
      }
      return accessLogItemMeta.getSuffix();
    }
  }
}
