/*
 * 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.server.core.uri.validator;

import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;

import org.apache.olingo.commons.api.edm.EdmFunction;
import org.apache.olingo.commons.api.edm.EdmProperty;
import org.apache.olingo.commons.api.edm.EdmReturnType;
import org.apache.olingo.commons.api.edm.EdmType;
import org.apache.olingo.commons.api.edm.constants.EdmTypeKind;
import org.apache.olingo.commons.api.http.HttpMethod;
import org.apache.olingo.server.api.uri.UriInfo;
import org.apache.olingo.server.api.uri.UriResource;
import org.apache.olingo.server.api.uri.UriResourceAction;
import org.apache.olingo.server.api.uri.UriResourceFunction;
import org.apache.olingo.server.api.uri.UriResourceKind;
import org.apache.olingo.server.api.uri.UriResourcePartTyped;
import org.apache.olingo.server.api.uri.UriResourceProperty;
import org.apache.olingo.server.api.uri.queryoption.SystemQueryOption;
import org.apache.olingo.server.api.uri.queryoption.SystemQueryOptionKind;

public class UriValidator {

  //@formatter:off (Eclipse formatter)
  //CHECKSTYLE:OFF (Maven checkstyle)
  private static final boolean[][] decisionMatrix =
    {
      /*                                          0-FILTER 1-FORMAT 2-EXPAND 3-ID     4-COUNT  5-ORDERBY 6-SEARCH 7-SELECT 8-SKIP   9-SKIPTOKEN 10-TOP 11-APPLY  12-DELTATOKEN */
      /*                              all  0 */ { true ,   true ,   true ,   false,   true ,   true ,    true ,   true ,   true ,   true ,      true , true,      true  },
      /*                            batch  1 */ { false,   false,   false,   false,   false,   false,    false,   false,   false,   false,      false, false,     false  },
      /*                        crossjoin  2 */ { true ,   true ,   true ,   false,   true ,   true ,    true ,   true ,   true ,   true ,      true , true,      true  },
      /*                         entityId  3 */ { false,   true ,   true ,   true ,   false,   false,    false,   true ,   false,   false,      false, false,     false },
      /*                         metadata  4 */ { false,   true ,   false,   false,   false,   false,    false,   false,   false,   false,      false, false,     false },
      /*                          service  5 */ { false,   true ,   false,   false,   false,   false,    false,   false,   false,   false,      false, false,     false },
      /*                        entitySet  6 */ { true ,   true ,   true ,   false,   true ,   true ,    true ,   true ,   true ,   true ,      true , true,      true  },
      /*                   entitySetCount  7 */ { true ,   false,   false,   false,   false,   false,    true ,   false,   false,   false,      false, true,      true  },
      /*                           entity  8 */ { false,   true ,   true ,   false,   false,   false,    false,   true ,   false,   false,      false, false,     false },
      /*                      mediaStream  9 */ { false,   false,   false,   false,   false,   false,    false,   false,   false,   false,      false, false,     false },
      /*                       references 10 */ { true ,   true ,   false,   false,   true ,   true ,    true ,   false,   true ,   true ,      true , false,     true },
      /*                        reference 11 */ { false,   true ,   false,   false,   false,   false,    false,   false,   false,   false,      false, false,     false },
      /*                  propertyComplex 12 */ { false,   true ,   true ,   false,   false,   false,    false,   true ,   false,   false,      false, false,     false },
      /*        propertyComplexCollection 13 */ { true ,   true ,   true ,   false,   true ,   true ,    false,   true ,   true ,   true ,      true , true ,     true },
      /*   propertyComplexCollectionCount 14 */ { true ,   false,   false,   false,   false,   false,    false,   false,   false,   false,      false, true ,     false },
      /*                propertyPrimitive 15 */ { false,   true ,   false,   false,   false,   false,    false,   false,   false,   false,      false, false,     false },
      /*      propertyPrimitiveCollection 16 */ { true ,   true ,   false,   false,   true ,   true ,    false,   false,   true ,   true ,      true , false,     true },
      /* propertyPrimitiveCollectionCount 17 */ { true ,   false,   false,   false,   false,   false,    false,   false,   false,   false,      false, false,     false },
      /*           propertyPrimitiveValue 18 */ { false,   true ,   false,   false,   false,   false,    false,   false,   false,   false,      false, false,     false },
      /*                             none 19 */ { false,   true ,   false,   false,   false,   false,    false,   false,   false,   false,      false, false,     false }
    };
  //CHECKSTYLE:ON
  //@formatter:on

  private enum UriType {
    all(0),
    batch(1),
    crossjoin(2),
    entityId(3),
    metadata(4),
    service(5),
    entitySet(6),
    entitySetCount(7),
    entity(8),
    mediaStream(9),
    references(10),
    reference(11),
    propertyComplex(12),
    propertyComplexCollection(13),
    propertyComplexCollectionCount(14),
    propertyPrimitive(15),
    propertyPrimitiveCollection(16),
    propertyPrimitiveCollectionCount(17),
    propertyPrimitiveValue(18),
    none(19);

    private final int idx;

    UriType(final int i) {
      idx = i;
    }

    public int getIndex() {
      return idx;
    }
  }

  private static final Map<SystemQueryOptionKind, Integer> OPTION_INDEX;
  static {
    Map<SystemQueryOptionKind, Integer> temp =
        new EnumMap<>(SystemQueryOptionKind.class);
    temp.put(SystemQueryOptionKind.FILTER, 0);
    temp.put(SystemQueryOptionKind.FORMAT, 1);
    temp.put(SystemQueryOptionKind.EXPAND, 2);
    temp.put(SystemQueryOptionKind.ID, 3);
    temp.put(SystemQueryOptionKind.COUNT, 4);
    temp.put(SystemQueryOptionKind.ORDERBY, 5);
    temp.put(SystemQueryOptionKind.SEARCH, 6);
    temp.put(SystemQueryOptionKind.SELECT, 7);
    temp.put(SystemQueryOptionKind.SKIP, 8);
    temp.put(SystemQueryOptionKind.SKIPTOKEN, 9);
    temp.put(SystemQueryOptionKind.TOP, 10);
    temp.put(SystemQueryOptionKind.APPLY, 11);
    temp.put(SystemQueryOptionKind.DELTATOKEN, 12);
    OPTION_INDEX = Collections.unmodifiableMap(temp);
  }

  public void validate(final UriInfo uriInfo, final HttpMethod httpMethod) throws UriValidationException {
    final UriType uriType = getUriType(uriInfo);
    if (HttpMethod.GET == httpMethod) {
      validateReadQueryOptions(uriType, uriInfo.getSystemQueryOptions());
    } else {
      validateNonReadQueryOptions(uriType, isAction(uriInfo), uriInfo.getSystemQueryOptions(), httpMethod);
      validatePropertyOperations(uriInfo, httpMethod);
    }
  }

  private UriType getUriType(final UriInfo uriInfo) throws UriValidationException {
    UriType uriType;

    switch (uriInfo.getKind()) {
    case all:
      uriType = UriType.all;
      break;
    case batch:
      uriType = UriType.batch;
      break;
    case crossjoin:
      uriType = UriType.crossjoin;
      break;
    case entityId:
      uriType = UriType.entityId;
      break;
    case metadata:
      uriType = UriType.metadata;
      break;
    case resource:
      uriType = getUriTypeForResource(uriInfo.getUriResourceParts());
      break;
    case service:
      uriType = UriType.service;
      break;
    default:
      throw new UriValidationException("Unsupported uriInfo kind: " + uriInfo.getKind(),
          UriValidationException.MessageKeys.UNSUPPORTED_URI_KIND, uriInfo.getKind().toString());
    }

    return uriType;
  }

  /**
   * Determines the URI type for a resource path.
   * The URI parser has already made sure that there are enough segments for a given type of the last segment,
   * but don't try to extract always the second-to-last segment, it could cause an {@link IndexOutOfBoundsException}.
   */
  private UriType getUriTypeForResource(final List<UriResource> segments) throws UriValidationException {
    final UriResource lastPathSegment = segments.get(segments.size() - 1);

    UriType uriType;
    switch (lastPathSegment.getKind()) {
    case count:
      uriType = getUriTypeForCount(segments.get(segments.size() - 2));
      break;
    case action:
      uriType = getUriTypeForAction(lastPathSegment);
      break;
    case complexProperty:
      uriType = getUriTypeForComplexProperty(lastPathSegment);
      break;
    case entitySet:
    case navigationProperty:
      uriType = getUriTypeForEntitySet(lastPathSegment);
      break;
    case function:
      uriType = getUriTypeForFunction(lastPathSegment);
      break;
    case primitiveProperty:
      uriType = getUriTypeForPrimitiveProperty(lastPathSegment);
      break;
    case ref:
      uriType = getUriTypeForRef(segments.get(segments.size() - 2));
      break;
    case singleton:
      uriType = UriType.entity;
      break;
    case value:
      uriType = getUriTypeForValue(segments.get(segments.size() - 2));
      break;
    default:
      throw new UriValidationException("Unsupported uriResource kind: " + lastPathSegment.getKind(),
          UriValidationException.MessageKeys.UNSUPPORTED_URI_RESOURCE_KIND, lastPathSegment.getKind().toString());
    }

    return uriType;
  }

  private UriType getUriTypeForValue(final UriResource secondLastPathSegment) throws UriValidationException {
    UriType uriType;
    switch (secondLastPathSegment.getKind()) {
    case primitiveProperty:
      uriType = UriType.propertyPrimitiveValue;
      break;
    case entitySet:
    case navigationProperty:
    case singleton:
      uriType = UriType.mediaStream;
      break;
    case function:
      UriResourceFunction uriFunction = (UriResourceFunction) secondLastPathSegment;
      final EdmFunction function = uriFunction.getFunction();
      uriType = function.getReturnType().getType().getKind() == EdmTypeKind.ENTITY ?
          UriType.mediaStream : UriType.propertyPrimitiveValue;
      break;
    default:
      throw new UriValidationException(
          "Unexpected kind in path segment before $value: " + secondLastPathSegment.getKind(),
          UriValidationException.MessageKeys.UNALLOWED_KIND_BEFORE_VALUE, secondLastPathSegment.toString());
    }
    return uriType;
  }

  private UriType getUriTypeForRef(final UriResource secondLastPathSegment) throws UriValidationException {
    return isCollection(secondLastPathSegment) ? UriType.references : UriType.reference;
  }

  private boolean isCollection(final UriResource pathSegment) throws UriValidationException {
    if (pathSegment instanceof UriResourcePartTyped) {
      return ((UriResourcePartTyped) pathSegment).isCollection();
    } else {
      throw new UriValidationException(
          "Path segment is not an instance of UriResourcePartTyped but " + pathSegment.getClass(),
          UriValidationException.MessageKeys.LAST_SEGMENT_NOT_TYPED, pathSegment.toString());
    }
  }

  private UriType getUriTypeForPrimitiveProperty(final UriResource lastPathSegment) throws UriValidationException {
    return isCollection(lastPathSegment) ? UriType.propertyPrimitiveCollection : UriType.propertyPrimitive;
  }

  private UriType getUriTypeForFunction(final UriResource lastPathSegment) throws UriValidationException {
    final UriResourceFunction uriFunction = (UriResourceFunction) lastPathSegment;
    final boolean isCollection = uriFunction.isCollection();
    final EdmTypeKind typeKind = uriFunction.getFunction().getReturnType().getType().getKind();
    UriType uriType;
    switch (typeKind) {
    case ENTITY:
      uriType = isCollection ? UriType.entitySet : UriType.entity;
      break;
    case PRIMITIVE:
    case ENUM:
    case DEFINITION:
      uriType = isCollection ? UriType.propertyPrimitiveCollection : UriType.propertyPrimitive;
      break;
    case COMPLEX:
      uriType = isCollection ? UriType.propertyComplexCollection : UriType.propertyComplex;
      break;
    default:
      throw new UriValidationException("Unsupported function return type: " + typeKind,
          UriValidationException.MessageKeys.UNSUPPORTED_FUNCTION_RETURN_TYPE, typeKind.toString());
    }

    return uriType;
  }

  private UriType getUriTypeForEntitySet(final UriResource lastPathSegment) throws UriValidationException {
    return isCollection(lastPathSegment) ? UriType.entitySet : UriType.entity;
  }

  private UriType getUriTypeForComplexProperty(final UriResource lastPathSegment) throws UriValidationException {
    return isCollection(lastPathSegment) ? UriType.propertyComplexCollection : UriType.propertyComplex;
  }

  private UriType getUriTypeForAction(final UriResource lastPathSegment) throws UriValidationException {
    final EdmReturnType rt = ((UriResourceAction) lastPathSegment).getAction().getReturnType();
    if (rt == null) {
      return UriType.none;
    }
    UriType uriType;
    switch (rt.getType().getKind()) {
    case ENTITY:
      uriType = rt.isCollection() ? UriType.entitySet : UriType.entity;
      break;
    case PRIMITIVE:
    case ENUM:
    case DEFINITION:
      uriType = rt.isCollection() ? UriType.propertyPrimitiveCollection : UriType.propertyPrimitive;
      break;
    case COMPLEX:
      uriType = rt.isCollection() ? UriType.propertyComplexCollection : UriType.propertyComplex;
      break;
    default:
      throw new UriValidationException("Unsupported action return type: " + rt.getType().getKind(),
          UriValidationException.MessageKeys.UNSUPPORTED_ACTION_RETURN_TYPE, rt.getType().getKind().toString());
    }
    return uriType;
  }

  private UriType getUriTypeForCount(final UriResource secondLastPathSegment) throws UriValidationException {
    UriType uriType;
    switch (secondLastPathSegment.getKind()) {
    case entitySet:
    case navigationProperty:
      uriType = UriType.entitySetCount;
      break;
    case complexProperty:
      uriType = UriType.propertyComplexCollectionCount;
      break;
    case primitiveProperty:
      uriType = UriType.propertyPrimitiveCollectionCount;
      break;
    case function:
      final UriResourceFunction uriFunction = (UriResourceFunction) secondLastPathSegment;
      final EdmFunction function = uriFunction.getFunction();
      final EdmType returnType = function.getReturnType().getType();
      switch (returnType.getKind()) {
      case ENTITY:
        uriType = UriType.entitySetCount;
        break;
      case COMPLEX:
        uriType = UriType.propertyComplexCollectionCount;
        break;
      case PRIMITIVE:
      case ENUM:
      case DEFINITION:
        uriType = UriType.propertyPrimitiveCollectionCount;
        break;
      default:
        throw new UriValidationException("Unsupported return type: " + returnType.getKind(),
            UriValidationException.MessageKeys.UNSUPPORTED_FUNCTION_RETURN_TYPE, returnType.getKind().toString());
      }
      break;
    default:
      throw new UriValidationException("Illegal path part kind before $count: " + secondLastPathSegment.getKind(),
          UriValidationException.MessageKeys.UNALLOWED_KIND_BEFORE_COUNT, secondLastPathSegment.toString());
    }

    return uriType;
  }

  private void validateReadQueryOptions(final UriType uriType, final List<SystemQueryOption> options)
      throws UriValidationException {
    for (final SystemQueryOption option : options) {
      final SystemQueryOptionKind kind = option.getKind();
      if (OPTION_INDEX.containsKey(kind)) {
        final int columnIndex = OPTION_INDEX.get(kind);
        if (!decisionMatrix[uriType.getIndex()][columnIndex]) {
          throw new UriValidationException("System query option not allowed: " + option.getName(),
              UriValidationException.MessageKeys.SYSTEM_QUERY_OPTION_NOT_ALLOWED, option.getName());
        }
      } else {
        throw new UriValidationException("Unsupported option: " + kind,
            UriValidationException.MessageKeys.UNSUPPORTED_QUERY_OPTION, kind.toString());
      }
    }
  }

  private void validateNonReadQueryOptions(final UriType uriType, final boolean isAction,
      final List<SystemQueryOption> options, final HttpMethod httpMethod) throws UriValidationException {
    if (httpMethod == HttpMethod.POST && isAction) {
      // From the OData specification:
      // For POST requests to an action URL the return type of the action determines the applicable
      // system query options that a service MAY support, following the same rules as GET requests.
      validateReadQueryOptions(uriType, options);

    } else if (httpMethod == HttpMethod.DELETE && uriType == UriType.references) {
      // Only $id is allowed as SystemQueryOption for DELETE on a reference collection.
      for (final SystemQueryOption option : options) {
        if (SystemQueryOptionKind.ID != option.getKind()) {
          throw new UriValidationException(
              "System query option " + option.getName() + " is not allowed for method " + httpMethod,
              UriValidationException.MessageKeys.SYSTEM_QUERY_OPTION_NOT_ALLOWED_FOR_HTTP_METHOD,
              option.getName(), httpMethod.toString());
        }
      }

    } else if (!options.isEmpty() && !checkIfSelectOrExpand(options, httpMethod)) {
      StringBuilder optionsString = new StringBuilder();
      for (final SystemQueryOption option : options) {
        optionsString.append(option.getName()).append(' ');
      }
      throw new UriValidationException(
          "System query option(s) " + optionsString.toString() + "not allowed for method " + httpMethod,
          UriValidationException.MessageKeys.SYSTEM_QUERY_OPTION_NOT_ALLOWED_FOR_HTTP_METHOD,
          optionsString.toString(), httpMethod.toString());
    }
  }

  private boolean checkIfSelectOrExpand(List<SystemQueryOption> options, HttpMethod httpMethod) {
	boolean isSelectOrExpand = false;
	for (SystemQueryOption queryOption : options) {
		isSelectOrExpand = ((queryOption.getKind() == SystemQueryOptionKind.EXPAND) || 
				(queryOption.getKind() == SystemQueryOptionKind.SELECT)) &&
				(httpMethod == HttpMethod.PUT || httpMethod == HttpMethod.PATCH ||
				httpMethod == HttpMethod.POST);
	}
	return isSelectOrExpand;
}

private boolean isAction(final UriInfo uriInfo) {
    List<UriResource> uriResourceParts = uriInfo.getUriResourceParts();
    return !uriResourceParts.isEmpty()
        && UriResourceKind.action == uriResourceParts.get(uriResourceParts.size() - 1).getKind();
  }

  private void validatePropertyOperations(final UriInfo uriInfo, final HttpMethod method)
      throws UriValidationException {
    final List<UriResource> parts = uriInfo.getUriResourceParts();
    final UriResource last = !parts.isEmpty() ? parts.get(parts.size() - 1) : null;
    final UriResource previous = parts.size() > 1 ? parts.get(parts.size() - 2) : null;
    if (last != null
        && (last.getKind() == UriResourceKind.primitiveProperty
        || last.getKind() == UriResourceKind.complexProperty
        || (last.getKind() == UriResourceKind.value
            && previous != null && previous.getKind() == UriResourceKind.primitiveProperty))) {
      final EdmProperty property = ((UriResourceProperty)
          (last.getKind() == UriResourceKind.value ? previous : last)).getProperty();
      if (method == HttpMethod.PATCH && property.isCollection()) {
        throw new UriValidationException("Attempt to patch collection property.",
            UriValidationException.MessageKeys.UNSUPPORTED_HTTP_METHOD, method.toString());
      }
      if (method == HttpMethod.DELETE && !property.isNullable()) {
        throw new UriValidationException("Attempt to delete non-nullable property.",
            UriValidationException.MessageKeys.UNSUPPORTED_HTTP_METHOD, method.toString());
      }
    }
  }
}
