// Examples of alternate types/union types/heterogeneous types.
class Foo {
  x: f64;
}

class Bar {
 y: f64;
}

fun extract_value(v: Foo or Bar) -> f64 {
  if v is Foo {
    v.x // Magic type binding!
  } else if v is Bar {
    v.y // Same magic re-binding of the variable to the new type
  } // No error; exhaustivity analysis should work.
}

// This is Bob Nystrom's example from
// https://journal.stuffwithstuff.com/2023/08/04/representing-heterogeneous-data/
//
class MeleeWeapon {
  damage: f64;
}

class RangedWeapon {
  minRange: f64;
  maxRange: f64;
}

class Monster {
  health: f64;
}

fun print(x:string) {}

fun in_range(weapon: MeleeWeapon or RangedWeapon, distance: f64) {
  if weapon is w : RangedWeapon {
    distance >= w.minRange and distance <= w.maxRange
  } else {
    distance == 1
  }
}

fun attack(weapon: MeleeWeapon or RangedWeapon, monster: Monster, distance: f64) {
  // This is worse than Bob's final version but but it works. `is` operator
  // should be the same precedence as `and` and left-associative, so the
  // `and` that follows the declaration in the `is` still has the variables
  // from the `is` binding in scope.
  if weapon is MeleeWeapon and distance > 1 or
     weapon is w : RangedWeapon and (distance < w.minRange or distance > w.maxRange) {
     print("You are out of range")
     return
  }

  // Bob says he doesn't want to do flow analysis and we're not, we're using
  // scoping rules and associativity to make the re-bindings work.
  //
  // NOTE: special syntax here: `identifier` is `TypeExpression` ALMOST means
  //       `identifier is identifier : TypeExpression` as the shorthand for checking
  //       local variables. The *almost* part is that the effective type of the
  //       variable changes but not the binding. (Is this what we want?)
  //
  // TODO: What do we do about captured variables?
  //
  let damage = if weapon is w: MeleeWeapon {
    roll_dice(w.damage)
  } else if weapon is w: RangedWeapon {
    w.maxRange - w.minRange
  };

  if monster.health <= damage {
    print("You kill the monster!");
    monster.health = 0
  } else {
    print("You wound the monster.");
    monster.health = monster.health - damage
  }
}

fun more_examples(weapon: MeleeWeapon or RangedWeapon) -> f64 or () {
  if weapon is w: RangedWeapon and w.maxRange > 10 {
    // w is still in scope here; the `and` is bound into a predicate expression
    // and breaks exhaustivity
    w.minRange
  }
}

// Some fun with iterators
class Finished {}
let FINISHED = new Finished {}

class Iterator {
  current: f64;

  fun next(self) -> f64 or Finished {
    if self.current < 10 {
      let result = self.current;
      self.current = self.current + 1;
      return result;
    }

    FINISHED
  }
}

fun test() -> f64 {
  let sum = 0;

  // A single step of an iterator...
  let it = new Iterator { current: 0 };
  if it.next() is v: f64 {
    sum = sum + v;
  }

  // Looping by hand over an iterator is pretty clean.
  let it = new Iterator { current: 0 };
  while it.next() is v: f64 {
    sum = sum + v;
  }

  // Unroll by hand...
  let it = new Iterator { current: 0 };
  loop {
    if it.next() is v: f64 {
      sum = sum + v;
    } else {
      return sum;
    }
  }

  // Not in this test but `for` over an object should turn into something
  // like the above.
}

// @ignore WIP
// @no-errors