/*
 * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
 *
 * 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
 *
 *     https://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.pkl.core.stdlib.base;

import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.nodes.IndirectCallNode;
import org.pkl.core.ast.lambda.ApplyVmFunction1Node;
import org.pkl.core.ast.lambda.ApplyVmFunction2Node;
import org.pkl.core.ast.lambda.ApplyVmFunction2NodeGen;
import org.pkl.core.ast.lambda.ApplyVmFunction3Node;
import org.pkl.core.ast.lambda.ApplyVmFunction3NodeGen;
import org.pkl.core.runtime.*;
import org.pkl.core.stdlib.ExternalMethod0Node;
import org.pkl.core.stdlib.ExternalMethod1Node;
import org.pkl.core.stdlib.ExternalMethod2Node;
import org.pkl.core.stdlib.ExternalPropertyNode;
import org.pkl.core.util.MutableBoolean;
import org.pkl.core.util.MutableReference;

public final class MappingNodes {
  private MappingNodes() {}

  public abstract static class isEmpty extends ExternalPropertyNode {
    @Specialization
    protected boolean eval(VmMapping self) {
      return self.isEmpty();
    }
  }

  public abstract static class isNotEmpty extends ExternalPropertyNode {
    @Specialization
    protected boolean eval(VmMapping self) {
      return !self.isEmpty();
    }
  }

  public abstract static class length extends ExternalPropertyNode {
    @Specialization
    protected long eval(VmMapping self) {
      return self.getLength();
    }
  }

  public abstract static class keys extends ExternalPropertyNode {
    @Specialization
    protected VmSet eval(VmMapping self) {
      return self.getAllKeys();
    }
  }

  public abstract static class containsKey extends ExternalMethod1Node {
    @Specialization
    protected boolean eval(VmMapping self, Object key) {
      if (self.hasCachedValue(key)) return true;

      for (VmObjectLike curr = self; curr != null; curr = curr.getParent()) {
        if (curr.hasMember(key)) return true;
      }

      return false;
    }
  }

  public abstract static class containsValue extends ExternalMethod1Node {
    @Specialization
    protected boolean eval(VmMapping self, Object value) {
      var foundValue = new MutableBoolean(false);
      self.iterateMemberValues(
          (key, member, memberValue) -> {
            if (memberValue == null) {
              memberValue = VmUtils.readMember(self, key);
            }
            foundValue.set(value.equals(memberValue));
            return !foundValue.get();
          });
      return foundValue.get();
    }
  }

  public abstract static class getOrNull extends ExternalMethod1Node {
    @Child private IndirectCallNode callNode = IndirectCallNode.create();

    @Specialization
    protected Object eval(VmMapping self, Object key) {
      return VmNull.lift(VmUtils.readMemberOrNull(self, key, callNode));
    }
  }

  public abstract static class getOrDefault extends ExternalMethod1Node {
    @Child private IndirectCallNode callNode = IndirectCallNode.create();
    @Child private ApplyVmFunction1Node applyNode = ApplyVmFunction1Node.create();

    @Specialization
    protected Object eval(VmMapping self, Object key) {
      var value = VmUtils.readMemberOrNull(self, key, callNode);
      if (value != null) {
        return value;
      }

      var defaultFunction = (VmFunction) VmUtils.readMember(self, Identifier.DEFAULT, callNode);
      return applyNode.execute(defaultFunction, key);
    }
  }

  public abstract static class fold extends ExternalMethod2Node {
    @Child private ApplyVmFunction3Node applyLambdaNode = ApplyVmFunction3NodeGen.create();

    @Specialization
    protected Object eval(VmMapping self, Object initial, VmFunction function) {
      var result = new MutableReference<>(initial);
      self.forceAndIterateMemberValues(
          (key, def, value) -> {
            result.set(applyLambdaNode.execute(function, result.get(), key, value));
            return true;
          });
      return result.get();
    }
  }

  public abstract static class every extends ExternalMethod1Node {
    @Child private ApplyVmFunction2Node applyLambdaNode = ApplyVmFunction2NodeGen.create();

    @Specialization
    protected boolean eval(VmMapping self, VmFunction function) {
      var result = new MutableBoolean(true);
      self.iterateMemberValues(
          (key, member, value) -> {
            if (value == null) {
              value = VmUtils.readMember(self, key);
            }
            result.set(applyLambdaNode.executeBoolean(function, key, value));
            return result.get();
          });
      return result.get();
    }
  }

  public abstract static class any extends ExternalMethod1Node {
    @Child private ApplyVmFunction2Node applyLambdaNode = ApplyVmFunction2NodeGen.create();

    @Specialization
    protected boolean eval(VmMapping self, VmFunction function) {
      var result = new MutableBoolean(false);
      self.iterateMemberValues(
          (key, member, value) -> {
            if (value == null) {
              value = VmUtils.readMember(self, key);
            }
            result.set(applyLambdaNode.executeBoolean(function, key, value));
            return !result.get();
          });
      return result.get();
    }
  }

  public abstract static class toMap extends ExternalMethod0Node {
    @Specialization
    protected VmMap eval(VmMapping self) {
      var builder = VmMap.builder();
      self.forceAndIterateMemberValues(
          (key, def, value) -> {
            builder.add(key, value);
            return true;
          });
      return builder.build();
    }
  }
}
