// Copyright 2025 International Digital Economy Academy
//
// 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.

///|
/// JavaScript expression evaluation via FFI
/// This file is only compiled for the JS backend

///|
/// Evaluate a JavaScript expression with a context object containing locals
/// The context is passed as a JSON string of key-value pairs
extern "js" fn js_eval_with_context(
  expr : String,
  context_json : String,
) -> String =
  #|(expr, contextJson) => {
  #|  try {
  #|    const context = JSON.parse(contextJson);
  #|    // Create a function that has all context variables in scope
  #|    const keys = Object.keys(context);
  #|    const values = Object.values(context);
  #|    const fn = new Function(...keys, 'return ' + expr);
  #|    const result = fn(...values);
  #|    return result === null || result === undefined ? '' : String(result);
  #|  } catch(e) {
  #|    return '';
  #|  }
  #|}

///|
/// Execute a JavaScript statement and return updated context as JSON
extern "js" fn js_exec_with_context(
  stmt : String,
  context_json : String,
) -> String =
  #|(stmt, contextJson) => {
  #|  try {
  #|    const context = JSON.parse(contextJson);
  #|    // Create a function that executes the statement and returns updated context
  #|    const keys = Object.keys(context);
  #|    const values = Object.values(context);
  #|    // Build code that: 1) declares all context vars, 2) runs statement, 3) returns all vars
  #|    let code = '';
  #|    for (let i = 0; i < keys.length; i++) {
  #|      code += 'var ' + keys[i] + ' = arguments[' + i + '];\n';
  #|    }
  #|    code += stmt + ';\n';
  #|    code += 'return JSON.stringify({';
  #|    for (let i = 0; i < keys.length; i++) {
  #|      if (i > 0) code += ',';
  #|      code += '"' + keys[i] + '":' + keys[i];
  #|    }
  #|    // Also capture any new vars defined with 'var' in the statement
  #|    const varMatch = stmt.match(/var\s+(\w+)/g);
  #|    if (varMatch) {
  #|      for (const m of varMatch) {
  #|        const varName = m.replace(/var\s+/, '');
  #|        if (!keys.includes(varName)) {
  #|          if (keys.length > 0 || varMatch.indexOf(m) > 0) code += ',';
  #|          code += '"' + varName + '":' + varName;
  #|        }
  #|      }
  #|    }
  #|    code += '});';
  #|    const fn = new Function(...keys, code);
  #|    return fn(...values);
  #|  } catch(e) {
  #|    return contextJson;
  #|  }
  #|}

///|
/// Check if a string represents a valid number
fn is_numeric_string(s : String) -> Bool {
  if s.length() == 0 {
    return false
  }
  let chars = s.to_array()
  let mut i = 0
  // Allow leading minus
  if chars[0] == '-' {
    i = 1
    if chars.length() == 1 {
      return false
    }
  }
  let mut has_dot = false
  while i < chars.length() {
    let c = chars[i]
    if c >= '0' && c <= '9' {
      i += 1
    } else if c == '.' && not(has_dot) {
      has_dot = true
      i += 1
    } else {
      return false
    }
  }
  true
}

///|
/// Convert Locals to JSON string for passing to JS
/// Converts "true"/"false" to boolean values and numeric strings to numbers
pub fn locals_to_json(locals : Locals) -> String {
  let buf = StringBuilder::new()
  buf.write_string("{")
  let mut first = true
  for k, v in locals.0 {
    if not(first) {
      buf.write_string(",")
    }
    first = false
    buf.write_string("\"")
    buf.write_string(escape_json_string(k))
    buf.write_string("\":")
    // Convert string booleans to actual booleans
    if v == "true" {
      buf.write_string("true")
    } else if v == "false" {
      buf.write_string("false")
    } else if is_numeric_string(v) {
      // Pass numbers as JSON numbers for proper JS arithmetic
      buf.write_string(v)
    } else {
      buf.write_string("\"")
      buf.write_string(escape_json_string(v))
      buf.write_string("\"")
    }
  }
  buf.write_string("}")
  buf.to_string()
}

///|
/// Escape special characters for JSON string
fn escape_json_string(s : String) -> String {
  let buf = StringBuilder::new()
  for c in s {
    match c {
      '"' => buf.write_string("\\\"")
      '\\' => buf.write_string("\\\\")
      '\n' => buf.write_string("\\n")
      '\r' => buf.write_string("\\r")
      '\t' => buf.write_string("\\t")
      _ => buf.write_char(c)
    }
  }
  buf.to_string()
}

///|
/// Evaluate an expression using JavaScript
/// This is the main entry point for JS expression evaluation
pub fn eval_js_expression(expr : String, locals : Locals) -> String {
  let context = locals_to_json(locals)
  js_eval_with_context(expr, context)
}

///|
/// Execute a JavaScript statement and update locals with any new/modified variables
pub fn exec_js_statement(stmt : String, locals : Locals) -> Unit {
  let context = locals_to_json(locals)
  let result = js_exec_with_context(stmt, context)
  // Parse result JSON and update locals
  parse_json_into_locals(result, locals)
}

///|
/// Parse a JSON object string and update locals with its key-value pairs
fn parse_json_into_locals(json : String, locals : Locals) -> Unit {
  // Simple JSON parser for {"key":"value",...} format
  if json.length() < 2 {
    return
  }
  let chars = json.to_array()
  let mut i = 0
  // Skip opening {
  if chars[i] == '{' {
    i += 1
  }
  while i < chars.length() {
    // Skip whitespace
    while i < chars.length() &&
          (chars[i] == ' ' || chars[i] == '\n' || chars[i] == '\t') {
      i += 1
    }
    if i >= chars.length() || chars[i] == '}' {
      break
    }
    // Skip comma
    if chars[i] == ',' {
      i += 1
      continue
    }
    // Read key (expect "key")
    if chars[i] != '"' {
      break
    }
    i += 1
    let key_start = i
    while i < chars.length() && chars[i] != '"' {
      if chars[i] == '\\' {
        i += 2
      } else {
        i += 1
      }
    }
    let key = try! json[key_start:i].to_string()
    i += 1 // skip closing "
    // Skip : and whitespace
    while i < chars.length() && (chars[i] == ':' || chars[i] == ' ') {
      i += 1
    }
    // Read value
    let value = if chars[i] == '"' {
      // String value
      i += 1
      let val_start = i
      while i < chars.length() && chars[i] != '"' {
        if chars[i] == '\\' {
          i += 2
        } else {
          i += 1
        }
      }
      let v = try! json[val_start:i].to_string()
      i += 1 // skip closing "
      v
    } else {
      // Number, boolean, or null
      let val_start = i
      while i < chars.length() && chars[i] != ',' && chars[i] != '}' {
        i += 1
      }
      try! json[val_start:i].to_string()
    }
    locals.set(key, value)
  }
}