

open Num

exception Infinite_result of string


(* Type definitions *)

type t =
  {
    nature: nature;
    sortingHash: int;
    hash: int;
    mutable count: int;
    mutable replacement: t
  }
and nature =
  | Addition of t list
  | And of t list
  | ArcCosine of t
  | ArcHyperbolicCosine of t
  | ArcHyperbolicSine of t
  | ArcHyperbolicTangent of t
  | ArcSine of t
  | ArcTangent of t
  | BlackBox of string * t list
  | BooleanValue of bool
  | Constant of string
  | Cosine of t
  | Derivative of t * num
  | DiscreteVariable of int
  | Equality of t * t
  | Exponential of t
  | Floor of t
  | Greater of t * t
  | HyperbolicCosine of t
  | HyperbolicSine of t
  | HyperbolicTangent of t
  | If of t * t * t
  | Logarithm of t
  | Multiplication of t list
  | Not of t
  | Number of num
  | Or of t list
  | Parameter of int
  | PartialDerivative of t * t
  | RationalPower of t * num
  | Sign of t
  | Sine of t
  | Tangent of t
  | TimeVariable
  | Variable of int


(* Node utilities *)

let nature node = node.nature

let hash node = node.hash

(* List utilities *)

let rec eq_list nodes nodes' = match nodes, nodes' with
  | [], [] -> true
  | node :: nodes, node' :: nodes' when node == node' -> eq_list nodes nodes'
  | _ -> false

let insert node nodes =
  let rec insert_into nodes = match nodes with
    | [] -> [node]
    | node' :: _ when node.sortingHash <= node'.sortingHash -> node :: nodes
    | node' :: nodes' -> node' :: insert_into nodes'
  in insert_into nodes

let remove node nodes =
  let rec remove_from = function
    | [] -> []
    | node' :: nodes' when node == node' -> nodes'
    | node' :: nodes' -> node' :: remove_from nodes'
  in remove_from nodes

let sort nodes =
  List.sort
    (fun node node' ->
      if node.sortingHash < node'.sortingHash then -1
      else if node.sortingHash > node'.sortingHash then 1
      else 0)
    nodes

let union nodes nodes' =
  List.fold_left
    (fun acc node -> if List.exists (( == ) node) acc then acc else node :: acc)
    nodes
    nodes'

let intersection nodes nodes' =
  List.fold_left
    (fun acc node ->
      if List.exists (( == ) node) nodes then node :: acc else acc)
    []
    nodes'


(* Node collections *)

type node = t

module Node =
  struct
    type t = node
    let hash node = node.hash
  end

module NodeSet = GraphNodeSet.Make(Node)

let global_count = ref 0

let unique_integer =
  let i = ref 0
  in function () -> let v = !i in begin incr i; v end

let create_node nature hash =
  let rec node =
    {
      nature = nature;
      sortingHash = unique_integer ();
      hash = hash;
      count = !global_count;
      replacement = node
    }
  in node

let additionNodeSet =
  let equal nodeList node = match node.nature with
    | Addition nodes -> eq_list nodeList nodes
    | _ -> invalid_arg "Argument mismatch."
  and hash nodes =
    List.fold_left
      (fun sum elt -> (sum lsl 3) + elt.hash)
      0x248c44
      nodes
  and create nodes hash = create_node (Addition nodes) hash
  in NodeSet.create 101 equal hash create

let andNodeSet =
  let equal nodeList node = match node.nature with
    | And nodes -> eq_list nodeList nodes
    | _ -> invalid_arg "Argument mismatch."
  and hash nodes =
    List.fold_left
      (fun sum elt -> (sum lsl 6) + elt.hash)
      0xe41e902
      nodes
  and create nodes hash = create_node (And nodes) hash
  in NodeSet.create 101 equal hash create

let arcCosineNodeSet =
  let equal node1 node = match node.nature with
    | ArcCosine node -> node == node1
    | _ -> invalid_arg "Argument mismatch."
  and hash node1 = (node1.hash lsl 6) lxor 0x4211038
  and create node1 hash = create_node (ArcCosine node1) hash
  in NodeSet.create 101 equal hash create

let arcHyperbolicCosineNodeSet =
  let equal node1 node = match node.nature with
    | ArcHyperbolicCosine node -> node == node1
    | _ -> invalid_arg "Argument mismatch."
  and hash node1 = (node1.hash lsl 4) lxor 0x24a5b008
  and create node1 hash = create_node (ArcHyperbolicCosine node1) hash
  in NodeSet.create 101 equal hash create

let arcHyperbolicSineNodeSet =
  let equal node1 node = match node.nature with
    | ArcHyperbolicSine node -> node == node1
    | _ -> invalid_arg "Argument mismatch."
  and hash node1 = (node1.hash lsl 3) lxor 0x21a8d400
  and create node1 hash = create_node (ArcHyperbolicSine node1) hash
  in NodeSet.create 101 equal hash create

let arcHyperbolicTangentNodeSet =
  let equal node1 node = match node.nature with
    | ArcHyperbolicTangent node -> node == node1
    | _ -> invalid_arg "Argument mismatch."
  and hash node1 = (node1.hash lsl 5) lxor 0x2bd00c4
  and create node1 hash = create_node (ArcHyperbolicTangent node1) hash
  in NodeSet.create 101 equal hash create

let arcSineNodeSet =
  let equal node1 node = match node.nature with
    | ArcSine node -> node == node1
    | _ -> invalid_arg "Argument mismatch."
  and hash node1 = (node1.hash lsl 3) lxor 0x114500a0
  and create node1 hash = create_node (ArcSine node1) hash
  in NodeSet.create 101 equal hash create

let arcTangentNodeSet =
  let equal node1 node = match node.nature with
    | ArcTangent node -> node == node1
    | _ -> invalid_arg "Argument mismatch."
  and hash node1 = (node1.hash lsl 4) lxor 0x200aa000
  and create node1 hash = create_node (ArcTangent node1) hash
  in NodeSet.create 101 equal hash create

let blackBoxNodeSet =
  let equal (string, nodeList) node = match node.nature with
    | BlackBox (name, nodes) -> name = string && eq_list nodeList nodes
    | _ -> invalid_arg "Argument mismatch."
  and hash (string, nodes) =
    (GraphNodeSet.hash string +
    List.fold_left (fun sum elt -> (sum lsl 3) + elt.hash) 0x20a0 nodes) land
    max_int
  and create (string, nodes) hash = create_node (BlackBox (string, nodes)) hash
  in NodeSet.create 101 equal hash create

let constantNodeSet =
  let equal string node = match node.nature with
    | Constant name -> name = string
    | _ -> invalid_arg "Argument mismatch."
  and hash string = GraphNodeSet.hash string lxor 0x709dc0
  and create string hash = create_node (Constant string) hash
  in NodeSet.create 101 equal hash create

let cosineNodeSet =
  let equal node1 node = match node.nature with
    | Cosine node -> node == node1
    | _ -> invalid_arg "Argument mismatch."
  and hash node1 = (node1.hash lsl 5) lxor 0x208af001
  and create node1 hash = create_node (Cosine node1) hash
  in NodeSet.create 101 equal hash create

let derivativeNodeSet =
  let equal (node1, num) node = match node.nature with
    | Derivative (node', num') -> node1 == node' && eq_num num num'
    | _ -> invalid_arg "Argument mismatch."
  and hash (node, num) =
    (GraphNodeSet.hash num lsl 10) lxor (node.hash + 0x1410200d)
  and create (node, num) hash = create_node (Derivative (node, num)) hash
  in NodeSet.create 101 equal hash create

let discreteVariableNodeSet =
  let equal i node = match node.nature with
    | DiscreteVariable i' -> i = i'
    | _ -> invalid_arg "Argument mismatch."
  and hash i = GraphNodeSet.hash i lxor 0x2fb049a3
  and create i hash = create_node (DiscreteVariable i) hash
  in NodeSet.create 101 equal hash create

let equalityNodeSet =
  let equal (node1, node2) node = match node.nature with
    | Equality (leftHandNode, rightHandNode) ->
        leftHandNode == node1 && rightHandNode == node2
    | _ -> invalid_arg "Argument mismatch."
  and hash (node1, node2) = (node1.hash lsl 5) lxor (node2.hash + 0x5218c660)
  and create (node1, node2) hash = create_node (Equality (node1, node2)) hash
  in NodeSet.create 101 equal hash create

let exponentialNodeSet =
  let equal node1 node = match node.nature with
    | Exponential node -> node == node1
    | _ -> invalid_arg "Argument mismatch."
  and hash node1 = (node1.hash lsl 4) lxor 0x20c22a00
  and create node1 hash = create_node (Exponential node1) hash
  in NodeSet.create 101 equal hash create

let floorNodeSet =
  let equal node1 node = match node.nature with
    | Floor node -> node == node1
    | _ -> invalid_arg "Argument mismatch."
  and hash node1 = (node1.hash lsl 2) lxor 0x8788b02
  and create node1 hash = create_node (Floor node1) hash
  in NodeSet.create 101 equal hash create

let greaterNodeSet =
  let equal (node1, node2) node = match node.nature with
    | Greater (leftHandNode, rightHandNode) ->
        leftHandNode == node1 && rightHandNode == node2
    | _ -> invalid_arg "Argument mismatch."
  and hash (node1, node2) =
    (node1.hash lsl 18) lxor (node2.hash + 0x11e02c02)
  and create (node1, node2) hash = create_node (Greater (node1, node2)) hash
  in NodeSet.create 101 equal hash create

let hyperbolicCosineNodeSet =
  let equal node1 node = match node.nature with
    | HyperbolicCosine node -> node == node1
    | _ -> invalid_arg "Argument mismatch."
  and hash node1 = (node1.hash lsl 4) lxor 0x2a091f0c
  and create node1 hash = create_node (HyperbolicCosine node1) hash
  in NodeSet.create 101 equal hash create

let hyperbolicSineNodeSet =
  let equal node1 node = match node.nature with
    | HyperbolicSine node -> node == node1
    | _ -> invalid_arg "Argument mismatch."
  and hash node1 = (node1.hash lsl 5) lxor 0x24f002f1
  and create node1 hash = create_node (HyperbolicSine node1) hash
  in NodeSet.create 101 equal hash create

let hyperbolicTangentNodeSet =
  let equal node1 node = match node.nature with
    | HyperbolicTangent node -> node == node1
    | _ -> invalid_arg "Argument mismatch."
  and hash node1 = (node1.hash lsl 3) lxor 0x2a221c8
  and create node1 hash = create_node (HyperbolicTangent node1) hash
  in NodeSet.create 101 equal hash create

let ifNodeSet =
  let equal (node1, node2, node3) node = match node.nature with
    | If (testNode, alternativeNode1, alternativeNode2) ->
        testNode == node1 && alternativeNode1 == node2 &&
        alternativeNode2 == node3
    | _ -> invalid_arg "Argument mismatch."
  and hash (node1, node2, node3) =
    (node1.hash lsl 17) lxor (node2.hash lsl 5) lxor
    (node3.hash + 0x5d403a30)
  and create (node1, node2, node3) hash =
    create_node (If (node1, node2, node3)) hash
  in NodeSet.create 101 equal hash create

let logarithmNodeSet =
  let equal node1 node = match node.nature with
    | Logarithm node -> node == node1
    | _ -> invalid_arg "Argument mismatch."
  and hash node1 = (node1.hash lsl 2) lxor 0x11bf004
  and create node1 hash = create_node (Logarithm node1) hash
  in NodeSet.create 101 equal hash create

let multiplicationNodeSet =
  let equal nodeList node = match node.nature with
    | Multiplication nodes -> eq_list nodeList nodes
    | _ -> invalid_arg "Argument mismatch."
  and hash nodes =
    List.fold_left
      (fun sum elt -> (sum lsl 3) + elt.hash)
      0x25a50039
      nodes
  and create nodes hash = create_node (Multiplication nodes) hash
  in NodeSet.create 101 equal hash create

let numberNodeSet =
  let equal num node = match node.nature with
    | Number num' -> eq_num num' num
    | _ -> invalid_arg "Argument mismatch."
  and hash num = GraphNodeSet.hash num lxor 0x59710f0
  and create num hash =
    let rec node =
      {
        nature = Number num;
        sortingHash = -unique_integer ();
        (* The sorting hash is nagated to ensure that numbers appear
           at the first positions in commutative n-ary expressions.  *)
        hash = hash;
        count = !global_count;
        replacement = node
      }
    in node
  in NodeSet.create 101 equal hash create

let notNodeSet =
  let equal node1 node = match node.nature with
    | Not node -> node == node1
    | _ -> invalid_arg "Argument mismatch."
  and hash node1 = (node1.hash lsl 5) lxor 0x18a20a0
  and create node1 hash = create_node (Not node1) hash
  in NodeSet.create 101 equal hash create

let orNodeSet =
  let equal nodes node = match node.nature with
    | Or nodes' -> eq_list nodes nodes'
    | _ -> invalid_arg "Argument mismatch."
  and hash nodes =
    List.fold_left
      (fun sum elt -> (sum lsl 3) + elt.hash)
      0x4102f024
      nodes
  and create nodes hash = create_node (Or nodes) hash
  in NodeSet.create 101 equal hash create

let parameterNodeSet =
  let equal i node = match node.nature with
    | Parameter i' -> i' = i
    | _ -> invalid_arg "Argument mismatch."
  and hash i = GraphNodeSet.hash i lxor 0x709dc0
  and create i hash = create_node (Parameter i) hash
  in NodeSet.create 101 equal hash create

let partialDerivativeNodeSet =
  let equal (node1, node2) node = match node.nature with
    | PartialDerivative (variableNode, argumentNode) ->
        variableNode == node1 && argumentNode == node2
    | _ -> invalid_arg "Argument mismatch."
  and hash (node1, node2) =
    (node1.hash lsl 12) lxor (node2.hash + 0x2f301000)
  and create (node1, node2) hash =
    create_node (PartialDerivative (node1, node2)) hash
  in NodeSet.create 101 equal hash create

let rationalPowerNodeSet =
  let equal (node1, num) node = match node.nature with
    | RationalPower (argumentNode, num')  ->
        argumentNode == node1 && (eq_num num' num)
    | _ -> invalid_arg "Argument mismatch."
  and hash (node1, num) =
    (GraphNodeSet.hash num lsl 20) lxor (node1.hash + 0xd41000d)
  and create (node1, num) hash = create_node (RationalPower (node1, num)) hash
  in NodeSet.create 101 equal hash create

let signNodeSet =
  let equal node1 node = match node.nature with
    | Sign node -> node == node1
    | _ -> invalid_arg "Argument mismatch."
  and hash node1 = (node1.hash lsl 5) lxor 0x100050f0
  and create node1 hash = create_node (Sign node1) hash
  in NodeSet.create 101 equal hash create

let sineNodeSet =
  let equal node1 node = match node.nature with
    | Sine node -> node == node1
    | _ -> invalid_arg "Argument mismatch."
  and hash node1 = (node1.hash lsl 6) lxor 0x8a8f00
  and create node1 hash = create_node (Sine node1) hash
  in NodeSet.create 101 equal hash create

let tangentNodeSet =
  let equal node1 node = match node.nature with
    | Tangent node -> node == node1
    | _ -> invalid_arg "Argument mismatch."
  and hash node1 = (node1.hash lsl 8) lxor 0x655001c9
  and create node1 hash = create_node (Tangent node1) hash
  in NodeSet.create 101 equal hash create

let variableNodeSet =
  let equal i node = match node.nature with
    | Variable i' -> i' = i
    | _ -> invalid_arg "Argument mismatch."
  and hash i = GraphNodeSet.hash i lxor 0x2fb04900
  and create i hash = create_node (Variable i) hash
  in NodeSet.create 101 equal hash create


(* Node creation *)

let zero_num = Int 0
let one_num = Int 1
let two_num = Int 2

let minus_one = NodeSet.find_or_add (Int (-1)) numberNodeSet
let zero = NodeSet.find_or_add zero_num numberNodeSet
let one = NodeSet.find_or_add one_num numberNodeSet
let two = NodeSet.find_or_add two_num numberNodeSet
let ten = NodeSet.find_or_add (Int 10) numberNodeSet
let one_over_two = NodeSet.find_or_add (div_num (one_num) two_num) numberNodeSet
let minus_one_over_two =
  NodeSet.find_or_add (div_num (Int (-1)) two_num) numberNodeSet
let pi = NodeSet.find_or_add "3.14159265359" constantNodeSet
let pi_over_two =
  NodeSet.find_or_add (insert one_over_two [pi]) multiplicationNodeSet
let minus_pi_over_two =
  NodeSet.find_or_add (insert minus_one_over_two [pi]) multiplicationNodeSet
let e = NodeSet.find_or_add "2.71828182846" constantNodeSet
let eps = NodeSet.find_or_add "0.00001" constantNodeSet
let false_value = create_node (BooleanValue false) 0
let true_value = create_node (BooleanValue true) 1
let time = create_node TimeVariable 0

let addition_neutral = zero
let multiplication_neutral = one
let and_neutral = true_value
let or_neutral = false_value

let create_addition = function
  | [] -> addition_neutral
  | [node] -> node
  | nodes -> NodeSet.find_or_add nodes additionNodeSet

let create_and = function
  | [] -> and_neutral
  | [node] -> node
  | nodes -> NodeSet.find_or_add nodes andNodeSet

let create_arcCosine node = NodeSet.find_or_add node arcCosineNodeSet

let create_arcHyperbolicCosine node =
  NodeSet.find_or_add node arcHyperbolicCosineNodeSet

let create_arcHyperbolicSine node =
  NodeSet.find_or_add node arcHyperbolicSineNodeSet

let create_arcHyperbolicTangent node =
  NodeSet.find_or_add node arcHyperbolicTangentNodeSet

let create_arcSine node = NodeSet.find_or_add node arcSineNodeSet

let create_arcTangent node = NodeSet.find_or_add node arcTangentNodeSet

let create_blackBox s nodes = NodeSet.find_or_add (s, nodes) blackBoxNodeSet

let create_booleanValue b = if b then true_value else false_value

let create_constant s = NodeSet.find_or_add s constantNodeSet

let create_cosine node = NodeSet.find_or_add node cosineNodeSet

let create_derivative node num =
  NodeSet.find_or_add (node, num) derivativeNodeSet

let create_discrete_variable i = NodeSet.find_or_add i discreteVariableNodeSet

let create_equality node node' =
  NodeSet.find_or_add (node, node') equalityNodeSet

let create_exponential node = NodeSet.find_or_add node exponentialNodeSet

let create_floor node =  NodeSet.find_or_add node floorNodeSet

let create_greater node node' = NodeSet.find_or_add (node, node') greaterNodeSet

let create_hyperbolicCosine node =
  NodeSet.find_or_add node hyperbolicCosineNodeSet

let create_hyperbolicSine node = NodeSet.find_or_add node hyperbolicSineNodeSet

let create_hyperbolicTangent node =
  NodeSet.find_or_add node hyperbolicTangentNodeSet

let create_if node node' node'' =
  NodeSet.find_or_add (node, node', node'') ifNodeSet

let create_logarithm node = NodeSet.find_or_add node logarithmNodeSet

let create_multiplication = function
  | [] -> multiplication_neutral
  | [node] -> node
  | nodes -> NodeSet.find_or_add nodes multiplicationNodeSet

let create_not node = NodeSet.find_or_add node notNodeSet

let create_number = function
  | Int 0 -> zero
  | Int 1 -> one
  | num -> NodeSet.find_or_add num numberNodeSet

let create_or = function
  | [] -> or_neutral
  | [node] -> node
  | nodes -> NodeSet.find_or_add nodes orNodeSet

let create_parameter i = NodeSet.find_or_add i parameterNodeSet

let create_partialDerivative node node' =
  NodeSet.find_or_add (node, node') partialDerivativeNodeSet

let create_rationalPower node num =
  NodeSet.find_or_add (node, num) rationalPowerNodeSet

let create_sign node = NodeSet.find_or_add node signNodeSet

let create_sine node = NodeSet.find_or_add node sineNodeSet

let create_tangent node = NodeSet.find_or_add node tangentNodeSet

let create_timeVariable () = time

let create_variable i = NodeSet.find_or_add i variableNodeSet


(* Reductions *)

let rec apply_if_possible create op node nodes =
  let rec apply_if_possible' = function
    | [] -> Some (create (insert node nodes))
    | node' :: nodes' ->
        begin match op node node' with
          | Some node'' ->
              apply_if_possible create op node'' (remove node' nodes)
          | None -> apply_if_possible' nodes'
        end
  in apply_if_possible' nodes

and symbolic_abs node =
  symbolic_if (symbolic_ge node zero) node (symbolic_minus node)

and symbolic_acos node =
  if node == minus_one then pi
  else if node == zero then pi_over_two
  else if node == one then zero
  else create_arcCosine node

and symbolic_acosh node =
  if node == one then zero
  else create_arcHyperbolicCosine node

and symbolic_add node node' = match symbolic_add_if_possible node node' with
  | Some node -> node
  | None -> create_addition (insert node [node'])

and symbolic_and node node' = match symbolic_and_if_possible node node' with
  | Some node -> node
  | None -> create_and (insert node [node'])

and symbolic_asin node =
  if node == minus_one then minus_pi_over_two
  else if node == zero then zero
  else if node == one then pi_over_two
  else create_arcSine node

and symbolic_asinh node =
  if node == zero then zero
  else create_arcHyperbolicSine node

and symbolic_atan node =
  if node == zero then zero
  else create_arcTangent node

and symbolic_atanh node =
  if node == zero then zero
  else create_arcHyperbolicTangent node

and symbolic_blackBox name nodes = create_blackBox name nodes

and symbolic_cos node =
  if node == zero then one
  else create_cosine node

and symbolic_cosh node =
  if node == zero then one
  else create_arcHyperbolicCosine node

and symbolic_derive node' num = match num with
  | Int 0 -> node'
  | Int n when n > 0 ->
      symbolic_derive (symbolic_derivative node') (pred_num num)
  | _ -> assert false

and symbolic_derivative node' =
  let ( + ) = symbolic_add
  and ( - ) = symbolic_sub
  and ( * ) = symbolic_mult
  and ( / ) = symbolic_div
  and ( ** ) = symbolic_rationalPower
  in match node'.nature with
    | Number _ | Constant _ | DiscreteVariable _ | Floor _ | Parameter _ |
      Sign _ -> zero
    | BlackBox _ | PartialDerivative _| Variable _ ->
        create_derivative node' one_num
    | Addition nodes ->
        List.fold_left (fun sum elt -> symbolic_derivative elt + sum) zero nodes
    | ArcCosine node ->
        symbolic_derivative node * minus_one /
        symbolic_sqrt (one - node ** two_num)
    | ArcHyperbolicCosine node ->
        symbolic_derivative node / symbolic_sqrt (node ** two_num - one)
    | ArcHyperbolicSine node ->
        symbolic_derivative node / symbolic_sqrt (node ** two_num + one)
    | ArcHyperbolicTangent node ->
        symbolic_derivative node / (node ** two_num - one)
    | ArcSine node ->
        symbolic_derivative node / symbolic_sqrt (one - node ** two_num)
    | ArcTangent node -> symbolic_derivative node / (one + node ** two_num)
    | Cosine node ->
        symbolic_derivative node * symbolic_minus (symbolic_sin node)
    | Derivative (node, num) -> create_derivative node (succ_num num)
    | Exponential node -> symbolic_derivative node * symbolic_exp node
    | HyperbolicCosine node -> symbolic_derivative node * symbolic_sinh node
    | HyperbolicSine node -> symbolic_derivative node * symbolic_cosh node
    | HyperbolicTangent node ->
        symbolic_derivative node * (one - node' ** two_num)
    | If (node, node', node'') ->
        create_if node (symbolic_derivative node') (symbolic_derivative node'')
    | Logarithm node -> symbolic_derivative node / node
    | Multiplication [] -> zero
    | Multiplication [node] -> symbolic_derivative node
    | Multiplication (node :: nodes) ->
        let mult = create_multiplication nodes in
        symbolic_derivative node * mult + node * symbolic_derivative mult
    | RationalPower (node, num) ->
        symbolic_derivative node * create_number num * node ** pred_num num
    | Sine node -> symbolic_derivative node * symbolic_cos node
    | Tangent node -> symbolic_derivative node * (one + node' ** two_num)
    | TimeVariable -> one
    | And _ | BooleanValue _ | Equality _ | Not _ | Or _ | Greater _ ->
        invalid_arg "symbolic_derivative"

and symbolic_div node node' =
  if node' == zero then raise (Infinite_result "Division by zero.")
  else if node == node' then one
  else symbolic_mult node (symbolic_invert node')

and symbolic_eq node node' =
  if node == node' then true_value
  else match node.nature, node'.nature with
    | Number num, Number num' -> create_booleanValue (eq_num num num')
    | _ -> create_equality node' node

and symbolic_exp node =
  if node == zero then one
  else if node == one then e
  else create_exponential node

and symbolic_ge node node' =
  symbolic_or (symbolic_gt node node') (symbolic_eq node node')

and symbolic_gt node node' = match node.nature, node'.nature with
  | Number num, Number num' -> create_booleanValue (gt_num num num')
  | _ -> create_greater node node'

and symbolic_if node node' node'' =
  if node' == node'' then node'
  else match node.nature with
    | BooleanValue b -> if b then node' else node''
    | Not bool -> symbolic_if bool node'' node'
    | _ -> create_if node node' node''

and symbolic_floor node = match node.nature with
  | Number num -> create_number (floor_num num)
  | Floor node' -> node
  | _ -> create_floor node

and symbolic_invert node =
  if node == zero then raise (Infinite_result "Division by zero.")
  else match node.nature with
    | Number num -> create_number (div_num (one_num) num)
    | Multiplication nodes ->
        create_multiplication (
          sort (List.map (fun elt -> symbolic_invert elt) nodes))
    | _ -> symbolic_power node minus_one

and symbolic_le node node' = symbolic_ge node' node

and symbolic_log node =
  if node == zero then raise (Infinite_result "Logarithm of zero.")
  else create_logarithm node

and symbolic_log10 node = symbolic_div (symbolic_log node) (symbolic_log ten)

and symbolic_lt node node' = symbolic_gt node' node

and symbolic_max node node' = symbolic_if (symbolic_gt node node') node node'

and symbolic_min node node' = symbolic_if (symbolic_gt node node') node' node

and symbolic_minus node =
  if node == zero then zero
  else match node.nature with
    | Number num -> create_number (minus_num num)
    | Addition nodes ->
        create_addition (
          sort (List.rev_map (fun elt -> symbolic_minus elt) nodes))
    | _ -> symbolic_mult minus_one node

and symbolic_mult node node' = match symbolic_mult_if_possible node node' with
  | Some node -> node
  | None -> create_multiplication (insert node [node'])

and symbolic_neq node node' = symbolic_not (symbolic_eq node node')

and symbolic_not node = match node.nature with
  | BooleanValue b -> create_booleanValue (not b)
  | Not bool -> bool
  | _ -> create_not node

and symbolic_or node node' = match symbolic_or_if_possible node node' with
  | Some node -> node
  | None -> create_or (insert node [node'])

and symbolic_partial_derivative var node' =
  symbolic_partial_derivative_with eps var node'

and symbolic_partial_derivative_with step var node' =
  let ( + ) = symbolic_add
  and ( - ) = symbolic_sub
  and ( * ) = symbolic_mult
  and ( / ) = symbolic_div
  and ( ** ) = symbolic_rationalPower in
  let partial_blackBox_derivatives s args =
    let dx_of_x x = step * (symbolic_if (symbolic_gt one (symbolic_abs x)) one (symbolic_abs x)) in
    let rec augmented_arguments_list = function
      | [] -> []
      | arg :: args ->
          (arg + dx_of_x arg :: args) :: List.map (fun aug_args -> arg :: aug_args) (augmented_arguments_list args)
    and partial_derivatives aug_argss =
      List.map2
        (fun aug_args arg -> (create_blackBox s aug_args - create_blackBox s args) / dx_of_x arg)
        aug_argss
        args
    in
    partial_derivatives (augmented_arguments_list args)
  in
  let rec partial_derivative node =
    if node == var then one
    else match node.nature with
      | Number _ | Constant _ | Derivative _ | DiscreteVariable _ | Floor _ |
        Parameter _ | Sign _ | TimeVariable | Variable _ -> zero
      | BlackBox (s, nodes) ->
          let dfs = partial_blackBox_derivatives s nodes in
          List.fold_left2 (fun acc node df -> acc + partial_derivative node * df) zero nodes dfs
      | PartialDerivative _ ->
          create_partialDerivative var node
      | Addition nodes ->
          List.fold_left (fun sum elt -> partial_derivative elt + sum) zero nodes
      | ArcCosine node' ->
          partial_derivative node' * minus_one /
          symbolic_sqrt (one - node' ** two_num)
      | ArcHyperbolicCosine node' ->
          partial_derivative node' / symbolic_sqrt (node' ** two_num - one)
      | ArcHyperbolicSine node' ->
          partial_derivative node' / symbolic_sqrt (node' ** two_num + one)
      | ArcHyperbolicTangent node' ->
          partial_derivative node' / (node ** two_num - one)
      | ArcSine node' ->
          partial_derivative node' / symbolic_sqrt (one - node' ** two_num)
      | ArcTangent node' -> partial_derivative node' / (one + node' ** two_num)
      | Cosine node' ->
          partial_derivative node' * symbolic_minus (symbolic_sin node')
      | Exponential node' -> partial_derivative node' * symbolic_exp node'
      | HyperbolicCosine node' -> partial_derivative node' * symbolic_sinh node'
      | HyperbolicSine node' -> partial_derivative node' * symbolic_cosh node'
      | HyperbolicTangent node' ->
          partial_derivative node' * (one - node' ** two_num)
      | If (cond, node', node'') ->
          symbolic_if cond (partial_derivative node') (partial_derivative node'')
      | Logarithm node' -> partial_derivative node' / node'
      | Multiplication [] -> zero
      | Multiplication [node'] -> partial_derivative node'
      | Multiplication (node' :: nodes) ->
          let mult = create_multiplication nodes in
          partial_derivative node' * mult + node' * partial_derivative mult
      | RationalPower (node', num) ->
          partial_derivative node' * create_number num * node' ** pred_num num
      | Sine node' -> partial_derivative node' * symbolic_cos node'
      | Tangent node' -> partial_derivative node' * (one + node ** two_num)
      | And _ | BooleanValue _ | Equality _ | Not _ | Or _ | Greater _ ->
          invalid_arg "partial_derivative : Invalid argument."
  in partial_derivative node'

and symbolic_power node node' = match node'.nature with
  | Number num' -> symbolic_rationalPower node num'
  | _ -> create_exponential (symbolic_mult node' (symbolic_log node))

and symbolic_rationalPower node num' =
  if node == zero && num' = zero_num then
    raise (Infinite_result "Zero raised to zero.")
  else if node == one || node == zero || num' = one_num then node
  else if num' = zero_num then one
  else match node.nature with
    | Number num when is_integer_num num' ->
        create_number (power_num num num')
    | RationalPower (node', num) ->
        symbolic_power node' (create_number (mult_num num num'))
    | _ -> create_rationalPower node num'

and symbolic_sgn node = match node.nature with
  | Number num -> create_number (Int (sign_num num))
  | _ -> create_sign node

and symbolic_sin node =
  if node == zero then zero
  else create_sine node

and symbolic_sinh node =
  if node == zero then zero
  else create_hyperbolicSine node

and symbolic_sqrt node = symbolic_power node one_over_two

and symbolic_sub node node' =
  if node' == zero then node
  else if node == node' then zero
  else symbolic_add node (symbolic_minus node')

and symbolic_tan node =
  if node == zero then zero
  else create_tangent node

and symbolic_tanh node =
  if node == zero then zero
  else create_hyperbolicTangent node


(* Intermediate functions *)

and symbolic_add_if_possible node node' =
  if node == zero then Some node'
  else if node' == zero then Some node
  else match node.nature, node'.nature with
    | Number num, Number num' -> Some (create_number (add_num num num'))
    | Number _, Addition nodes' ->
        add_number_to_addition_if_possible node nodes'
    | Multiplication _, Multiplication _ ->
        add_multiplication_to_multiplication_if_possible node node'
    | Multiplication _, Addition nodes' ->
        apply_if_possible create_addition symbolic_add_if_possible node nodes'
    | Multiplication _, _ ->
        add_expression_to_multiplication_if_possible node' node
    | Addition nodes, Number _ -> add_number_to_addition_if_possible node' nodes
    | Addition nodes, Multiplication _ ->
        apply_if_possible create_addition symbolic_add_if_possible node' nodes
    | Addition nodes, Addition _ ->
        Some (List.fold_left (fun sum elt -> symbolic_add elt sum) node' nodes)
    | Addition nodes, _ ->
        apply_if_possible create_addition symbolic_add_if_possible node' nodes
    | _, Multiplication _ ->
        add_expression_to_multiplication_if_possible node node'
    | _, Addition nodes' ->
        apply_if_possible create_addition symbolic_add_if_possible node nodes'
    | _ when node == node' -> Some (create_multiplication (insert two [node]))
    | _ -> None

and add_number_to_addition_if_possible node nodes =
  match node.nature, nodes with
    | Number _, [] -> Some node
    | Number num, node' :: nodes' ->
        begin match node'.nature with
          | Number num' -> let sum = add_num num num' in
              if sum == zero_num then Some (create_addition nodes')
              else Some (create_addition (insert (create_number sum) nodes'))
          | _ -> Some (create_addition (insert node nodes))
        end
    | _ -> invalid_arg "First Argument must be a number node."

and add_factors_if_possible num nodes num' nodes' =
  if eq_list nodes nodes' then
    let sum = create_number (add_num num num') in
      if sum == zero then Some zero
      else Some (create_multiplication (insert sum nodes))
  else None

and add_multiplication_to_multiplication_if_possible node node' =
  match node.nature, node'.nature with
    | Multiplication nodes, Multiplication nodes' ->
        begin match nodes, nodes' with
          | [], _ -> symbolic_add_if_possible one node'
          | _, [] -> symbolic_add_if_possible one node
          | node :: nodes2, node' :: nodes2' ->
              begin match node.nature, node'.nature with
                | Number num, Number num' ->
                    add_factors_if_possible num nodes2 num' nodes2'
                | Number num, _ ->
                    add_factors_if_possible num nodes2 one_num nodes'
                | _, Number num' ->
                    add_factors_if_possible one_num nodes num' nodes2'
                | _ ->
                    add_factors_if_possible one_num nodes one_num nodes'
              end
        end
    | _ -> invalid_arg "Arguments must be multiplication nodes."

and add_expression_to_multiplication_if_possible node node' =
  match node'.nature with
    | Multiplication [] -> symbolic_add_if_possible one node
    | Multiplication (node' :: nodes') ->
        begin match node'.nature with
          | Number num' -> add_factors_if_possible one_num [node] num' nodes'
          | _ -> None
        end
    | _ -> invalid_arg "Second argument must be a multiplication node."

and symbolic_mult_if_possible node node' =
  if node == zero || node' == zero then Some zero
  else if node == one then Some node'
  else if node' == one then Some node
  else match node.nature, node'.nature with
    | Number num, Number num' -> Some (create_number (mult_num num num'))
    | Number _, Addition nodes' ->
        Some (
          create_addition (
            sort (List.map (fun elt -> symbolic_mult node elt) nodes')))
    | Addition nodes, Number _ ->
        Some (
          create_addition (
            sort (List.map (fun elt -> symbolic_mult node' elt) nodes)))
    | Multiplication nodes, Multiplication _ ->
        Some (
          List.fold_left (fun prod elt -> symbolic_mult elt prod) node' nodes)
    | _, Multiplication nodes' ->
        apply_if_possible
          create_multiplication
          symbolic_mult_if_possible
          node
          nodes'
    | Multiplication nodes, _ ->
        apply_if_possible
          create_multiplication
          symbolic_mult_if_possible
          node'
          nodes
    | RationalPower _,RationalPower _ ->
        mult_power_by_power_if_possible node node'
    | RationalPower _, _ -> mult_expression_by_power_if_possible node' node
    | _, RationalPower _ -> mult_expression_by_power_if_possible node node'
    |_ when node == node' -> Some (symbolic_rationalPower node two_num)
    | _ -> None

and mult_expression_by_power_if_possible node node' = match node'.nature with
  | RationalPower (node', num') when node == node' ->
      Some (symbolic_rationalPower node (add_num one_num num'))
  | RationalPower _ -> None
  | _ -> invalid_arg "Second argument must be a power node."

and mult_power_by_power_if_possible node node' =
  match node.nature, node'.nature with
    | RationalPower (node, num), RationalPower (node', num')
      when node == node' ->
        Some (symbolic_rationalPower node (add_num num num'))
    | RationalPower _, RationalPower _ -> None
    | _ -> invalid_arg "Arguments must be power nodes."

and symbolic_and_if_possible node node' =
  if node == false_value || node' == false_value then Some false_value
  else if node == true_value then Some node'
  else if node' == true_value then Some node
  else if node == node' then Some node else
  match node.nature, node'.nature with
    | BooleanValue b, BooleanValue b' -> Some (create_booleanValue (b && b'))
    | Not bool, Not bool' -> Some (symbolic_not (symbolic_or bool bool'))
    | Not bool, _ when bool == node' -> Some false_value
    | _, Not bool' when node == bool' -> Some false_value
    | And nodes, And _ ->
        Some(List.fold_left (fun sum elt -> symbolic_and elt sum) node' nodes)
    | And nodes, _ ->
        apply_if_possible create_and symbolic_and_if_possible node' nodes
    | _, And nodes' ->
        apply_if_possible create_and symbolic_and_if_possible node nodes'
    | _ -> None

and symbolic_or_if_possible node node' =
  if node == true_value || node' == true_value then Some true_value
  else if node == false_value then Some node'
  else if node' == false_value then Some node
  else if node == node' then Some node else
  match node.nature, node'.nature with
    | BooleanValue b, BooleanValue b' -> Some (create_booleanValue (b || b'))
    | Not bool, Not bool' -> Some (symbolic_not (symbolic_and bool bool'))
    | Not bool, _ when bool == node' -> Some true_value
    | _, Not bool' when node == bool' -> Some true_value
    | Or nodes, Or _ ->
        Some(List.fold_left (fun sum elt -> symbolic_or elt sum) node' nodes)
    | Or nodes, _ ->
        apply_if_possible create_or symbolic_or_if_possible node' nodes
    | _, Or nodes' ->
        apply_if_possible create_or symbolic_or_if_possible node nodes'
    | _ -> None


(* Symbolic n-ary function application *)

and apply_addition nodes =
  List.fold_left (fun acc elt -> symbolic_add elt acc) addition_neutral nodes

and apply_and nodes =
  List.fold_left (fun acc elt -> symbolic_and elt acc) and_neutral nodes

and apply_blackBox s nodes = symbolic_blackBox s nodes

and apply_max = function
  | [] -> raise (Infinite_result "apply_max")
  | node :: nodes ->
      List.fold_left (fun acc elt -> symbolic_max elt acc) node nodes

and apply_min = function
  | [] -> raise (Infinite_result "apply_min")
  | node :: nodes ->
      List.fold_left (fun acc elt -> symbolic_min elt acc) node nodes

and apply_multiplication nodes =
  List.fold_left
    (fun acc elt -> symbolic_mult elt acc)
    multiplication_neutral
    nodes

and apply_or nodes =
  List.fold_left (fun acc elt -> symbolic_or elt acc) or_neutral nodes


(* Input/output *)

and output out_channel node =
  let mult_precedence = 50 in
  let rec precedence node = match node.nature with
    | Addition [] | And [] | BooleanValue _ | Constant _ | DiscreteVariable _ |
      Multiplication [] | Or [] | Parameter _ | TimeVariable | Variable _ ->
        1000
    | ArcCosine _ | ArcHyperbolicCosine _ | ArcHyperbolicSine _ |
      ArcHyperbolicTangent _ | ArcSine _ | ArcTangent _ | BlackBox _ |
      Cosine _ | Derivative _ | Exponential _ | Floor _ | HyperbolicCosine _ |
      HyperbolicSine _ | HyperbolicTangent _ | If _ | Logarithm _ | Not _ |
      PartialDerivative _ | Sign _ | Sine _| Tangent _ -> 10000
    | Addition _ -> 10
    | And _ -> 5
    | Equality _ -> 3
    | Multiplication _ -> mult_precedence
    | Number (Ratio _) -> mult_precedence
    | Number num when lt_num num zero_num -> 75
    | Number (Int _) | Number (Big_int _)  -> 1000
    | Or _ -> 7
    | RationalPower (_, num) when lt_num num zero_num -> mult_precedence
    | RationalPower _ -> 100
    | Greater _ -> 9
  and output'' node = match node.nature with
    | Addition [] -> output_char' '0'
    | Addition nodes' ->
      let not_negatives, negatives =
        List.partition
          (fun node -> match node.nature with
            | Number num when lt_num num zero_num -> false
            | Multiplication nodes ->
                begin
                  let not_reciprocals =
                    List.filter
                      (fun node -> match node.nature with
                        | RationalPower (_, num) when lt_num num zero_num ->
                            false
                        | _ -> true)
                      nodes
                  in match not_reciprocals with
                    | [] -> true
                    | node' :: _ ->
                        begin match node'.nature with
                          | Number num when lt_num num zero_num -> false
                          | _ -> true
                        end
                end
            | _ -> true)
          nodes'
      in
      begin match not_negatives with
        | [] -> ()
        | node' :: nodes' ->
            output' (precedence node) node';
            List.iter (fun elt -> output_string' " + ";
            output' (precedence node) elt) nodes'
      end;
      begin match negatives with
        | [] -> ()
        | nodes' -> List.iter (output' (precedence node)) nodes'
      end
    | And [] -> output_string' "true"
    | And (node' :: nodes') ->
        output' (precedence node) node';
        List.iter (fun elt -> output_string' " and ";
        output' (precedence node) elt) nodes'
    | ArcCosine node' -> output_string' "acos"; output' (precedence node) node'
    | ArcHyperbolicCosine node' ->
        output_string' "acosh"; output' (precedence node) node'
    | ArcHyperbolicSine node' ->
        output_string' "asinh"; output' (precedence node) node'
    | ArcHyperbolicTangent node' ->
        output_string' "atanh"; output' (precedence node) node'
    | ArcSine node' -> output_string' "asin"; output' (precedence node) node'
    | ArcTangent node' -> output_string' "atan"; output' (precedence node) node'
    | BlackBox (name, node' :: nodes') ->
      output_string' name; output_char' '('; output' 0 node';
      List.iter (fun elt -> output_string' ", "; output' 0 elt) nodes';
      output_char' ')'
    | BlackBox _ -> invalid_arg "Invalid black box node"
    | BooleanValue b -> output_string' (if b then "true" else "false")
    | Constant s -> output_string' s
    | Cosine node' -> output_string' "cos"; output' (precedence node) node'
    | Derivative (node', num') ->
        output_string' "der("; output' 0 node'; output_string' ", ";
        output_string' (string_of_num num'); output_char' ')'
    | Equality (node', node'') ->
        output' (precedence node) node'; output_string' " = ";
        output' (precedence node) node''
    | Exponential node' -> output_string' "exp"; output' (precedence node) node'
    | Floor node' -> output_string' "int"; output' (precedence node) node'
    | HyperbolicCosine node' ->
        output_string' "cosh"; output' (precedence node) node'
    | HyperbolicSine node' ->
        output_string' "sinh"; output' (precedence node) node'
    | HyperbolicTangent node' ->
        output_string' "tanh"; output' (precedence node) node'
    | If (node', node'', node''') ->
      output_string' "if("; output' 0 node';
      output_string' ", "; output' 0 node'';
      output_string' ", "; output' 0 node'''; output_char' ')'
    | DiscreteVariable i -> output_string' "discrete"; output_int' i
    | Logarithm node' -> output_string' "ln"; output' (precedence node) node'
    | Multiplication [] -> output_char' '1'
    | Multiplication nodes' ->
      let not_reciprocals, reciprocals = List.fold_left
        (fun (nodes, nodes') node -> match node.nature with
          | Number (Ratio _ as num) ->
            let ratio = ratio_of_num num in
            let numerator = num_of_big_int (Ratio.numerator_ratio ratio)
            and denominator = num_of_big_int (Ratio.denominator_ratio ratio) in
            if eq_num numerator one_num then
              nodes, (create_number denominator :: nodes')
            else
              (create_number numerator :: nodes),
              (create_number denominator :: nodes')
          | RationalPower (node'', num) when eq_num num (Int (-1)) ->
              nodes, (node'' :: nodes')
          | RationalPower (node'', num) when lt_num num zero_num ->
              nodes, (create_rationalPower node'' (minus_num num) :: nodes')
          | _ -> (node :: nodes), nodes')
        ([], [])
        nodes'
      in
      begin match List.rev not_reciprocals with
        | [] -> output_char' '1'
        | node' :: nodes' ->
            begin match node'.nature with
              | Number (Int -1) ->
                output_char' '-';
                begin match nodes' with
                  | [] -> output_char' '1'
                  | node' :: nodes' ->
                      output' (precedence node) node';
                      List.iter
                        (fun elt ->
                          output_string' " * "; output' (precedence node) elt)
                        nodes'
                end
              | _ ->
                output' (precedence node) node';
                List.iter
                  (fun elt ->
                    output_string' " * "; output' (precedence node) elt)
                  nodes'
            end
      end;
      begin match reciprocals with
        | [] -> ()
        | [node'] -> output_string' " / "; output' (precedence node + 1) node'
        | node' :: nodes' ->
            output_string' " / (";
            output' (precedence node) node';
            List.iter
              (fun elt -> output_string' " * "; output' (precedence node) elt)
              nodes';
            output_char' ')'
      end
    | Not node' -> output_string' "not"; output' (precedence node) node'
    | Number num -> output_string' (string_of_num num)
    | Or [] -> output_string' "false"
    | Or (node' :: nodes') ->
        output' (precedence node) node';
        List.iter
          (fun elt -> output_string' " or "; output' (precedence node) elt)
          nodes'
    | Parameter i ->
        output_string' "parameter("; output_int' i; output_char' ')'
    | PartialDerivative (node', node'') ->
        output_string' "pder("; output' 0 node'';
        output_string' ", "; output' 0 node'; output_char' ')'
    | RationalPower (node', num) when ge_num num zero_num ->
        output' (precedence node) node'; output_string' " ^ ";
        begin match num with
          | Int _ | Big_int _ -> output_string' (string_of_num num)
          | Ratio _ ->
              output_char' '('; output_string' (string_of_num num);
              output_char' ')'
        end
    | RationalPower (node', num) when eq_num num (Int (-1)) ->
        output_string' "1 / "; output' (mult_precedence + 1) node'
    | RationalPower (node', num) ->
        output_string' "1 / "; output' (precedence node) node';
        output_string' " ^ ";
        begin match num with
          | Int _ | Big_int _ -> output_string' (string_of_num (minus_num num))
          | Ratio _ ->
              output_char' '('; output_string' (string_of_num (minus_num num));
              output_char' ')'
        end
    | Sign node' -> output_string' "sgn"; output' (precedence node) node'
    | Sine node' -> output_string' "sin"; output' (precedence node) node'
    | Greater (node', node'') ->
        output' (precedence node) node'; output_string' " > ";
        output' (precedence node) node''
    | Tangent node' -> output_string' "tan"; output' (precedence node) node'
    | TimeVariable -> output_string' "time"
    | Variable i -> output_string' "variable("; output_int' i; output_char' ')'
  and output_char' c = output_char out_channel c
  and output_int' i = output_string out_channel (string_of_int i)
  and output_string' s = output_string out_channel s
  and output' prec node =
    if precedence node <= prec then begin
      output_string' "(";  output'' node; output_string' ")"
    end else output'' node
  in output' 0 node


(* Symbolic manipulation helpers *)

and exists p node =
  p node || match node.nature with
    | BooleanValue _  | Constant _ | DiscreteVariable _ | Number _ |
      Parameter _ | TimeVariable | Variable _ -> false
    | ArcCosine node | ArcHyperbolicCosine node |
      ArcHyperbolicSine node | ArcHyperbolicTangent node | ArcSine node |
      ArcTangent node | Cosine node | Derivative (node, _) |
      Exponential node | Floor node | HyperbolicCosine node |
      HyperbolicSine node | HyperbolicTangent node | Logarithm node |
      Not node | RationalPower (node, _) | Sign node | Sine node |
      Tangent node -> exists p node
    | Equality (node1, node2) | Greater (node1, node2) |
      PartialDerivative (node1, node2) -> exists p node1 || exists p node2
    | If (node1, node2, node3) ->
        exists p node1 || exists p node2 || exists p node3
    | And nodes | Addition nodes | BlackBox (_, nodes) | Multiplication nodes |
      Or nodes -> List.exists (exists p) nodes

and is_subnode_of node node' = exists (fun node -> node == node') node

and variables_of node = match node.nature with
  | BooleanValue _  | Constant _ | DiscreteVariable _ | Number _ | Parameter _ |
    TimeVariable -> []
  | Variable _ -> [node]
  | ArcCosine node | ArcHyperbolicCosine node |
    ArcHyperbolicSine node | ArcHyperbolicTangent node | ArcSine node |
    ArcTangent node | Cosine node | Derivative (node, _) |
    Exponential node | Floor node | HyperbolicCosine node |
    HyperbolicSine node | HyperbolicTangent node | Logarithm node |
    Not node | RationalPower (node, _) | Sign node | Sine node |
    Tangent node -> variables_of node
  | Equality (node1, node2) | Greater (node1, node2) |
    PartialDerivative (node1, node2) ->
      union (variables_of node1) (variables_of node2)
  | If (node1, node2, node3) ->
      union
        (variables_of node1)
        (union (variables_of node2) (variables_of node3))
  | And nodes | Addition nodes | BlackBox (_, nodes) | Multiplication nodes |
    Or nodes ->
      List.fold_left (fun acc node -> union (variables_of node) acc) [] nodes

and assignable_variables_of node = match node.nature with
  | BooleanValue _  | Constant _ | DiscreteVariable _ | Number _ | Parameter _ |
    TimeVariable -> []
  | Variable _ -> [node]
  | ArcCosine node | ArcHyperbolicCosine node |
    ArcHyperbolicSine node | ArcHyperbolicTangent node | ArcSine node |
    ArcTangent node | Cosine node | Derivative (node, _) |
    Exponential node | Floor node | HyperbolicCosine node |
    HyperbolicSine node | HyperbolicTangent node | Logarithm node |
    RationalPower (node, _) | Sign node | Sine node | Tangent node ->
      assignable_variables_of node
  | Equality (node1, node2) | Greater (node1, node2) |
    PartialDerivative (node1, node2) ->
      union (assignable_variables_of node1) (assignable_variables_of node2)
  | If (_, node1, node2) ->
      union (* intersection is too pessimistic (since v.1.1.4. *)
        (assignable_variables_of node1)
        (assignable_variables_of node2)
  | Addition nodes | BlackBox (_, nodes) | Multiplication nodes ->
      List.fold_left
        (fun acc node -> union (assignable_variables_of node) acc)
        []
        nodes
  | And _ | Or _ | Not _ -> []

and derivatives_of node = match node.nature with
  | BooleanValue _  | Constant _ | DiscreteVariable _ | Number _ | Parameter _ |
    TimeVariable | Variable _ -> []
  | ArcCosine node' | ArcHyperbolicCosine node' |
    ArcHyperbolicSine node' | ArcHyperbolicTangent node' | ArcSine node' |
    ArcTangent node' | Cosine node' | Exponential node' | Floor node' |
    HyperbolicCosine node' | HyperbolicSine node' | HyperbolicTangent node' |
    Logarithm node' | Not node' | RationalPower (node', _) | Sign node' |
    Sine node' | Tangent node' -> derivatives_of node'
  | Derivative _ -> [node]
  | Equality (node1, node2) | Greater (node1, node2) | If (_, node1, node2) |
    PartialDerivative (node1, node2) ->
      union (derivatives_of node1) (derivatives_of node2)
  | And nodes | Addition nodes | BlackBox (_, nodes) | Multiplication nodes |
    Or nodes ->
      List.fold_left (fun acc node -> union (derivatives_of node) acc) [] nodes

and invert_if_possible_with_respect_to node left right =
  let not_null node = match node.nature with
    | Constant _ -> true
    | Number num -> num <>/ zero_num
    | _ -> false
  in
  let invert_addition_if_possible nodes =
    match List.partition (fun node' -> is_subnode_of node' node) nodes with
      | [node'], nodes' -> Some (node', nodes')
      | [], _ -> invalid_arg "invert_addition_if_possible"
      | _ -> None
  and invert_multiplication_if_possible nodes =
    match List.partition (fun node' -> is_subnode_of node' node) nodes with
      | [node'], nodes' when List.for_all not_null nodes' -> Some (node', nodes')
      | [], _ -> invalid_arg "invert_multiplication_if_possible"
      | _ -> None
  in
  if node == left then Some right
  else match left.nature with
    | BlackBox _ | Cosine _ | Derivative _ | Floor _ | HyperbolicCosine _ |
      PartialDerivative _ | Sign _ | Sine _ | Tangent _ -> None
    | Addition nodes ->
        begin match invert_addition_if_possible nodes with
          | None -> None
          | Some (node', nodes') ->
              invert_if_possible_with_respect_to
                node
                node'
                (symbolic_sub right (create_addition (sort nodes')))
        end
    | ArcCosine node' ->
        invert_if_possible_with_respect_to node node' (symbolic_cos right)
    | ArcHyperbolicCosine node' ->
        invert_if_possible_with_respect_to node node' (symbolic_cosh right)
    | ArcHyperbolicSine node' ->
        invert_if_possible_with_respect_to node node' (symbolic_sinh right)
    | ArcHyperbolicTangent node' ->
        invert_if_possible_with_respect_to node node' (symbolic_tanh right)
    | ArcSine node' ->
        invert_if_possible_with_respect_to node node' (symbolic_sin right)
    | ArcTangent node' ->
        invert_if_possible_with_respect_to node node' (symbolic_tan right)
    | Exponential node' ->
        invert_if_possible_with_respect_to node node' (symbolic_log right)
    | HyperbolicSine node' ->
        invert_if_possible_with_respect_to node node' (symbolic_asinh right)
    | HyperbolicTangent node' ->
        invert_if_possible_with_respect_to node node' (symbolic_atanh right)
    | If (cond, node', node'') -> None
    | Logarithm node' ->
        invert_if_possible_with_respect_to node node' (symbolic_exp right)
    | Multiplication nodes ->
        begin match invert_multiplication_if_possible nodes with
          | None -> None
          | Some (node', nodes') ->
              invert_if_possible_with_respect_to
                node
                node'
                (symbolic_div right (create_multiplication (sort nodes')))
        end
    | RationalPower (node', num)
      when is_integer_num num && eq_num (mod_num num two_num) zero_num -> None
    | RationalPower (node', num)
      when is_integer_num num ->
        invert_if_possible_with_respect_to
          node
          node'
          (symbolic_rationalPower right (minus_num num))
    | RationalPower _ -> None
    | And _ | Constant _ | BooleanValue _ | Equality _ | Greater _ |
      DiscreteVariable _ | Not _ | Number _ | Or _ | Parameter _ |
      TimeVariable | Variable _ ->
        invalid_arg "invert_if_possible_with_respect_to"

and exists_except_in_conditions p node =
  p node || match node.nature with
    | BooleanValue _  | Constant _ | DiscreteVariable _ | Number _ |
      Parameter _ | TimeVariable | Variable _ -> false
    | ArcCosine node | ArcHyperbolicCosine node |
      ArcHyperbolicSine node | ArcHyperbolicTangent node | ArcSine node |
      ArcTangent node | Cosine node | Derivative (node, _) |
      Exponential node | Floor node | HyperbolicCosine node |
      HyperbolicSine node | HyperbolicTangent node | Logarithm node |
      Not node | RationalPower (node, _) | Sign node | Sine node |
      Tangent node -> exists_except_in_conditions p node
    | Equality (node1, node2) | Greater (node1, node2) | If (_, node1, node2) |
      PartialDerivative (node1, node2) ->
        exists_except_in_conditions p node1 ||
        exists_except_in_conditions p node2
    | And nodes | Addition nodes | BlackBox (_, nodes) | Multiplication nodes |
      Or nodes -> List.exists (exists_except_in_conditions p) nodes

and inversion_difficulty node left right =
  let is_derivative_of_node node' = match node'.nature with
    | Derivative (node', _) -> node' == node
    | _ -> false
  in match derivatives_of left with
    | ders when List.exists is_derivative_of_node ders -> 0
    | [] ->
        begin
          try match invert_if_possible_with_respect_to node left right with
            | Some _ -> 1
            | None -> 2
          with
            | Invalid_argument _ -> 3
        end
    | _ -> 2

and replace node node' node'' =
  let rec rewrite node'' =
    if node'' == node then node'
    else match node''.nature with
    | Addition nodes -> apply_addition (List.map rewrite nodes)
    | And nodes -> apply_and (List.map rewrite nodes)
    | ArcCosine node -> symbolic_acos (rewrite node)
    | ArcHyperbolicCosine node -> symbolic_acosh (rewrite node)
    | ArcHyperbolicSine node -> symbolic_asinh (rewrite node)
    | ArcHyperbolicTangent node -> symbolic_atanh (rewrite node)
    | ArcSine node -> symbolic_asin (rewrite node)
    | ArcTangent node -> symbolic_atan (rewrite node)
    | BlackBox (s, nodes) -> apply_blackBox s (List.map rewrite nodes)
    | Cosine node -> symbolic_cos (rewrite node)
    | Derivative (node, num) -> symbolic_derive (rewrite node) num
    | Equality (node, node') -> symbolic_eq (rewrite node) (rewrite node')
    | Exponential node -> symbolic_exp (rewrite node)
    | Floor node -> symbolic_floor (rewrite node)
    | Greater (node, node') -> symbolic_gt (rewrite node) (rewrite node')
    | HyperbolicCosine node -> symbolic_cosh (rewrite node)
    | HyperbolicSine node -> symbolic_sinh (rewrite node)
    | HyperbolicTangent node -> symbolic_tanh (rewrite node)
    | If (node, node', node'') ->
        symbolic_if (rewrite node) (rewrite node') (rewrite node'')
    | Logarithm node -> symbolic_log (rewrite node)
    | Multiplication nodes -> apply_multiplication (List.map rewrite nodes)
    | Not node -> symbolic_not (rewrite node)
    | Or nodes -> apply_or (List.map rewrite nodes)
    | PartialDerivative (node, node') ->
        create_partialDerivative (rewrite node) (rewrite node')
    | RationalPower (node, num) -> symbolic_rationalPower (rewrite node) num
    | Sign node -> symbolic_sgn (rewrite node)
    | Sine node -> symbolic_sin (rewrite node)
    | Tangent node -> symbolic_tan (rewrite node)
    | BooleanValue _ | Constant _ | DiscreteVariable _ | Number _ |
      Parameter _ | TimeVariable | Variable _ -> node''
  in
  rewrite node''

(*and replace node node' node'' =
  let rec rewrite node =
    if node.count = !global_count then
      node.replacement
    else
      let node' = replace' node in
      node.count <- !global_count;
      node.replacement <- node';
      node'
  and replace' node = match node.nature with
    | Addition nodes -> apply_addition (List.map rewrite nodes)
    | And nodes -> apply_and (List.map rewrite nodes)
    | ArcCosine node -> symbolic_acos (rewrite node)
    | ArcHyperbolicCosine node -> symbolic_acosh (rewrite node)
    | ArcHyperbolicSine node -> symbolic_asinh (rewrite node)
    | ArcHyperbolicTangent node -> symbolic_atanh (rewrite node)
    | ArcSine node -> symbolic_asin (rewrite node)
    | ArcTangent node -> symbolic_atan (rewrite node)
    | BlackBox (s, nodes) -> apply_blackBox s (List.map rewrite nodes)
    | Cosine node -> symbolic_cos (rewrite node)
    | Derivative (node, num) -> symbolic_derive (rewrite node) num
    | Equality (node, node') -> symbolic_eq (rewrite node) (rewrite node')
    | Exponential node -> symbolic_exp (rewrite node)
    | Floor node -> symbolic_floor (rewrite node)
    | Greater (node, node') -> symbolic_gt (rewrite node) (rewrite node')
    | HyperbolicCosine node -> symbolic_cosh (rewrite node)
    | HyperbolicSine node -> symbolic_sinh (rewrite node)
    | HyperbolicTangent node -> symbolic_tanh (rewrite node)
    | If (node, node', node'') ->
        symbolic_if (rewrite node) (rewrite node') (rewrite node'')
    | Logarithm node -> symbolic_log (rewrite node)
    | Multiplication nodes -> apply_multiplication (List.map rewrite nodes)
    | Not node -> symbolic_not (rewrite node)
    | Or nodes -> apply_or (List.map rewrite nodes)
    | PartialDerivative (node, node') ->
        create_partialDerivative (rewrite node) (rewrite node')
    | RationalPower (node, num) -> symbolic_rationalPower (rewrite node) num
    | Sign node -> symbolic_sgn (rewrite node)
    | Sine node -> symbolic_sin (rewrite node)
    | Tangent node -> symbolic_tan (rewrite node)
    | BooleanValue _ | Constant _ | DiscreteVariable _ | Number _ |
      Parameter _ | TimeVariable | Variable _ -> node.replacement
  in
  incr global_count;
  assert (!global_count <> 0);
  node.count <- !global_count;
  node.replacement <- node';
  let rewritten_node = rewrite node'' in
  node.replacement <- node;
  rewritten_node
*)
