/******************************************************************************
 *  This file is a part of Ultralight, an ultra-portable web-browser engine.  *
 *                                                                            *
 *  See <https://ultralig.ht> for licensing and more.                         *
 *                                                                            *
 *  (C) 2023 Ultralight, Inc.                                                 *
 *****************************************************************************/
#pragma once
#include <AppCore/Defines.h>
#include <JavaScriptCore/JavaScript.h>
#include <JavaScriptCore/JSStringRef.h>
#include <Ultralight/String.h>
#include <functional>
#include <memory>

namespace ultralight {

///
/// Set the current JSContext.
///
/// Most JavaScriptCore C API calls require an active JavaScript execution
/// context (JSContextRef). You can get the JSContextRef for a page via
/// `View::LockJSContext()`. This context changes with each page navigation.
///
/// **Note**:
///    You MUST set a JSContext before using most of the C++ API below.
///
void AExport SetJSContext(JSContextRef ctx);

///
/// Get the current JSContext.
///
JSContextRef AExport GetJSContext();

///
/// JavaScript String wrapper that automatically manages JSStringRef lifetime
/// and provides helpful conversions.
///
class AExport JSString {
public:
  /// Create empty string
  JSString();

  /// Create from C-string
  JSString(const char* str);

  /// Create from Ultralight String
  JSString(const String& str);

  /// Create from existing JSStringRef
  JSString(JSStringRef str);

  /// Copy constructor 
  JSString(const JSString& other);

  /// Destructor
  ~JSString();

  /// Assignment operator 
  JSString& operator=(const JSString& other);

  /// Cast to String
  operator String();

  /// Cast to underlying JSStringRef
  operator JSStringRef() const { return instance_; }

protected:
  JSStringRef instance_;
};

class JSArray;
class JSObject;
class JSFunction;

/// Tag type used with the JSValue constructor to create "Null" types
struct AExport JSValueNullTag {};

/// Tag type used with the JSValue constructor to create "Undefined" types
struct AExport JSValueUndefinedTag {};

///
/// JavaScript variant value wrapper that automatically manages JSValueRef 
/// lifetime and provides helpful conversions.
///
class AExport JSValue {
public:
  /// Create null (empty) JSValue
  JSValue();

  /// Create null JSValue explicitly
  JSValue(JSValueNullTag);

  /// Create undefined JSValue
  JSValue(JSValueUndefinedTag);

  /// Create boolean JSValue
  JSValue(bool val);

  /// Create unsigned integer JSValue (aka, Number) [will be cast to double]
  JSValue(uint32_t val);

  /// Create integer JSValue (aka, Number) [will be cast to double]
  JSValue(int32_t val);

  /// Create unsigned integer JSValue (aka, Number) [will be cast to double]
  JSValue(uint64_t val);

  /// Create integer JSValue (aka, Number) [will be cast to double]
  JSValue(int64_t val);

  /// Create double JSValue (aka, Number)
  JSValue(double val);

  /// Create string JSValue
  JSValue(const char* val);

  /// Create string JSValue
  JSValue(const String& val);

  /// Create string JSValue
  JSValue(JSString val);

  /// Create from existing JSValueRef
  JSValue(JSValueRef val);

  /// Create object JSValue
  JSValue(JSObjectRef obj);

  /// Copy constructor, a shallow copy is made, the constructed JSValue will
  /// point to the same JSValueRef.
  JSValue(const JSValue& other);

  /// Destructor
  virtual ~JSValue();

  /// A shallow copy is made, this JSValue will point to the same JSValueRef
  virtual JSValue& operator=(const JSValue& other);

  /// Whether or not the value is a JavaScript Null type.
  bool IsNull() const;

  /// Whether or not the value is a JavaScript Undefined type.
  bool IsUndefined() const;

  /// Whether or not the value is a JavaScript Boolean type.
  bool IsBoolean() const;

  /// Whether or not the value is a JavaScript Number type.
  bool IsNumber() const;

  /// Whether or not the value is a JavaScript String type.
  bool IsString() const;

  /// Whether or not the value is a JavaScript Object type.
  bool IsObject() const;

  /// Whether or not the value is a JavaScript Array type.
  bool IsArray() const;

  /// Whether or not the value is a JavaScript Function type.
  bool IsFunction() const;

  /// Get the value as a Boolean
  bool ToBoolean() const;

  /// Get the value as a Number (Double)
  double ToNumber() const;

  /// Get the value as a Number (Integer)
  int64_t ToInteger() const { return static_cast<int64_t>(ToNumber()); }

  /// Get the value as a String
  JSString ToString() const;

  /// Get the value as an Object (will debug assert if not an Object)
  JSObject ToObject() const;

  /// Get the value as an Array (will debug asset if not an Array)
  JSArray ToArray() const;

  /// Get the value as a Function (will debug asset if not a Function)
  JSFunction ToFunction() const;

  operator bool() const { return ToBoolean(); }

  operator double() const { return ToNumber(); }

  operator uint32_t() const { return static_cast<uint32_t>(ToNumber()); }

  operator int32_t() const { return static_cast<uint32_t>(ToNumber()); }

  operator uint64_t() const { return static_cast<uint64_t>(ToNumber()); }

  operator int64_t() const { return ToInteger(); }

  operator String() const { return ToString(); }

  operator JSString() const { return ToString(); }

  operator JSObject() const;

  operator JSObjectRef() const;

  operator JSArray() const;

  operator JSFunction() const;

  /// Get the underlying JSValueRef
  operator JSValueRef() const { return instance(); }

  ///
  /// Get the bound context for this JSValue (it is cached at creation).
  ///
  JSContextRef context() const { return ctx_; }

  ///
  /// Set the JSContext for this JSValue.
  ///
  /// **Note**:
  ///    JSValues created from within a JSCallback have a temporary JSContext
  ///    that is destroyed when the callback returns. You will need to "move"
  ///    any JSValues created within these callbacks to the View's main context
  ///    (call set_context() with the main context) before using them outside
  ///    the callback.
  ///
  void set_context(JSContextRef context) { ctx_ = context; }

protected:
  JSValue(JSContextRef ctx);
  JSValue(JSContextRef ctx, JSValueRef val);
  virtual JSValueRef instance() const;

  JSContextRef ctx_;
  JSValueRef instance_ = nullptr;
  friend class JSFunction;
};

/// 
/// A vector of JSValues, used for passing around arguments in JSCallback.
/// 
class AExport JSArgs {
public:
  /// Create an empty list of JavaScript arguments
  JSArgs();

  /// Create a list of JavaScript arguments using a C++ initializer list
  JSArgs(const std::initializer_list<JSValue>& values);

  /// Copy-constructor
  JSArgs(const JSArgs& other);

  /// Destructor
  ~JSArgs();

  /// Assignment operator
  JSArgs& operator=(const JSArgs& other);

  ///
  /// Access an element of the argument list by index.
  ///
  /// **Note**:
  ///    All JSValues are actually wrappers of JSValueRef instances so even
  ///    though this function doesn't return a JSValue& you are still operating
  ///    directly on the underlying JavaScript value instance.
  ///
  JSValue operator[](size_t pos);

  ///
  /// Access an element of the argument list by index. (const overload)
  ///
  /// **Note**:
  ///    All JSValues are actually wrappers of JSValueRef instances so even
  ///    though this function doesn't return a JSValue& you are still operating
  ///    directly on the underlying JavaScript value instance.
  ///
  const JSValue operator[](size_t pos) const;

  /// Whether or not the argument list is empty.
  bool empty() const;

  /// The number of elements in the argument list.
  size_t size() const;

  /// Clear the argument list.
  void clear();

  /// Add a new argument to the end of the list.
  void push_back(const JSValue& val);

  /// Remove the last item from the end of the list.
  void pop_back();

  /// Get the argument list as a C-array of JSValues
  JSValue* data();

  /// Get the argument list as a C-array of JSValues (const overload)
  const JSValue* data() const;
protected:
  void* instance_;
};

///
/// JSCallback typedef used for binding C++ callbacks to JavaScript functions.
///
/// Takes two arguments (const JSObject& thisObj, const JSArgs& args) and
/// returns nothing (void).
///   
typedef std::function<void(const JSObject&, const JSArgs&)> JSCallback;

///
/// JSCallbackWithRetval typedef used for binding C++ callbacks to JavaScript
/// functions with an optional return value.
///
/// Takes two arguments (const JSObject& thisObj, const JSArgs& args) and
/// returns a JSValue back to JavaScript.
///   
typedef std::function<JSValue(const JSObject&, const JSArgs&)> JSCallbackWithRetval;

///
/// Macro to help bind C++ member functions to a JSCallback
///
/// Usage: JSCallback callback = BindJSCallback(&MyClass::MyMemberFunction);
///
/// **Note**: Expected to run from within an instance of 'MyClass', note the
///           'this' keyword in the macro.
///
#define BindJSCallback(fn) (JSCallback)std::bind(fn, this, std::placeholders::_1, std::placeholders::_2)

///
/// Macro to help bind C++ member functions to a JSCallbackWithRetval
///
/// Usage: JSCallback callback = BindJSCallback(&MyClass::MyMemberFunction);
///
/// **Note**: Expected to run from within an instance of 'MyClass', note the
///           'this' keyword in the macro.
///
#define BindJSCallbackWithRetval(fn) (JSCallbackWithRetval)std::bind(fn, this, std::placeholders::_1, std::placeholders::_2)

/// 
/// Wrapper for JSObject property value (JSValue subclass). Allows new value assignment
/// to object property, binding C++ callbacks to object properties via function objects,
/// as well as value query via the JSValue interface.
/// 
class AExport JSPropertyValue : public JSValue {
public:
  virtual ~JSPropertyValue();

  /// Assign a new value to the property (internally calls JSObjectSetProperty)
  virtual JSPropertyValue& operator=(const JSValue& value);

  /// Bind to native C++ callback (creates a Function object that can be called from JS)
  JSPropertyValue& operator=(const JSCallback& callback);

  /// Bind to native C++ callback with return value (creates a Function object that can be called from JS)
  JSPropertyValue& operator=(const JSCallbackWithRetval& callback);

protected:
  virtual JSValueRef instance() const;
  JSPropertyValue(JSContextRef ctx, JSObjectRef proxy_obj, unsigned idx);
  JSPropertyValue(JSContextRef ctx, JSObjectRef proxy_obj, JSString idx);
  JSPropertyValue(const JSPropertyValue&) = default;
  JSPropertyValue& operator=(const JSPropertyValue&) = delete;

  JSObject* proxyObj_;
  bool using_numeric_idx_;
  unsigned numeric_idx_;
  JSString string_idx_;
  friend class JSArray;
  friend class JSObject;
};

///
/// JSArray wrapper that automatically manages lifetime and provides
/// convenient access to indices and Array functions.
/// 
class AExport JSArray {
public:
  /// Create empty Array
  JSArray();

  /// Create Array from list of JSValues
  JSArray(const std::initializer_list<JSValue>& values);

  /// Create Array from existing JSObjectRef (JavaScriptCore C API)
  JSArray(JSObjectRef array_obj);

  /// Copy constructor (shallow copy, will point to same instance)
  JSArray(const JSArray& other);

  ~JSArray();

  /// Assignment (shallow assignment, will point to same instance)
  JSArray& operator=(const JSArray& other);

  /// Get number of elements in the Array
  unsigned length();

  /// Push an element to back of Array
  void push(const JSValue& val);

  /// Find the index (location) of a certain value, will return -1 if not found
  int indexOf(const JSValue& val, int start = 0) const;

  /// Get a property by array index (numbering starts at 0)
  JSPropertyValue operator[](unsigned idx) const;

  /// Get the underlying JSObjectRef (JavaScriptCore C API)
  operator JSObjectRef() const { return instance_; }

  ///
  /// Get the bound context for this JSArray (it is cached at creation).
  ///
  JSContextRef context() const { return ctx_; }

  ///
  /// Set the JSContext for this JSArray.
  ///
  /// **Note**:
  ///    JSArrays created from within a JSCallback have a temporary JSContext
  ///    that is destroyed when the callback returns. You will need to "move"
  ///    any JSArrays created within these callbacks to the View's main context
  ///    (call set_context() with the main context) before using them outside
  ///    the callback.
  ///
  void set_context(JSContextRef context) { ctx_ = context; }

protected:
  JSArray(JSContextRef ctx, JSValueRef val);

  JSContextRef ctx_;
  JSObjectRef instance_;
  friend class JSValue;
};

///
/// JSObject wrapper that automatically manages lifetime and provides
/// convenient access to properties.
/// 
class AExport JSObject {
public:
  /// Create empty Object
  JSObject();

  /// Create from existing JSObjectRef from JavaScriptCore C API
  JSObject(JSObjectRef obj);
  
  /// Copy constructor (shallow copy, will point to same instance)
  JSObject(const JSObject& other);

  ~JSObject();

  /// Assignment (shallow assignment, will point to same instance)
  JSObject& operator=(const JSObject& other);

  /// Get a property by name
  JSPropertyValue operator[](JSString propertyName) const;

  /// Check if a property exists
  bool HasProperty(JSString propertyName) const;

  /// Remove a property
  bool DeleteProperty(JSString propertyName);

  /// Get the underlying JSObjectRef (JavaScriptCore C API)
  operator JSObjectRef() const { return instance_; }

  ///
  /// Get the bound context for this JSObject (it is cached at creation).
  ///
  JSContextRef context() const { return ctx_; }

  ///
  /// Set the JSContext for this JSObject.
  ///
  /// **Note**:
  ///    JSObjects created from within a JSCallback have a temporary JSContext
  ///    that is destroyed when the callback returns. You will need to "move"
  ///    any JSObjects created within these callbacks to the View's main context
  ///    (call set_context() with the main context) before using them outside
  ///    the callback.
  ///
  void set_context(JSContextRef context) { ctx_ = context; }

protected:
  JSObject(JSContextRef ctx, JSValueRef val);
  JSObject(JSContextRef ctx, JSObjectRef obj);

  JSContextRef ctx_;
  JSObjectRef instance_;
  friend class JSValue;
  friend class JSPropertyValue;
};

///
/// JSFunction wrapper that automatically manages lifetime and provides
/// convenient function invocation operators.
///
class AExport JSFunction {
public:
  /// Create an empty Function.
  /// NOTE: It is OKAY to create this without calling SetJSContext() first.
  JSFunction();

  /// Copy constructor (shallow copy, will point to same instance)
  JSFunction(const JSFunction& other);

  ~JSFunction();

  /// Assignment (shallow assignment, will point to same instance)
  JSFunction& operator=(const JSFunction& other);

  /// Whether or not this is a valid, callable Function object.
  bool IsValid() const;

  /// Call function (using Global Object for 'this') and return the result.
  JSValue operator()(const JSArgs& args);

  /// Call function (with explicit object for 'this') and return the result
  JSValue operator()(const JSObject& thisObject, const JSArgs& args);

  /// Get the underlying JSObjectRef (JavaScriptCore C API)
  operator JSObjectRef() const { return instance_; }

  ///
  /// Get the bound context for this JSFunction (it is cached at creation).
  ///
  JSContextRef context() const { return ctx_; }

  ///
  /// Set the JSContext for this JSFunction.
  ///
  /// **Note**:
  ///    JSFunctions created from within a JSCallback have a temporary JSContext
  ///    that is destroyed when the callback returns. You will need to "move"
  ///    any JSFunctions created within these callbacks to the View's main context
  ///    (call set_context() with the main context) before using them outside
  ///    the callback.
  ///
  void set_context(JSContextRef context) { ctx_ = context; }

protected:
  JSFunction(JSContextRef ctx, JSValueRef val);

  JSContextRef ctx_;
  JSObjectRef instance_;
  friend class JSValue;  
};

///
/// Get the Global Object for the current JSContext.
/// In JavaScript, this would be equivalent to the "window" object.
///
JSObject AExport JSGlobalObject();

///
/// Evaluate a string of JavaScript and return a result.
/// 
JSValue AExport JSEval(const JSString& str);

}  // namespace ultralight
