/*
 * 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.parser;

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

import org.apache.olingo.commons.api.edm.Edm;
import org.apache.olingo.commons.api.edm.EdmEntityType;
import org.apache.olingo.commons.api.edm.EdmStructuredType;
import org.apache.olingo.commons.api.edm.EdmType;
import org.apache.olingo.commons.api.ex.ODataRuntimeException;
import org.apache.olingo.server.api.OData;
import org.apache.olingo.server.api.uri.UriInfo;
import org.apache.olingo.server.api.uri.UriInfoKind;
import org.apache.olingo.server.api.uri.UriResource;
import org.apache.olingo.server.api.uri.UriResourceAction;
import org.apache.olingo.server.api.uri.UriResourceCount;
import org.apache.olingo.server.api.uri.UriResourceEntitySet;
import org.apache.olingo.server.api.uri.UriResourceFunction;
import org.apache.olingo.server.api.uri.UriResourcePartTyped;
import org.apache.olingo.server.api.uri.UriResourceRef;
import org.apache.olingo.server.api.uri.UriResourceValue;
import org.apache.olingo.server.api.uri.queryoption.AliasQueryOption;
import org.apache.olingo.server.api.uri.queryoption.ApplyItem;
import org.apache.olingo.server.api.uri.queryoption.ApplyOption;
import org.apache.olingo.server.api.uri.queryoption.ExpandItem;
import org.apache.olingo.server.api.uri.queryoption.ExpandOption;
import org.apache.olingo.server.api.uri.queryoption.FilterOption;
import org.apache.olingo.server.api.uri.queryoption.OrderByItem;
import org.apache.olingo.server.api.uri.queryoption.OrderByOption;
import org.apache.olingo.server.api.uri.queryoption.QueryOption;
import org.apache.olingo.server.api.uri.queryoption.SearchOption;
import org.apache.olingo.server.api.uri.queryoption.SelectOption;
import org.apache.olingo.server.api.uri.queryoption.SystemQueryOption;
import org.apache.olingo.server.api.uri.queryoption.SystemQueryOptionKind;
import org.apache.olingo.server.core.uri.UriInfoImpl;
import org.apache.olingo.server.core.uri.UriResourceStartingTypeFilterImpl;
import org.apache.olingo.server.core.uri.parser.UriTokenizer.TokenKind;
import org.apache.olingo.server.core.uri.parser.search.SearchParser;
import org.apache.olingo.server.core.uri.queryoption.AliasQueryOptionImpl;
import org.apache.olingo.server.core.uri.queryoption.ApplyOptionImpl;
import org.apache.olingo.server.core.uri.queryoption.CountOptionImpl;
import org.apache.olingo.server.core.uri.queryoption.DeltaTokenOptionImpl;
import org.apache.olingo.server.core.uri.queryoption.ExpandOptionImpl;
import org.apache.olingo.server.core.uri.queryoption.FilterOptionImpl;
import org.apache.olingo.server.core.uri.queryoption.FormatOptionImpl;
import org.apache.olingo.server.core.uri.queryoption.IdOptionImpl;
import org.apache.olingo.server.core.uri.queryoption.OrderByOptionImpl;
import org.apache.olingo.server.core.uri.queryoption.SearchOptionImpl;
import org.apache.olingo.server.core.uri.queryoption.SelectOptionImpl;
import org.apache.olingo.server.core.uri.queryoption.SkipOptionImpl;
import org.apache.olingo.server.core.uri.queryoption.SkipTokenOptionImpl;
import org.apache.olingo.server.core.uri.queryoption.SystemQueryOptionImpl;
import org.apache.olingo.server.core.uri.queryoption.TopOptionImpl;
import org.apache.olingo.server.core.uri.queryoption.apply.DynamicStructuredType;
import org.apache.olingo.server.core.uri.validator.UriValidationException;

public class Parser {
  private static final String ATOM = "atom";
  private static final String JSON = "json";
  private static final String XML = "xml";
  private static final String DOLLAR = "$";
  private static final String AT = "@";
  private static final String NULL = "null";
  private static final String ENTITY = "$entity";
  private static final String HTTP = "http";

  private final Edm edm;
  private final OData odata;

  public Parser(final Edm edm, final OData odata) {
    this.edm = edm;
    this.odata = odata;
  }

  public UriInfo parseUri(final String path, final String query, final String fragment, String baseUri)
      throws UriParserException, UriValidationException {

    UriInfoImpl contextUriInfo = new UriInfoImpl();
   
    // Read the query options (system and custom options).
    // This is done before parsing the resource path because the aliases have to be available there.
    // System query options that can only be parsed with context from the resource path will be post-processed later.
    final List<QueryOption> options =
        query == null ? Collections.<QueryOption> emptyList() : UriDecoder.splitAndDecodeOptions(query);
    for (final QueryOption option : options) {
      final String optionName = option.getName();
      String value = option.getText();
      if(UriDecoder.isFormEncoding()){
        value = getFormEncodedValue(value);
      }
      // Parse the untyped option and retrieve a system-option or alias-option instance (or null for a custom option).
      final QueryOption parsedOption = parseOption(optionName, value);
      try {
        contextUriInfo.setQueryOption(parsedOption == null ? option : parsedOption);
      } catch (final ODataRuntimeException e) {
        throw new UriParserSyntaxException(
            parsedOption instanceof SystemQueryOption ?
                "Double system query option!" :
                "Alias already specified! Name: " + optionName,
            e,
            parsedOption instanceof SystemQueryOption ?
                UriParserSyntaxException.MessageKeys.DOUBLE_SYSTEM_QUERY_OPTION :
                UriParserSyntaxException.MessageKeys.DUPLICATED_ALIAS,
            optionName);
      }
    }

    // Read the decoded path segments.
    EdmType contextType = null;
    boolean contextIsCollection = false;

    List<String> pathSegmentsDecoded = UriDecoder.splitAndDecodePath(path);
    int numberOfSegments = pathSegmentsDecoded.size();
    // Remove an initial empty segment resulting from the expected '/' at the beginning of the path.
    if (numberOfSegments > 1 && pathSegmentsDecoded.get(0).isEmpty()) {
      pathSegmentsDecoded.remove(0);
      numberOfSegments--;
    }

    final String firstSegment = pathSegmentsDecoded.get(0);

    if (firstSegment.isEmpty()) {
      ensureLastSegment(firstSegment, 1, numberOfSegments);
      contextUriInfo.setKind(UriInfoKind.service);

    } else if ("$batch".equals(firstSegment)) {
      ensureLastSegment(firstSegment, 1, numberOfSegments);
      contextUriInfo.setKind(UriInfoKind.batch);

    } else if ("$metadata".equals(firstSegment)) {
      ensureLastSegment(firstSegment, 1, numberOfSegments);
      contextUriInfo.setKind(UriInfoKind.metadata);
      contextUriInfo.setFragment(fragment);

    } else if ("$all".equals(firstSegment)) {
      ensureLastSegment(firstSegment, 1, numberOfSegments);
      contextUriInfo.setKind(UriInfoKind.all);
      contextIsCollection = true;

    } else if ("$entity".equals(firstSegment)) {
      if (null != contextUriInfo.getIdOption()) {
        String idOptionText = contextUriInfo.getIdOption().getText();
        if (idOptionText.startsWith(HTTP)) {
          baseUri = UriDecoder.decode(baseUri);
          if (idOptionText.contains(baseUri)) {
            idOptionText = idOptionText.substring(baseUri.length() + 1);
          } else {
            throw new UriParserSemanticException("$id cannot have an absolute path",
                UriParserSemanticException.MessageKeys.NOT_IMPLEMENTED_SYSTEM_QUERY_OPTION);
          }
        }
        if (numberOfSegments > 1) {
          /**
           * If url is of the form 
           * http://localhost:8080/odata-server-tecsvc/odata.svc/$entity/
           * olingo.odata.test1.ETAllPrim?$id=ESAllPrim(32767)
           */
          final ResourcePathParser resourcePathParser = new ResourcePathParser
            (edm, contextUriInfo.getAliasMap());
          String typeCastSegment = pathSegmentsDecoded.get(1);
          ensureLastSegment(typeCastSegment, 2, numberOfSegments);
          contextType = resourcePathParser.parseDollarEntityTypeCast(typeCastSegment);
          contextUriInfo = (UriInfoImpl) new Parser(edm, odata).
              parseUri("/" + idOptionText, query, fragment, baseUri);
          contextUriInfo.setEntityTypeCast((EdmEntityType) contextType);
        } else if (numberOfSegments == 1) {
          /**
           * If url is of the form 
           * http://localhost:8080/odata-server-tecsvc/odata.svc/$entity?$id=ESAllPrim(32527)
           */
          contextUriInfo = (UriInfoImpl) new Parser(edm, odata).
                  parseUri("/" + idOptionText, query, fragment, baseUri);
        }
        contextType = contextUriInfo.getEntityTypeCast();
        contextUriInfo.setKind(UriInfoKind.entityId);
        contextIsCollection = false;
      } else {
        /**
         * If url is of the form 
         * http://localhost:8080/odata-server-tecsvc/odata.svc/$entity/olingo.odata.test1.ETKeyNav/$ref
         */
        ensureLastSegment(firstSegment, 2, numberOfSegments);
        /**
         * If url is of the form 
         * http://localhost:8080/odata-server-tecsvc/odata.svc/$entity/olingo.odata.test1.ETKeyNav
         */
        throw new UriParserSyntaxException("The entity-id MUST be specified using the system query option $id",
                  UriParserSyntaxException.MessageKeys.ENTITYID_MISSING_SYSTEM_QUERY_OPTION_ID);
      }
    } else if (firstSegment.startsWith("$crossjoin")) {
      ensureLastSegment(firstSegment, 1, numberOfSegments);
      contextUriInfo.setKind(UriInfoKind.crossjoin);
      final List<String> entitySetNames = new ResourcePathParser(edm, contextUriInfo.getAliasMap())
          .parseCrossjoinSegment(firstSegment);
      for (final String name : entitySetNames) {
        contextUriInfo.addEntitySetName(name);
      }
      contextIsCollection = true;

    } else {
      contextUriInfo.setKind(UriInfoKind.resource);
      final ResourcePathParser resourcePathParser = new ResourcePathParser(edm, contextUriInfo.getAliasMap());
      int count = 0;
      UriResource lastSegment = null;
      for (final String pathSegment : pathSegmentsDecoded) {
        count++;
        if (pathSegment.startsWith(ENTITY)) {
          /**
           * If url is of the form 
           * http://localhost:8080/odata-server-tecsvc/odata.svc/ESAllPrim/$entity
           */
          throw new UriParserSyntaxException("The entity-id MUST be specified using the system query option $id",
                    UriParserSyntaxException.MessageKeys.ENTITYID_MISSING_SYSTEM_QUERY_OPTION_ID);
        } else {
          final UriResource segment = resourcePathParser.parsePathSegment(pathSegment, lastSegment);
          if (segment != null) {
            if (segment instanceof UriResourceCount
                || segment instanceof UriResourceRef
                || segment instanceof UriResourceValue) {
              ensureLastSegment(pathSegment, count, numberOfSegments);
            } else if (segment instanceof UriResourceAction
                || segment instanceof UriResourceFunction
                && !((UriResourceFunction) segment).getFunction().isComposable()) {
              if (count < numberOfSegments) {
                throw new UriValidationException(
                    "The segment of an action or of a non-composable function must be the last resource-path segment.",
                    UriValidationException.MessageKeys.UNALLOWED_RESOURCE_PATH,
                    pathSegmentsDecoded.get(count));
              }
              lastSegment = segment;
            } else if (segment instanceof UriResourceStartingTypeFilterImpl) {
              throw new UriParserSemanticException("First resource-path segment must not be namespace-qualified.",
                  UriParserSemanticException.MessageKeys.NAMESPACE_NOT_ALLOWED_AT_FIRST_ELEMENT);
            } else {
              lastSegment = segment;
            }
            contextUriInfo.addResourcePart(segment);
          }
        }
      }

      if (lastSegment instanceof UriResourcePartTyped) {
        final UriResourcePartTyped typed = (UriResourcePartTyped) lastSegment;
        contextType = ParserHelper.getTypeInformation(typed);
        if (contextType != null && ((lastSegment instanceof UriResourceEntitySet &&
            (((UriResourceEntitySet) lastSegment).getTypeFilterOnCollection() != null
                || ((UriResourceEntitySet) lastSegment).getTypeFilterOnEntry() != null))
            || contextUriInfo.getIdOption() != null) && contextType instanceof EdmEntityType) {
          contextUriInfo.setEntityTypeCast((EdmEntityType) contextType);
        }
        contextIsCollection = typed.isCollection();
      }
    }

    // Post-process system query options that need context information from the resource path.
    if (contextType instanceof EdmStructuredType && contextUriInfo.getApplyOption() != null) {
      // Data aggregation may change the structure of the result.
      contextType = new DynamicStructuredType((EdmStructuredType) contextType);
    }
    parseApplyOption(contextUriInfo.getApplyOption(), contextType,
        contextUriInfo.getEntitySetNames(), contextUriInfo.getAliasMap());
    parseFilterOption(contextUriInfo.getFilterOption(), contextType,
        contextUriInfo.getEntitySetNames(), contextUriInfo.getAliasMap());
    parseOrderByOption(contextUriInfo.getOrderByOption(), contextType,
        contextUriInfo.getEntitySetNames(), contextUriInfo.getAliasMap());
    parseExpandOption(contextUriInfo.getExpandOption(), contextType,
        contextUriInfo.getKind() == UriInfoKind.all, contextUriInfo.getEntitySetNames(),
        contextUriInfo.getAliasMap());
    parseSelectOption(contextUriInfo.getSelectOption(), contextType, contextIsCollection);

    return contextUriInfo;
  }

  private String getFormEncodedValue(String value) {
    if(value.contains("+")){
      value = value.replaceAll("\\+", " ");
    }
    return value;    
  }

  private QueryOption parseOption(final String optionName, final String optionValue)
      throws UriParserException, UriValidationException {
    if (optionName.startsWith(DOLLAR)) {
      final SystemQueryOptionKind kind = SystemQueryOptionKind.get(optionName);
      if (kind == null) {
        throw new UriParserSyntaxException("Unknown system query option!",
            UriParserSyntaxException.MessageKeys.UNKNOWN_SYSTEM_QUERY_OPTION, optionName);
      }
      SystemQueryOptionImpl systemOption;
      switch (kind) {
      case SEARCH:
        SearchOption searchOption = new SearchParser().parse(optionValue);
        SearchOptionImpl tmp = new SearchOptionImpl();
        tmp.setSearchExpression(searchOption.getSearchExpression());
        systemOption = tmp;
        break;
      case FILTER:
        systemOption = new FilterOptionImpl();
        break;
      case COUNT:
        if ("true".equals(optionValue) || "false".equals(optionValue)) {
          systemOption = new CountOptionImpl().setValue(Boolean.parseBoolean(optionValue));
        } else {
          throw new UriParserSyntaxException("Illegal value of $count option!",
              UriParserSyntaxException.MessageKeys.WRONG_VALUE_FOR_SYSTEM_QUERY_OPTION,
              optionName, optionValue);
        }
        break;
      case ORDERBY:
        systemOption = new OrderByOptionImpl();
        break;
      case SKIP:
        systemOption = new SkipOptionImpl()
            .setValue(ParserHelper.parseNonNegativeInteger(optionName, optionValue, true));
        break;
      case SKIPTOKEN:
        if (optionValue.isEmpty()) {
          throw new UriParserSyntaxException("Illegal value of $skiptoken option!",
              UriParserSyntaxException.MessageKeys.WRONG_VALUE_FOR_SYSTEM_QUERY_OPTION,
              optionName, optionValue);
        }
        systemOption = new SkipTokenOptionImpl().setValue(optionValue);
        break;
      case DELTATOKEN:
        if (optionValue.isEmpty()) {
          throw new UriParserSyntaxException("Illegal value of $deltatoken option!",
              UriParserSyntaxException.MessageKeys.WRONG_VALUE_FOR_SYSTEM_QUERY_OPTION,
              optionName, optionValue);
        }
        systemOption = new DeltaTokenOptionImpl().setValue(optionValue);
        break;
      case TOP:
        systemOption = new TopOptionImpl()
            .setValue(ParserHelper.parseNonNegativeInteger(optionName, optionValue, true));
        break;
      case EXPAND:
        systemOption = new ExpandOptionImpl();
        break;
      case SELECT:
        systemOption = new SelectOptionImpl();
        break;
      case FORMAT:
        if (optionValue.equalsIgnoreCase(JSON)
            || optionValue.equalsIgnoreCase(XML)
            || optionValue.equalsIgnoreCase(ATOM)
            || isFormatSyntaxValid(optionValue)) {
          systemOption = new FormatOptionImpl().setFormat(optionValue);
        } else {
          throw new UriParserSyntaxException("Illegal value of $format option!",
              UriParserSyntaxException.MessageKeys.WRONG_VALUE_FOR_SYSTEM_QUERY_OPTION_FORMAT, optionValue);
        }
        break;
      case ID:
        if (optionValue.isEmpty()) {
          throw new UriParserSyntaxException("Illegal value of $id option!",
              UriParserSyntaxException.MessageKeys.WRONG_VALUE_FOR_SYSTEM_QUERY_OPTION,
              optionName, optionValue);
        }
        systemOption = new IdOptionImpl().setValue(optionValue);
        break;
      case LEVELS:
        throw new UriParserSyntaxException("System query option '$levels' is allowed only inside '$expand'!",
            UriParserSyntaxException.MessageKeys.SYSTEM_QUERY_OPTION_LEVELS_NOT_ALLOWED_HERE);
      case APPLY:
        systemOption = new ApplyOptionImpl();
        break;
      default:
          throw new UriParserSyntaxException("System query option '" + kind + "' is not known!",
              UriParserSyntaxException.MessageKeys.UNKNOWN_SYSTEM_QUERY_OPTION, optionName);
      }
      systemOption.setText(optionValue);
      return systemOption;

    } else if (optionName.startsWith(AT)) {
      // Aliases can only be parsed in the context of their usage, so the value is not checked here.
      return new AliasQueryOptionImpl()
          .setName(optionName)
          .setText(NULL.equals(optionValue) ? null : optionValue);

    } else {
      // The option is a custom query option; the caller can re-use its query option.
      return null;
    }
  }

  private void parseFilterOption(FilterOption filterOption, final EdmType contextType,
      final List<String> entitySetNames, final Map<String, AliasQueryOption> aliases)
      throws UriParserException, UriValidationException {
    if (filterOption != null) {
      final String optionValue = filterOption.getText();
      UriTokenizer filterTokenizer = new UriTokenizer(optionValue);
      // The referring type could be a primitive type or a structured type.
      ((FilterOptionImpl) filterOption).setExpression(
          new FilterParser(edm, odata).parse(filterTokenizer, contextType, entitySetNames, aliases)
              .getExpression());
      checkOptionEOF(filterTokenizer, filterOption.getName(), optionValue);
    }
  }

  private void parseOrderByOption(OrderByOption orderByOption, final EdmType contextType,
      final List<String> entitySetNames, final Map<String, AliasQueryOption> aliases)
      throws UriParserException, UriValidationException {
    if (orderByOption != null) {
      final String optionValue = orderByOption.getText();
      UriTokenizer orderByTokenizer = new UriTokenizer(optionValue);
      final OrderByOption option = new OrderByParser(edm, odata).parse(orderByTokenizer,
          contextType instanceof EdmStructuredType ? (EdmStructuredType) contextType : null,
          entitySetNames,
          aliases);
      checkOptionEOF(orderByTokenizer, orderByOption.getName(), optionValue);
      for (final OrderByItem item : option.getOrders()) {
        ((OrderByOptionImpl) orderByOption).addOrder(item);
      }
    }
  }

  private void parseExpandOption(ExpandOption expandOption, final EdmType contextType, final boolean isAll,
      final List<String> entitySetNames, final Map<String, AliasQueryOption> aliases)
      throws UriParserException, UriValidationException {
    if (expandOption != null) {
      if (!(contextType instanceof EdmStructuredType || isAll
      || (entitySetNames != null && !entitySetNames.isEmpty()))) {
        throw new UriValidationException("Expand is only allowed on structured types!",
            UriValidationException.MessageKeys.SYSTEM_QUERY_OPTION_NOT_ALLOWED, expandOption.getName());
      }
      final String optionValue = expandOption.getText();
      UriTokenizer expandTokenizer = new UriTokenizer(optionValue);
      final ExpandOption option = new ExpandParser(edm, odata, aliases, entitySetNames).parse(expandTokenizer,
          contextType instanceof EdmStructuredType ? (EdmStructuredType) contextType : null);
      checkOptionEOF(expandTokenizer, expandOption.getName(), optionValue);
      for (final ExpandItem item : option.getExpandItems()) {
        ((ExpandOptionImpl) expandOption).addExpandItem(item);
      }
    }
  }

  private void parseSelectOption(SelectOption selectOption, final EdmType contextType,
      final boolean contextIsCollection) throws UriParserException, UriValidationException {
    if (selectOption != null) {
      final String optionValue = selectOption.getText();
      UriTokenizer selectTokenizer = new UriTokenizer(optionValue);
      ((SelectOptionImpl) selectOption).setSelectItems(
          new SelectParser(edm).parse(selectTokenizer,
              contextType instanceof EdmStructuredType ? (EdmStructuredType) contextType : null,
              contextIsCollection)
              .getSelectItems());
      checkOptionEOF(selectTokenizer, selectOption.getName(), optionValue);
    }
  }

  private void parseApplyOption(ApplyOption applyOption, EdmType contextType,
      final List<String> entitySetNames, final Map<String, AliasQueryOption> aliases)
      throws UriParserException, UriValidationException {
    if (applyOption != null) {
      final String optionValue = applyOption.getText();
      UriTokenizer applyTokenizer = new UriTokenizer(optionValue);
      final ApplyOption option = new ApplyParser(edm, odata).parse(applyTokenizer,
          contextType instanceof EdmStructuredType ? (EdmStructuredType) contextType : null,
          entitySetNames,
          aliases);
      checkOptionEOF(applyTokenizer, applyOption.getName(), optionValue);
      for (final ApplyItem item : option.getApplyItems()) {
        ((ApplyOptionImpl) applyOption).add(item);
      }
      ((ApplyOptionImpl) applyOption).setEdmStructuredType(option.getEdmStructuredType());
    }
  }

  private void ensureLastSegment(final String segment, final int pos, final int size)
      throws UriParserSyntaxException {
    if (pos < size) {
      throw new UriParserSyntaxException(segment + " must be the last segment.",
          UriParserSyntaxException.MessageKeys.MUST_BE_LAST_SEGMENT, segment);
    }
  }

  private boolean isFormatSyntaxValid(final String value) {
    final int index = value.indexOf('/');
    return index > 0 && index < value.length() - 1 && index == value.lastIndexOf('/');
  }

  private void checkOptionEOF(UriTokenizer tokenizer, final String optionName, final String optionValue)
      throws UriParserException {
    if (!tokenizer.next(TokenKind.EOF)) {
      throw new UriParserSyntaxException("Illegal value of '" + optionName + "' option!",
          UriParserSyntaxException.MessageKeys.WRONG_VALUE_FOR_SYSTEM_QUERY_OPTION,
          optionName, optionValue);
    }
  }
}
