/**
 * Copyright (c) 2014 SQUARESPACE, Inc.
 *
 * Licensed 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 com.squarespace.template.plugins;

import static com.squarespace.template.GeneralUtils.isTruthy;

import java.util.ArrayList;
import java.util.List;

import com.fasterxml.jackson.databind.JsonNode;
import com.squarespace.template.Arguments;
import com.squarespace.template.ArgumentsException;
import com.squarespace.template.BasePredicate;
import com.squarespace.template.CodeExecuteException;
import com.squarespace.template.Context;
import com.squarespace.template.GeneralUtils;
import com.squarespace.template.JsonUtils;
import com.squarespace.template.Patterns;
import com.squarespace.template.Predicate;
import com.squarespace.template.PredicateRegistry;
import com.squarespace.template.ReferenceScanner.References;
import com.squarespace.template.ReprEmitter;
import com.squarespace.template.StringView;
import com.squarespace.template.SymbolTable;
import com.squarespace.template.VariableRef;


public class CorePredicates implements PredicateRegistry {

  /**
   * Registers the active predicates in this registry.
   */
  @Override
  public void registerPredicates(SymbolTable<StringView, Predicate> table) {
    table.add(DEBUG);
    table.add(EQUAL);
    table.add(EVEN);
    table.add(GREATER_THAN);
    table.add(GREATER_THAN_OR_EQUAL);
    table.add(LESS_THAN);
    table.add(LESS_THAN_OR_EQUAL);
    table.add(NOT_EQUAL);
    table.add(NTH);
    table.add(ODD);
    table.add(PLURAL);
    table.add(SINGULAR);
  };

  public static final Predicate DEBUG = new BasePredicate("debug?", false) {

    @Override
    public boolean apply(Context ctx, Arguments args) throws CodeExecuteException {
      return isTruthy(ctx.resolve("debug"));
    }

  };

  /**
   * Class of predicates that take 1 argument which is either (a) a JSON value or
   * (b) a variable reference.
   */
  private static abstract class JsonPredicate extends BasePredicate {

    JsonPredicate(String identifier) {
      super(identifier, true);
    }

    JsonPredicate(String identifier, boolean requiresArgs) {
      super(identifier, requiresArgs);
    }

    public abstract void limitArgs(Arguments args) throws ArgumentsException;

    @Override
    public void addReferences(Arguments args, References refs) {
      addVariableNames(args, refs);
    }

    @Override
    public void validateArgs(Arguments args) throws ArgumentsException {
      limitArgs(args);
      List<Object> parsed = new ArrayList<>();
      for (int i = 0; i < args.count(); i++) {
        parsed.add(parse(args, i));
      }
      args.setOpaque(parsed);
    };

    private Object parse(Arguments args, int index) throws ArgumentsException {
      String raw = args.get(index);
      // Peek at content to see if its JSON-like. This will cut down on the
      // number of failed JSON parse attempts.
      if (GeneralUtils.isJsonStart(raw)) {
        JsonNode result = JsonUtils.decode(raw, true);
        if (!result.isMissingNode()) {
          return result;
        }
      }

      // Attempt to parse variable name.
      int length = raw.length();
      if (Patterns.VARIABLE_REF_DOTTED.match(raw, 0, length) != -1) {
        return new VariableRef(raw);
      }

      throw new ArgumentsException("Argument " + raw + " must be a valid JSON value or variable reference.");
    }

    @SuppressWarnings("unchecked")
    protected JsonNode resolve(Context ctx, Arguments args, int index) {
      List<Object> parsed = (List<Object>) args.getOpaque();
      Object arg = parsed.get(index);
      if (arg instanceof VariableRef) {
        VariableRef ref = (VariableRef)arg;
        return ctx.resolve(ref.reference());
      }
      return (JsonNode)arg;
    }

    @SuppressWarnings("unchecked")
    private void addVariableNames(Arguments args, References refs) {
      List<Object> parsed = (List<Object>) args.getOpaque();
      for (Object arg : parsed) {
        if (arg instanceof VariableRef) {
          String name = ReprEmitter.get(((VariableRef)arg).reference());
          refs.addVariable(name);
        }
      }
    }

  }

  public static final Predicate EQUAL = new JsonPredicate("equal?") {

    @Override
    public void limitArgs(Arguments args) throws ArgumentsException {
      args.between(1, 2);
    }

    @Override
    public boolean apply(Context ctx, Arguments args) throws CodeExecuteException {
      JsonNode arg0 = resolve(ctx, args, 0);
      if (args.count() == 1) {
        return ctx.node().equals(arg0);
      } else {
        return arg0.equals(resolve(ctx, args, 1));
      }
    }

  };


  public static final Predicate EVEN = new JsonPredicate("even?", false) {

    @Override
    public void limitArgs(Arguments args) throws ArgumentsException {
      args.atMost(1);
    }

    @Override
    public boolean apply(Context ctx, Arguments args) throws CodeExecuteException {
      JsonNode node = ctx.node();
      if (args.count() == 1) {
        node = resolve(ctx, args, 0);
      }
      if (node.isIntegralNumber()) {
        return (node.asLong() % 2) == 0;
      }
      return false;
    }

  };


  public static final Predicate GREATER_THAN = new JsonPredicate("greaterThan?") {

    @Override
    public void limitArgs(Arguments args) throws ArgumentsException {
      args.between(1, 2);
    }

    @Override
    public boolean apply(Context ctx, Arguments args) throws CodeExecuteException {
      JsonNode arg0 = resolve(ctx, args, 0);
      if (args.count() == 1) {
        return JsonUtils.compare(ctx.node(), arg0) > 0;
      }
      return JsonUtils.compare(arg0, resolve(ctx, args, 1)) > 0;
    }

  };


  public static final Predicate GREATER_THAN_OR_EQUAL = new JsonPredicate("greaterThanOrEqual?") {

    @Override
    public void limitArgs(Arguments args) throws ArgumentsException {
      args.between(1, 2);
    }

    @Override
    public boolean apply(Context ctx, Arguments args) throws CodeExecuteException {
      JsonNode arg0 = resolve(ctx, args, 0);
      if (args.count() == 1) {
        return JsonUtils.compare(ctx.node(), arg0) >= 0;
      }
      return JsonUtils.compare(arg0, resolve(ctx, args, 1)) >= 0;
    }

  };


  public static final Predicate LESS_THAN = new JsonPredicate("lessThan?") {

    @Override
    public void limitArgs(Arguments args) throws ArgumentsException {
      args.between(1, 2);
    }

    @Override
    public boolean apply(Context ctx, Arguments args) throws CodeExecuteException {
      JsonNode arg0 = resolve(ctx, args, 0);
      if (args.count() == 1) {
        return JsonUtils.compare(ctx.node(), arg0) < 0;
      }
      return JsonUtils.compare(arg0, resolve(ctx, args, 1)) < 0;
    }

  };


  public static final Predicate LESS_THAN_OR_EQUAL = new JsonPredicate("lessThanOrEqual?") {

    @Override
    public void limitArgs(Arguments args) throws ArgumentsException {
      args.between(1, 2);
    }

    @Override
    public boolean apply(Context ctx, Arguments args) throws CodeExecuteException {
      JsonNode arg0 = resolve(ctx, args, 0);
      if (args.count() == 1) {
        return JsonUtils.compare(ctx.node(), arg0) <= 0;
      }
      return JsonUtils.compare(arg0, resolve(ctx, args, 1)) <= 0;
    }

  };


  public static final Predicate NOT_EQUAL = new JsonPredicate("notEqual?") {

    @Override
    public void limitArgs(Arguments args) throws ArgumentsException {
      args.between(1, 2);
    }

    @Override
    public boolean apply(Context ctx, Arguments args) throws CodeExecuteException {
      JsonNode arg0 = resolve(ctx, args, 0);
      if (args.count() == 1) {
        return !ctx.node().equals(arg0);
      } else {
        return !arg0.equals(resolve(ctx, args, 1));
      }
    }

  };

  public static final Predicate NTH = new JsonPredicate("nth?", false) {

    @Override
    public void limitArgs(Arguments args) throws ArgumentsException {
      args.between(1, 2);
    }

    @Override
    public boolean apply(Context ctx, Arguments args) throws CodeExecuteException {
      JsonNode node = ctx.node();
      JsonNode modulus = resolve(ctx, args, 0);
      if (args.count() == 2) {
        node = modulus;
        modulus = resolve(ctx, args, 1);
      }
      if (node.isIntegralNumber() && modulus.isIntegralNumber()) {
        return node.asLong() % modulus.asLong() == 0;
      }
      return false;
    }

  };


  public static final Predicate ODD = new JsonPredicate("odd?", false) {

    @Override
    public void limitArgs(Arguments args) throws ArgumentsException {
      args.atMost(1);
    }

    @Override
    public boolean apply(Context ctx, Arguments args) throws CodeExecuteException {
      JsonNode node = ctx.node();
      if (args.count() == 1) {
        node = resolve(ctx, args, 0);
      }
      if (node.isIntegralNumber()) {
        return (node.asLong() % 2) != 0;
      }
      return false;
    }

  };


  public static final Predicate PLURAL = new BasePredicate("plural?", false) {

    @Override
    public boolean apply(Context ctx, Arguments args) throws CodeExecuteException {
      return ctx.node().asLong() > 1;
    }

  };


  public static final Predicate SINGULAR = new BasePredicate("singular?", false) {

    @Override
    public boolean apply(Context ctx, Arguments args) throws CodeExecuteException {
      return ctx.node().asLong() == 1;
    }

  };

}