/*
 * 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.lucene.spatial.query;

import java.io.IOException;
import java.text.ParseException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.StringTokenizer;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.exception.InvalidShapeException;
import org.locationtech.spatial4j.shape.Shape;

/**
 * Parses a string that usually looks like "OPERATION(SHAPE)" into a {@link SpatialArgs} object. The
 * set of operations supported are defined in {@link SpatialOperation}, such as "Intersects" being a
 * common one. The shape portion is defined by WKT {@link org.locationtech.spatial4j.io.WKTReader},
 * but it can be overridden/customized via {@link #parseShape(String,
 * org.locationtech.spatial4j.context.SpatialContext)}. There are some optional name-value pair
 * parameters that follow the closing parenthesis. Example:
 *
 * <pre>
 *   Intersects(ENVELOPE(-10,-8,22,20)) distErrPct=0.025
 * </pre>
 *
 * <p>In the future it would be good to support something at least semi-standardized like a variant
 * of <a
 * href="http://docs.geoserver.org/latest/en/user/filter/ecql_reference.html#spatial-predicate">
 * [E]CQL</a>.
 *
 * @lucene.experimental
 */
public class SpatialArgsParser {

  public static final String DIST_ERR_PCT = "distErrPct";
  public static final String DIST_ERR = "distErr";

  /** Writes a close approximation to the parsed input format. */
  static String writeSpatialArgs(SpatialArgs args) {
    StringBuilder str = new StringBuilder();
    str.append(args.getOperation().getName());
    str.append('(');
    str.append(args.getShape().toString());
    if (args.getDistErrPct() != null)
      str.append(" distErrPct=")
          .append(String.format(Locale.ROOT, "%.2f%%", args.getDistErrPct() * 100d));
    if (args.getDistErr() != null) str.append(" distErr=").append(args.getDistErr());
    str.append(')');
    return str.toString();
  }

  /**
   * Parses a string such as "Intersects(ENVELOPE(-10,-8,22,20)) distErrPct=0.025".
   *
   * @param v The string to parse. Mandatory.
   * @param ctx The spatial context. Mandatory.
   * @return Not null.
   * @throws IllegalArgumentException if the parameters don't make sense or an add-on parameter is
   *     unknown
   * @throws ParseException If there is a problem parsing the string
   * @throws InvalidShapeException When the coordinates are invalid for the shape
   */
  public SpatialArgs parse(String v, SpatialContext ctx)
      throws ParseException, InvalidShapeException {
    int idx = v.indexOf('(');
    int edx = v.lastIndexOf(')');

    if (idx < 0 || idx > edx) {
      throw new ParseException("missing parens: " + v, -1);
    }

    SpatialOperation op = SpatialOperation.get(v.substring(0, idx).trim());

    String body = v.substring(idx + 1, edx).trim();
    if (body.length() < 1) {
      throw new ParseException("missing body : " + v, idx + 1);
    }

    Shape shape = parseShape(body, ctx);
    SpatialArgs args = newSpatialArgs(op, shape);

    if (v.length() > (edx + 1)) {
      body = v.substring(edx + 1).trim();
      if (body.length() > 0) {
        Map<String, String> aa = parseMap(body);
        readNameValuePairs(args, aa);
        if (!aa.isEmpty()) {
          throw new IllegalArgumentException("unused parameters: " + aa);
        }
      }
    }
    args.validate();
    return args;
  }

  protected SpatialArgs newSpatialArgs(SpatialOperation op, Shape shape) {
    return new SpatialArgs(op, shape);
  }

  protected void readNameValuePairs(SpatialArgs args, Map<String, String> nameValPairs) {
    args.setDistErrPct(readDouble(nameValPairs.remove(DIST_ERR_PCT)));
    args.setDistErr(readDouble(nameValPairs.remove(DIST_ERR)));
  }

  protected Shape parseShape(String str, SpatialContext ctx) throws ParseException {
    try {
      return ctx.getFormats().getWktReader().read(str);
    } catch (IOException e) {
      throw new ParseException(e.toString(), 0); // impossible but whatever;
    }
  }

  protected static Double readDouble(String v) {
    return v == null ? null : Double.valueOf(v);
  }

  protected static boolean readBool(String v, boolean defaultValue) {
    return v == null ? defaultValue : Boolean.parseBoolean(v);
  }

  /**
   * Parses "a=b zScaling=d f" (whitespace separated) into name-value pairs. If there is no '=' as
   * in 'f' above then it's short for f=f.
   */
  protected static Map<String, String> parseMap(String body) {
    Map<String, String> map = new HashMap<>();
    StringTokenizer st = new StringTokenizer(body, " \n\t");
    while (st.hasMoreTokens()) {
      String a = st.nextToken();
      int idx = a.indexOf('=');
      if (idx > 0) {
        String k = a.substring(0, idx);
        String v = a.substring(idx + 1);
        map.put(k, v);
      } else {
        map.put(a, a);
      }
    }
    return map;
  }
}
