// accept
// Fortis v0.1.0: a coffee shop's orders, read from text, checked, priced and summarised.
import fortis.host { Console }
import fortis.collections { Deque, SortedSet }

const maxQuantity: Int = 20

// ---- Types that carry their rules -------------------------------------------------------------------

// A newtype: its own type over Int<0..>, never mixed up with a plain Int.
export type Pence = Int<0..>

// An enum that converts from text by name, ignoring ASCII case.
export enum Size derive Equatable, Hashable, Comparable, Displayable, convert from String {
  Small
  Medium
  Large
}

export error BadOrder {
  code: "shop.bad_order"
  payload { reason: String }
}

export interface Priced {
  pure func price(self) Pence
}

// A mixin lends methods; it requires methods, never an interface.
export mixin Labelled {
  requires pure func name(self) String
  requires pure func price(self) Pence

  export pure func label(self) String => "${self.name()}: ${self.price()}p"
}

// A contract names a set of effect rules once and is used like a modifier.
export contract Pricing {
  pure
  forbids recovers
}

export class Drink implements Priced with Labelled {
  export size: Size
  export quantity: Int<1..maxQuantity>      // a refinement with a const bound

  export pure func name(self) String => "${self.quantity} x ${self.size} coffee"

  export Pricing pure func price(self) Pence {
    let each: Int<150..350> = match self.size {
      Small => 150
      Medium => 250
      Large => 350
    }
    return Pence(each * self.quantity)      // proved in range, so no check is needed
  }

  // A conversion the type declares: "large, 2" as Drink.
  export try convert from String(line) {
    let parts = line.split(",")
    if parts.count() != 2 {
      fail BadOrder(reason: "expected size, quantity")
    }
    let size = (parts.get(0) ?? "").trim() as Size or fail
    let n = (parts.get(1) ?? "").trim() as Int or fail
    return Drink(size, quantity: n as Int<1..maxQuantity> or fail)
  }
}

// A class that implements an interface through an `implement` block.
export class Pastry {
  export name: String
  export pence: Pence
}

implement Priced for Pastry {
  pure func price(self) Pence => self.pence
}

// A field-by-field conversion, derived.
export class Ticket derive convert from Drink {
  export size: Size
  export quantity: Int<1..maxQuantity>
}

// A newtype over a collection: reading methods everywhere, editing only in this module.
export type Basket = List[Drink]

// A generic class that may hold a linear value, such as an outcome, and must hand it on.
export class Pending[T] where T: linear {
  item: T

  export func take(owned self) T {
    let Pending(item: value) = move self
    return move value
  }
}

// Range patterns.
export pure func band(total: Pence) String {
  return match total as Int {               // a newtype unwraps with `as` in its own module
    0..<500 => "small"
    500..<2000 => "medium"
    otherwise => "large"
  }
}

// Generics with bounds.
export pure func larger[T](a: T, b: T) T where T implements Comparable, T implements Copyable {
  if a > b {
    return copy a
  }
  return copy b
}

try func addCount(sum: Int, text: StringView) Int {
  let n = text.trim() as Int or fail
  return Int.saturatingAdd(sum, n)
}

try func checkLine(console: Console, line: String) {
  let d = line as Drink or fail
  console.print("checked ${d.label()}")
}

pure func costOf(items: List[dynamic Priced]) Int {
  return items.fold(0, fn(sum, item) => Int.saturatingAdd(sum, item.price() as Int))
}

// ---- The program ------------------------------------------------------------------------------------

try recovers func main(console: Console) {
  let orders = "large, 2\nsmall, 6\nmedium, 1\nhuge, 3\nlarge, many\nsmall, 40\n" as String

  // Traditional: a for loop, a match on the outcome, a report for each bad line.
  let mut basket = Basket(List.default())
  for line in orders.lines() {
    match line as Drink {
      success(drink) => basket.push(move drink)
      failure(f) => {
        console.print("skipped '${line}'")
        report f
      }
    }
  }

  // Functional: lambdas written into a call work out what they capture.
  basket.each(fn(drink) => console.print(drink.label()))
  let total = basket.fold(0, fn(sum, drink) => Int.saturatingAdd(sum, drink.price() as Int))
  console.print("total ${total}p, any large? ${basket.any(fn(d) => d.size == Large)}")
  console.print("all under 20? ${basket.all(fn(d) => d.quantity < 20)}, small orders ${basket.countWhere(fn(d) => d.size == Small)}")
  let cheapest = basket.sortedBy(fn(d) => d.price())
  console.print("cheapest first: ${cheapest.first()?.label() ?? "none"}")
  console.print("first large at ${basket.indexWhere(fn(d) => d.size == Large) ?? -1}")
  basket.groupBy(fn(d) => d.size).each(fn(size, drinks) => console.print("${size}: ${drinks.count()} order(s)"))
  basket.map(fn(d) => d.size).enumerate().each(fn(pair) => console.print("#${pair.0} ${pair.1}"))

  // A lambda that changes a variable outside it.
  let mut cups = 0
  basket.each(fn(d) {
    cups = Int.saturatingAdd(cups, d.quantity)
  })
  console.print("cups ${cups}, larger size ${larger(Size.Small, Medium)}")

  // Failing callbacks: tryMap, tryEach and tryFold stop at the first failure.
  let counts = "3, 4, 5".split(",").tryMap(fn(t) => t.trim() as Int) or fail
  let sum = "3, 4, 5".split(",").tryFold(0, fn(acc, t) => addCount(acc, t)) or fail
  "small, 1\nlarge, 2".lines().tryEach(fn(line) => checkLine(console, line)) or fail
  console.print("parsed ${counts.count()} counts, sum ${sum}, both lines valid")

  // Helpers on T? and Outcome[T].
  let firstPrice = basket.first().map(fn(d) => d.price() as Int) ?? 0
  let doubled = ("21" as Int).map(fn(n) => Int.saturatingAdd(n, n)) or recover 0
  console.print("first price ${firstPrice}, doubled ${doubled}")

  // Conversions: derived, unwrapped, checked.
  let ticket = basket.first()?.size ?? Small
  let firstTicket = Drink(size: Large, quantity: 2) as Ticket
  let totalPence = total as Pence or recover Pence(0)
  console.print("ticket ${firstTicket.size} x ${firstTicket.quantity}, size ${ticket}, band ${band(totalPence)}")

  // Interfaces through `dynamic`, with a class that implements one by an `implement` block.
  let till: List[dynamic Priced] = List.of(Drink(size: Small, quantity: 1), Pastry(name: "croissant", pence: Pence(180)))
  console.print("till ${costOf(till)}p")

  // A linear value inside a generic class still has to be handled.
  let pending = Pending(item: "medium, 1" as Drink)
  let late = move pending.take() or fail
  console.print("late order ${late.label()}")

  // More collection types.
  let mut queue: Deque[String] = Deque.default()
  queue.pushBack("Ann" as String)
  queue.pushBack("Bo" as String)
  queue.pushFront("Cy" as String)
  let served = queue.popFront() ?? ("nobody" as String)
  console.print("served ${served}, next ${queue.peekFront() ?? "nobody"}, waiting ${queue.count()}")
  let sizes = SortedSet.fromList(basket.map(fn(d) => d.size))
  console.print("sizes from ${sizes.first() ?? Small} to ${sizes.last() ?? Large}")
  let mut shelf = Array[Int, 3].filled(0)
  console.print("shelf slot 1 holds ${shelf.get(1) ?? -1}, view of ${shelf.asView().count()}")

  // Maps and sets, the traditional way.
  let mut seen: Set[Size] = Set.default()
  let mut cupsBySize: Map[Size, Int] = Map.default()
  for d in basket {
    discard seen.add(d.size)
    cupsBySize.set(d.size, Int.saturatingAdd(cupsBySize.get(d.size) ?? 0, d.quantity))
  }
  let mut wanted: Set[Size] = Set.default()
  discard wanted.add(Large)
  discard wanted.add(Medium)
  console.print("${seen.count()} sizes, large cups ${cupsBySize.get(Large) ?? 0}, both ${seen.intersection(wanted).count()}")
  console.print("keys ${cupsBySize.keys().count()}, cups ${cupsBySize.values().fold(0, fn(a, n) => Int.saturatingAdd(a, n))}")

  // Text.
  let receipt = String.join(basket.map(fn(d) => d.label()), " | ")
  console.print(receipt.asciiUpper())
  console.print("has small? ${receipt.contains("Small")}, starts ${receipt.startsWith("2 x")}, at ${receipt.find("|") ?? 0}")
  console.print(receipt.replace(" coffee", "").repeat(1))
  console.print("-".repeat(20))

  let mut spare = List.of(1, 2, 3)
  spare.clear()
  console.print("spare ${spare.count()}")
}

// ---- Tests ------------------------------------------------------------------------------------------

test "a large coffee costs 350p a cup" {
  expect Drink(size: Large, quantity: 2).price() == Pence(700)
}

test "lines that are not orders fail" cases {
  "unknown size": { line: "huge, 1" }
  "not a number": { line: "large, many" }
  "too many": { line: "small, 40" }
} as row {
  let ok = row.line as Drink catch e {
    report e
    recover Drink(size: Small, quantity: 1)
  }
  expect ok.size == Size.Small
}

property "every quantity in range prices above zero" forall (n: Int<1..20>) {
  expect Drink(size: Small, quantity: n).price() as Int > 0
}
