/*
 * Licensed to Julian Hyde under one or more contributor license
 * agreements.  See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Julian Hyde 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 net.hydromatic.morel.eval;

import net.hydromatic.morel.ast.Core;
import net.hydromatic.morel.ast.Pos;
import net.hydromatic.morel.util.ImmutablePairList;

import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;

import static net.hydromatic.morel.util.Pair.allMatch;
import static net.hydromatic.morel.util.Static.skip;

import static java.util.Objects.requireNonNull;

/** Value that is sufficient for a function to bind its argument
 * and evaluate its body. */
public class Closure implements Comparable<Closure>, Applicable {
  /** Environment for evaluation. Contains the variables "captured" from the
   * environment when the closure was created. */
  private final EvalEnv evalEnv;

  /** A list of (pattern, code) pairs. During bind, the value being bound is
   * matched against each pattern. When a match is found, the code for that
   * pattern is used to evaluate.
   *
   * <p>For example, when applying
   * {@code fn x => case x of 0 => "yes" | _ => "no"}
   * to the value {@code 1}, the first pattern ({@code 0} fails) but the second
   * pattern ({@code _}) succeeds, and therefore we evaluate the second
   * code {@code "no"}. */
  private final ImmutablePairList<Core.Pat, Code> patCodes;
  private final Pos pos;

  /** Not a public API. */
  public Closure(EvalEnv evalEnv,
      ImmutablePairList<Core.Pat, Code> patCodes, Pos pos) {
    this.evalEnv = requireNonNull(evalEnv).fix();
    this.patCodes = requireNonNull(patCodes);
    this.pos = pos;
  }

  @Override public String toString() {
    return "Closure(evalEnv = " + evalEnv + ", patCodes = " + patCodes + ")";
  }

  public int compareTo(Closure o) {
    return 0;
  }

  /** Binds an argument value to create a new environment for a closure.
   *
   * <p>When calling a simple function such as {@code (fn x => x + 1) 2},
   * the binder sets just contains one variable, {@code x}, and the
   * new environment contains {@code x = 1}.  If the function's
   * parameter is a match, more variables might be bound. For example,
   * when you invoke {@code (fn (x, y) => x + y) (3, 4)}, the binder
   * sets {@code x} to 3 and {@code y} to 4. */
  EvalEnv bind(Object argValue) {
    final EvalEnvHolder envRef = new EvalEnvHolder(evalEnv);
    for (Core.Pat pat : patCodes.leftList()) {
      if (bindRecurse(pat, argValue, envRef)) {
        return envRef.env;
      }
    }
    throw new AssertionError("no match");
  }

  /** Similar to {@link #bind}, but evaluates an expression first. */
  EvalEnv evalBind(EvalEnv env) {
    final EvalEnvHolder envRef = new EvalEnvHolder(env);
    for (Map.Entry<Core.Pat, Code> patCode : patCodes) {
      final Object argValue = patCode.getValue().eval(env);
      final Core.Pat pat = patCode.getKey();
      if (bindRecurse(pat, argValue, envRef)) {
        return envRef.env;
      }
    }
    throw new AssertionError("no match");
  }

  /** Similar to {@link #bind}, but also evaluates. */
  Object bindEval(Object argValue) {
    final EvalEnvHolder envRef = new EvalEnvHolder(evalEnv);
    for (Map.Entry<Core.Pat, Code> patCode : patCodes) {
      final Core.Pat pat = patCode.getKey();
      if (bindRecurse(pat, argValue, envRef)) {
        final Code code = patCode.getValue();
        return code.eval(envRef.env);
      }
    }
    throw new Codes.MorelRuntimeException(Codes.BuiltInExn.BIND, pos);
  }

  @Override public Object apply(EvalEnv env, Object argValue) {
    return bindEval(argValue);
  }

  @Override public Describer describe(Describer describer) {
    return describer.start("closure", d -> {});
  }

  /** Attempts to bind a value to a pattern. Returns whether it has succeeded in
   * matching the whole pattern.
   *
   * <p>Each time it matches a name, calls a consumer. It's possible that the
   * consumer is called a few times even if the whole pattern ultimately fails
   * to match. */
  public static boolean bindRecurse(Core.Pat pat, Object argValue,
      BiConsumer<Core.NamedPat, Object> envRef) {
    final List<Object> listValue;
    final Core.LiteralPat literalPat;
    switch (pat.op) {
    case ID_PAT:
      final Core.IdPat idPat = (Core.IdPat) pat;
      envRef.accept(idPat, argValue);
      return true;

    case WILDCARD_PAT:
      return true;

    case AS_PAT:
      final Core.AsPat asPat = (Core.AsPat) pat;
      envRef.accept(asPat, argValue);
      return bindRecurse(asPat.pat, argValue, envRef);

    case BOOL_LITERAL_PAT:
    case CHAR_LITERAL_PAT:
    case STRING_LITERAL_PAT:
      literalPat = (Core.LiteralPat) pat;
      return literalPat.value.equals(argValue);

    case INT_LITERAL_PAT:
      literalPat = (Core.LiteralPat) pat;
      return ((BigDecimal) literalPat.value).intValue() == (Integer) argValue;

    case REAL_LITERAL_PAT:
      literalPat = (Core.LiteralPat) pat;
      return ((BigDecimal) literalPat.value).doubleValue() == (Double) argValue;

    case TUPLE_PAT:
      final Core.TuplePat tuplePat = (Core.TuplePat) pat;
      listValue = (List) argValue;
      return allMatch(tuplePat.args, listValue,
          (pat1, value) -> bindRecurse(pat1, value, envRef));

    case RECORD_PAT:
      final Core.RecordPat recordPat = (Core.RecordPat) pat;
      listValue = (List) argValue;
      return allMatch(recordPat.args, listValue,
          (pat1, value) -> bindRecurse(pat1, value, envRef));

    case LIST_PAT:
      final Core.ListPat listPat = (Core.ListPat) pat;
      listValue = (List) argValue;
      if (listValue.size() != listPat.args.size()) {
        return false;
      }
      return allMatch(listPat.args, listValue,
          (pat1, value) -> bindRecurse(pat1, value, envRef));

    case CONS_PAT:
      final Core.ConPat consPat = (Core.ConPat) pat;
      @SuppressWarnings("unchecked") final List<Object> consValue =
          (List) argValue;
      if (consValue.isEmpty()) {
        return false;
      }
      final Object head = consValue.get(0);
      final List<Object> tail = skip(consValue);
      List<Core.Pat> patArgs = ((Core.TuplePat) consPat.pat).args;
      return bindRecurse(patArgs.get(0), head, envRef)
          && bindRecurse(patArgs.get(1), tail, envRef);

    case CON0_PAT:
      final Core.Con0Pat con0Pat = (Core.Con0Pat) pat;
      final List con0Value = (List) argValue;
      return con0Value.get(0).equals(con0Pat.tyCon);

    case CON_PAT:
      final Core.ConPat conPat = (Core.ConPat) pat;
      final List conValue = (List) argValue;
      return conValue.get(0).equals(conPat.tyCon)
          && bindRecurse(conPat.pat, conValue.get(1), envRef);

    default:
      throw new AssertionError("cannot compile " + pat.op + ": " + pat);
    }
  }

  /** Callback for {@link #bindRecurse(Core.Pat, Object, BiConsumer)} that
   * modifies an environment. */
  private static class EvalEnvHolder
      implements BiConsumer<Core.NamedPat, Object> {
    EvalEnv env;

    EvalEnvHolder(EvalEnv env) {
      this.env = env;
    }

    @Override public void accept(Core.NamedPat namedPat, Object o) {
      env = env.bind(namedPat.name, o);
    }
  }
}

// End Closure.java
