001/**
002 * Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
003 * <p>
004 * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * <p>
008 * http://www.gnu.org/licenses/lgpl-3.0.txt
009 * <p>
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package dev.tinyflow.core.util;
017
018import dev.tinyflow.core.chain.Chain;
019import dev.tinyflow.core.util.graalvm.JsInteropUtils;
020import org.graalvm.polyglot.Context;
021import org.graalvm.polyglot.HostAccess;
022import org.graalvm.polyglot.Value;
023
024import java.util.HashMap;
025import java.util.Map;
026
027public class JsConditionUtil {
028
029    // 使用 Context.Builder 构建上下文,线程安全
030    private static final Context.Builder CONTEXT_BUILDER = Context.newBuilder("js")
031            .option("engine.WarnInterpreterOnly", "false")
032            .allowHostAccess(HostAccess.ALL)       // 允许访问 Java 对象的方法和字段
033            .allowHostClassLookup(className -> false) // 禁止动态加载任意 Java 类
034            .option("js.ecmascript-version", "2021");  // 使用较新的 ECMAScript 版本
035
036    /**
037     * 执行 JavaScript 表达式并返回 boolean 结果
038     *
039     * @param code    JS 表达式(应返回布尔或可转换为布尔的值)
040     * @param chain   Chain 上下文对象
041     * @param initMap 初始变量映射
042     * @return true 表示满足条件,继续执行;false 表示跳过
043     */
044    public static boolean eval(String code, Chain chain, Map<String, Object> initMap) {
045        try (Context context = CONTEXT_BUILDER.build()) {
046            Map<String, Object> _result = new HashMap<>();
047            Value bindings = context.getBindings("js");
048
049            // 合并上下文变量
050            Map<String, Object> contextVariables = collectContextVariables(chain, initMap);
051            contextVariables.forEach((key, value) -> {
052                bindings.putMember(key, JsInteropUtils.wrapJavaValueForJS(context, value));
053            });
054
055            bindings.putMember("_result", _result);
056            String resultCode = "_result.value = " + code;
057
058            context.eval("js", resultCode);
059            Object value = _result.get("value");
060            return toBoolean(value);
061        } catch (Exception e) {
062            throw new JsCodeException("JavaScript exec failed: " + e.getMessage()
063                    + "\ncode:"
064                    + "\n```\n"
065                    + "\n" + code
066                    + "\n```\n"
067                    , e);
068        }
069    }
070
071
072    public static long evalLong(String code, Chain chain, Map<String, Object> initMap) {
073        try (Context context = CONTEXT_BUILDER.build()) {
074            Map<String, Object> _result = new HashMap<>();
075            Value bindings = context.getBindings("js");
076
077            // 合并上下文变量
078            Map<String, Object> contextVariables = collectContextVariables(chain, initMap);
079            contextVariables.forEach((key, value) -> {
080                bindings.putMember(key, JsInteropUtils.wrapJavaValueForJS(context, value));
081            });
082
083            bindings.putMember("_result", _result);
084            String resultCode = "_result.value = " + code;
085
086            context.eval("js", resultCode);
087            Object value = _result.get("value");
088            return toLong(value);
089        } catch (Exception e) {
090            throw new JsCodeException("JavaScript exec failed: " + e.getMessage()
091                    + "\ncode:"
092                    + "\n```\n"
093                    + "\n" + code
094                    + "\n```\n"
095                    , e);
096        }
097    }
098
099
100    /**
101     * 将任意对象安全转换为 long 类型
102     */
103    private static long toLong(Object value) {
104        if (value == null) {
105            return 0L;
106        }
107
108        if (value instanceof Number) {
109            return ((Number) value).longValue();
110        }
111
112        if (value instanceof String) {
113            String str = ((String) value).trim();
114            if (str.isEmpty()) {
115                return 0L;
116            }
117            try {
118                // 支持整数和浮点字符串(如 "123", "45.67")
119                return Double.valueOf(str).longValue();
120            } catch (NumberFormatException e) {
121                throw new RuntimeException("无法将字符串 \"" + str + "\" 转换为 long", e);
122            }
123        }
124
125        if (value instanceof Value) {
126            Value v = (Value) value;
127            if (v.isNumber()) {
128                return v.asLong(); // GraalVM 的 asLong() 会自动处理 double/integer
129            } else if (v.isString()) {
130                return toLong(v.asString());
131            } else if (v.isNull()) {
132                return 0L;
133            } else {
134                throw new RuntimeException("无法将 JS 值 " + v + " 转换为 long");
135            }
136        }
137
138        // 兜底:尝试 toString 后解析
139        try {
140            String str = value.toString().trim();
141            return str.isEmpty() ? 0L : Double.valueOf(str).longValue();
142        } catch (Exception e) {
143            throw new RuntimeException("无法将对象 " + value + " 转换为 long", e);
144        }
145    }
146
147    /**
148     * 收集上下文中的变量
149     */
150    private static Map<String, Object> collectContextVariables(Chain chain, Map<String, Object> initMap) {
151        Map<String, Object> variables = new HashMap<>();
152
153        // 添加 Chain Memory 中的变量(去掉前缀)
154        chain.getState().getMemory().forEach((key, value) -> {
155            int dotIndex = key.indexOf(".");
156            String varName = (dotIndex >= 0) ? key.substring(dotIndex + 1) : key;
157            variables.put(varName, value);
158        });
159
160        // 添加 _chain 和 initMap 变量
161        if (initMap != null) {
162            variables.putAll(initMap);
163//            initMap.forEach((s, o) -> variables.put(s, o != null ? o : ""));
164        }
165
166        return variables;
167    }
168
169    /**
170     * 将任意对象转换为布尔值
171     */
172    private static boolean toBoolean(Object value) {
173        if (value == null) {
174            return false;
175        }
176        if (value instanceof Boolean) {
177            return (Boolean) value;
178        }
179        if (value instanceof Number) {
180            return ((Number) value).intValue() != 0;
181        }
182        if (value instanceof String) {
183            String str = ((String) value).trim().toLowerCase();
184            return !str.isEmpty() && !"0".equals(str) && !"false".equals(str);
185        }
186        if (value instanceof Value) {
187            Value v = (Value) value;
188            if (v.isBoolean()) {
189                return v.asBoolean();
190            } else if (v.isNumber()) {
191                return v.asDouble() != 0;
192            } else if (v.isString()) {
193                String str = v.asString().trim().toLowerCase();
194                return !str.isEmpty() && !"0".equals(str) && !"false".equals(str);
195            } else {
196                return !v.isNull();
197            }
198        }
199        return true;
200    }
201
202}