///|
fn numeric_operator(
  operator : @ast.BinaryOp,
  left : Double,
  right : Double,
) -> Double raise @model.PromqlError {
  match operator {
    @ast.BinaryOp::Add => left + right
    @ast.BinaryOp::Sub => left - right
    @ast.BinaryOp::Mul => left * right
    @ast.BinaryOp::Div => left / right
    @ast.BinaryOp::Mod => left % right
    @ast.BinaryOp::Pow => @math.pow(left, right)
    _ => raise @model.EvalError("operator is not arithmetic")
  }
}

///|
fn comparison_operator(
  operator : @ast.BinaryOp,
  left : Double,
  right : Double,
) -> Bool raise @model.PromqlError {
  match operator {
    @ast.BinaryOp::Eq => left == right
    @ast.BinaryOp::Neq => left != right
    @ast.BinaryOp::Gt => left > right
    @ast.BinaryOp::Lt => left < right
    @ast.BinaryOp::Gte => left >= right
    @ast.BinaryOp::Lte => left <= right
    _ => raise @model.EvalError("operator is not a comparison")
  }
}

///|
fn is_comparison(operator : @ast.BinaryOp) -> Bool {
  match operator {
    @ast.BinaryOp::Eq
    | @ast.BinaryOp::Neq
    | @ast.BinaryOp::Gt
    | @ast.BinaryOp::Lt
    | @ast.BinaryOp::Gte
    | @ast.BinaryOp::Lte => true
    _ => false
  }
}

///|
fn is_set_operator(operator : @ast.BinaryOp) -> Bool {
  match operator {
    @ast.BinaryOp::And | @ast.BinaryOp::Or | @ast.BinaryOp::Unless => true
    _ => false
  }
}

///|
fn signature(
  labels : @model.LabelSet,
  vector_match : @ast.VectorMatch?,
) -> @model.LabelSet {
  match vector_match {
    None => labels.remove("__name__")
    Some({ on: true, labels: names, .. }) => labels.only(names)
    Some({ on: false, labels: names, .. }) => {
      let without : Array[String] = ["__name__"]
      for name in names {
        without.push(name)
      }
      labels.without(without)
    }
  }
}

///|
fn map_instant_scalar(
  points : Array[@model.Point],
  scalar : Double,
  scalar_on_left : Bool,
  operator : @ast.BinaryOp,
  bool_modifier : Bool,
) -> Array[@model.Point] raise @model.PromqlError {
  let output : Array[@model.Point] = []
  for point in points {
    let left = if scalar_on_left { scalar } else { point.value }
    let right = if scalar_on_left { point.value } else { scalar }
    if is_comparison(operator) {
      let matched = comparison_operator(operator, left, right)
      if bool_modifier {
        output.push({
          labels: point.labels.remove("__name__"),
          value: if matched {
            1.0
          } else {
            0.0
          },
        })
      } else if matched {
        output.push({
          labels: point.labels.remove("__name__"),
          value: point.value,
        })
      }
    } else {
      output.push({
        labels: point.labels.remove("__name__"),
        value: numeric_operator(operator, left, right),
      })
    }
  }
  output
}

///|
fn apply_set_operator(
  operator : @ast.BinaryOp,
  left : Array[@model.Point],
  right : Array[@model.Point],
  vector_match : @ast.VectorMatch?,
) -> Array[@model.Point] {
  let right_keys : Array[String] = []
  for point in right {
    right_keys.push(signature(point.labels, vector_match).to_string())
  }
  let output : Array[@model.Point] = []
  if operator is @ast.BinaryOp::Or {
    for point in left {
      output.push(point)
    }
    for point in right {
      let key = signature(point.labels, vector_match).to_string()
      let mut exists = false
      for left_point in left {
        if signature(left_point.labels, vector_match).to_string() == key {
          exists = true
          break
        }
      }
      if !exists {
        output.push(point)
      }
    }
  } else {
    for point in left {
      let key = signature(point.labels, vector_match).to_string()
      let mut found = false
      for right_key in right_keys {
        if right_key == key {
          found = true
          break
        }
      }
      if operator is @ast.BinaryOp::And && found {
        output.push(point)
      }
      if operator is @ast.BinaryOp::Unless && !found {
        output.push(point)
      }
    }
  }
  output
}

///|
fn apply_vector_vector(
  operator : @ast.BinaryOp,
  left : Array[@model.Point],
  right : Array[@model.Point],
  bool_modifier : Bool,
  vector_match : @ast.VectorMatch?,
) -> Array[@model.Point] raise @model.PromqlError {
  if is_set_operator(operator) {
    return apply_set_operator(operator, left, right, vector_match)
  }
  let output : Array[@model.Point] = []
  for left_point in left {
    let left_key = signature(left_point.labels, vector_match).to_string()
    for right_point in right {
      let right_key = signature(right_point.labels, vector_match).to_string()
      if left_key != right_key {
        continue
      }
      let mut labels = match vector_match {
        Some({ side: @ast.GroupSide::GroupRight, .. }) => right_point.labels
        _ => left_point.labels
      }
      match vector_match {
        Some({ side: @ast.GroupSide::GroupLeft, include_labels, .. }) =>
          for name in include_labels {
            if right_point.labels.get(name) is Some(value) {
              labels = labels.set(name, value)
            }
          }
        Some({ side: @ast.GroupSide::GroupRight, include_labels, .. }) =>
          for name in include_labels {
            if left_point.labels.get(name) is Some(value) {
              labels = labels.set(name, value)
            }
          }
        _ => ()
      }
      if is_comparison(operator) {
        let matched = comparison_operator(
          operator,
          left_point.value,
          right_point.value,
        )
        if bool_modifier {
          output.push({
            labels: labels.remove("__name__"),
            value: if matched {
              1.0
            } else {
              0.0
            },
          })
        } else if matched {
          output.push({ labels, value: left_point.value, })
        }
      } else {
        output.push({
          labels: labels.remove("__name__"),
          value: numeric_operator(operator, left_point.value, right_point.value),
        })
      }
    }
  }
  output.sort_by((left_point, right_point) => {
    left_point.labels.to_string().compare(right_point.labels.to_string())
  })
  output
}