ReactFlow bindings for ReScript

module Provider = {
  @module("@xyflow/react") @react.component
  external make: (~children: React.element) => React.element = "ReactFlowProvider"
}

module Controls = {
  @module("@xyflow/react") @react.component
  external make: (~showInteractive: bool=?) => React.element = "Controls"
}

module MiniMap = {
  @module("@xyflow/react") @react.component
  external make: (~zoomable: bool=?, ~pannable: bool=?) => React.element = "MiniMap"
}

module Background = {
  @module("@xyflow/react") @react.component
  external make: (
    ~color: string=?,
    ~variant: string=?,
    ~gap: int=?,
    ~size: int=?,
  ) => React.element = "Background"
}

module ViewportPortal = {
  @module("@xyflow/react") @react.component
  external make: (~children: React.element) => React.element = "ViewportPortal"
}

type position = [#top | #left | #bottom | #right]

module Handle = {
  type handleType = [#source | #target]

  @module("@xyflow/react") @react.component
  external make: (
    @as("type") ~type_: handleType,
    ~position: position,
    ~id: string=?,
  ) => React.element = "Handle"
}

module NodeResizer = {
  @module("@xyflow/react") @react.component
  external make: (
    ~minWidth: float=?,
    ~minHeight: float=?,
    ~isVisible: bool=?,
    ~maxWidth: float=?,
    ~maxHeight: float=?,
    ~lineStyle: ReactDOM.Style.t=?,
  ) => React.element = "NodeResizer"
}

type nodeTypes<'props> = dict<React.componentLike<'props, React.element>>

type proOptions = {hideAttribution: bool}

type nodeXYPosition = {x: float, y: float}

@unboxed
type nodeExtent =
  | @as("parent") Parent
  | CoordinateExtent(((float, float), (float, float)))
type nodeOrigin = (float, float)
type nodeHandle
type nodeMeasuredDimensions = {
  width?: float,
  height?: float,
}
/** Documentation: https://reactflow.dev/api-reference/types/node#fields */
type node<'data> = {
  id: string,
  position: nodeXYPosition,
  data: 'data,
  @as("type") type_: string,
  sourcePosition?: position,
  targetPosition?: position,
  hidden?: bool,
  selected?: bool,
  dragging?: bool,
  draggable?: bool,
  selectable?: bool,
  connectable?: bool,
  resizing?: bool,
  deletable?: bool,
  dragHandle?: string,
  width: Nullable.t<float>,
  height: Nullable.t<float>,
  parentId?: string,
  zIndex?: float,
  extent?: nodeExtent,
  expandParent?: bool,
  ariaLabel?: string,
  focusable?: bool,
  style?: ReactDOM.Style.t,
  className?: string,
  origin?: nodeOrigin,
  handles?: array<nodeHandle>,
  measured?: nodeMeasuredDimensions,
}

type edgeLabelOptions = {
  label?: React.element,
  labelStyle?: ReactDOM.Style.t,
  labelShowBg?: bool,
  labelBgStyle?: ReactDOM.Style.t,
  labelBgPadding?: (float, float),
  labelBgBorderRadius?: float,
}

type edgeMarkerType = [#arrow | #arrowclosed]
type edgeMarker = {
  @as("type") type_: edgeMarkerType,
  color?: string,
  width?: float,
  height?: float,
  markerUnits?: string,
  orient?: string,
  strokeWidth?: float,
}
@unboxed
type reconnectable =
  | @as(true) True
  | @as(false) False
  | HandleType(Handle.handleType)
type edgePathOptions = {
  // SmoothStepEdge
  offset?: float,
  borderRadius?: float,
  // BezierEdge
  curvature?: float,
}
/** Documentation: https://reactflow.dev/api-reference/types/edge#variants */
type edge<'data> = {
  ...edgeLabelOptions,
  id: string,
  @as("type")
  type_?: string,
  style?: ReactDOM.Style.t,
  className?: string,
  source: string,
  target: string,
  sourceHandle?: Nullable.t<string>,
  targetHandle?: Nullable.t<string>,
  data: 'data,
  hidden?: bool,
  animated?: bool,
  selected?: bool,
  selectable?: bool,
  deletable?: bool,
  focusable?: bool,
  reconnectable?: reconnectable,
  markerStart?: edgeMarker,
  markerEnd?: edgeMarker,
  zIndex?: float,
  interactionWidth?: float,
  ariaLabel?: string,
  // For SmoothStepEdge and BezierEdgeType variant
  pathOptions?: edgePathOptions,
}

// https://reactflow.dev/api-reference/types/node-change
@tag("type")
type rec nodeChange =
  | @as("dimensions")
  Dimensions({
      id: string,
      dimensions?: nodeMeasuredDimensions,
      resizing?: bool,
      setAttributes?: bool,
    })
  | @as("position")
  Position({
      id: string,
      position?: nodeXYPosition,
      positionAbsolute?: nodeXYPosition,
      dragging?: bool,
    })
  | @as("select") Selection({id: string, selected: bool})
  | @as("remove") Remove({id: string})
  | @as("add") Add({item: node<NodeData.t>})
  | @as("replace") Replace({id: string, item: node<NodeData.t>})

// TODO
type edgeChange

@module("@xyflow/react")
external applyNodeChanges: (array<nodeChange>, array<node<'a>>) => array<node<'a>> =
  "applyNodeChanges"

@module("@xyflow/react")
external applyEdgeChanges: (array<edgeChange>, array<edge<'a>>) => array<edge<'a>> =
  "applyEdgeChanges"

@module("@xyflow/react")
external useNodesState: array<node<'a>> => (
  array<node<'a>>,
  (array<node<'a>> => array<node<'a>>) => unit,
  array<nodeChange> => unit,
) = "useNodesState"

@module("@xyflow/react")
external useEdgesState: array<edge<'a>> => (
  array<edge<'a>>,
  (array<edge<'a>> => array<edge<'a>>) => unit,
  array<edgeChange> => unit,
) = "useEdgesState"

type fitViewNode = {id: string}
type fitViewOptions = {
  // TODO: padding can also be string ending with `px` or `%`
  padding?: float,
  includeHiddenNodes?: bool,
  minZoom?: float,
  maxZoom?: float,
  duration?: float,
  nodes?: array<fitViewNode>,
}

module Viewport = {
  type t = {x: float, y: float, zoom: float}
}

module Instance = {
  type t

  type zoomOptions = {duration: float}

  type position = {x: float, y: float}

  @send
  external getZoom: t => float = "getZoom"
  @send
  external zoomTo: (t, float, ~options: zoomOptions=?) => unit = "zoomTo"

  @send
  external screenToFlowPosition: (t, position) => position = "screenToFlowPosition"
  @send
  external flowToScreenPosition: (t, position) => position = "flowToScreenPosition"

  @send
  external fitView: (t, ~options: fitViewOptions=?) => bool = "fitView"

  @send
  external getViewport: t => Viewport.t = "getViewport"
  @send
  external setViewport: (t, Viewport.t, ~options: zoomOptions=?) => promise<bool> = "setViewport"

  type setCenterOptions = {
    duration: float,
    zoom: float,
  }
  /** setCenter(x, y, options) */
  @send
  external setCenter: (t, float, float, ~options: setCenterOptions=?) => promise<bool> = "setCenter"

  @send
  external getNodes: t => array<node<NodeData.t>> = "getNodes"
}

@module("@xyflow/react")
external useReactFlow: unit => Instance.t = "useReactFlow"

@unboxed
type panOnDrag = Bool(bool) | MouseButtons(array<int>)

type selectionMode = [#partial | #full]

@module("@xyflow/react") @react.component
external make: (
  ~nodeTypes: nodeTypes<'props>,
  ~nodes: array<node<'nodeData>>,
  ~edges: array<edge<'edgeData>>,
  ~onNodesChange: array<nodeChange> => unit=?,
  ~onEdgesChange: array<edgeChange> => unit=?,
  ~onViewportChange: Viewport.t => unit=?,
  ~onInit: Instance.t => unit=?,
  ~defaultViewport: Viewport.t=?,
  ~fitView: bool=?,
  ~fitViewOptions: fitViewOptions=?,
  ~minZoom: float=?,
  ~maxZoom: float=?,
  ~onlyRenderVisibleElements: bool=?,
  ~panOnDrag: panOnDrag=?,
  ~panOnScroll: bool=?,
  ~selectionOnDrag: bool=?,
  ~selectionMode: selectionMode=?,
  ~proOptions: proOptions=?,
  ~nodesConnectable: bool=?,
  ~edgesFocusable: bool=?,
  ~elevateEdgesOnSelect: bool=?,
  ~deleteKeyCode: array<string>=?,
  ~zoomOnDoubleClick: bool=?,
  ~onPaneClick: ReactEvent.Mouse.t => unit=?,
  ~onSelectionDragStart: (ReactEvent.Mouse.t, array<node<'nodeData>>) => unit=?,
  ~onSelectionDrag: (ReactEvent.Mouse.t, array<node<'nodeData>>) => unit=?,
  ~onSelectionDragStop: (ReactEvent.Mouse.t, array<node<'nodeData>>) => unit=?,
  ~onSelectionStart: ReactEvent.Mouse.t => unit=?,
  ~onSelectionEnd: ReactEvent.Mouse.t => unit=?,
  ~children: React.element=?,
) => React.element = "ReactFlow"

@module("@xyflow/react")
external useUpdateNodeInternals: unit => string => unit = "useUpdateNodeInternals"

// See type definitions here for complete list of fields:
// - https://github.com/xyflow/xyflow/blob/reactflow%4011.11.3/packages/core/src/types/general.ts#L144-L238
// - https://github.com/xyflow/xyflow/blob/%40xyflow/react%4012.0.4/packages/react/src/types/store.ts#L50-L149
type userSelectionRect = {
  width: float,
  height: float,
  startX: float,
  startY: float,
  x: float,
  y: float,
}
type storeState = {
  nodesSelectionActive?: bool,
  userSelectionActive?: bool,
  userSelectionRect?: Nullable.t<userSelectionRect>,
  transform?: (float, float, float),
}

@module("@xyflow/react")
external useStore: (storeState => 'a, ~equalityFn: ('a, 'a) => bool=?) => 'a = "useStore"

let useUserSelectionActive = () =>
  useStore(state => state.userSelectionActive, ~equalityFn=Object.is)

type storeSubscribe
type store = {getState: unit => storeState, setState: storeState => unit, subscribe: storeSubscribe}
@module("@xyflow/react")
external useStoreApi: unit => store = "useStoreApi"