///|
/// The non-mutating methods of the builtin types, which a profile opens.
///
/// PurePy has no methods at all, and that absence is load-bearing: the
/// evaluator's attribute rule has no case for a builtin value, which is what
/// makes `xs.append(1)` unreachable and therefore what makes a list immutable.
/// A profile that added methods carelessly would take that away.
///
/// It cannot, and the reason is worth stating rather than trusting. A `Value`
/// is an immutable enum with no identity. A method here is a function from a
/// receiver and some arguments to a NEW value; there is no cell to write to
/// and no aliasing that could observe a write, so `append` cannot be
/// implemented as mutation even by mistake. The tables below are `match name`
/// with a `_ => None` default, so the mutating half of Python's API --
/// `append`, `extend`, `insert`, `pop`, `remove`, `sort`, `reverse`, `clear`,
/// `update`, `setdefault` -- is absent rather than denied, and absent means
/// the same `Stuck` an unknown attribute has always produced.
///
/// One trap, written down because the names are one letter apart: `sorted(xs)`
/// answers a new list and `xs.sort()` mutates. The builtin is a profile away;
/// the method never will be.
///
/// `None` means "no such method". The caller turns it into the message an
/// unknown attribute already has, so a typo reads the same as it always did.

///|
/// `s.upper()`, `s.split(",")`, and the rest of `str`.
pub fn str_method(
  recv : String,
  name : String,
  args : Array[Value],
) -> Outcome? {
  match (name, args) {
    ("upper", []) => Some(Val(Str(recv.to_upper())))
    ("lower", []) => Some(Val(Str(recv.to_lower())))
    ("strip", []) => Some(Val(Str(recv.trim().to_owned())))
    ("lstrip", []) =>
      Some(Val(Str(recv.trim_start(chars=" \t\n\r").to_owned())))
    ("rstrip", []) => Some(Val(Str(recv.trim_end(chars=" \t\n\r").to_owned())))
    ("startswith", [Str(p)]) => Some(Val(Bool(recv.has_prefix(p))))
    ("endswith", [Str(p)]) => Some(Val(Bool(recv.has_suffix(p))))
    ("replace", [Str(a), Str(b)]) =>
      Some(Val(Str(recv.replace_all(old=a, new=b))))
    ("split", [Str(sep)]) =>
      if sep.is_empty() {
        // Python raises ValueError, which PurePy does not model.
        Some(Stuck("splitting on an empty separator"))
      } else {
        Some(Val(List(split_on(recv, sep).map(fn(t) { Value::Str(t) }))))
      }
    // `s.split()` with no argument splits on runs of whitespace and drops the
    // empty pieces, which is a different rule and not a default argument.
    ("split", []) =>
      Some(Val(List(split_space(recv).map(fn(t) { Value::Str(t) }))))
    ("join", [xs]) =>
      match elems(xs) {
        None => Some(Aborts(TypeError))
        Some(parts) => {
          let out : Array[String] = []
          for x in parts {
            match x {
              Str(t) => out.push(t)
              _ => return Some(Aborts(TypeError))
            }
          }
          Some(Val(Str(out.join(recv))))
        }
      }
    ("count", [Str(t)]) => Some(Val(Int(BigInt::from_int(count_of(recv, t)))))
    ("find", [Str(t)]) => Some(Val(Int(BigInt::from_int(index_of(recv, t)))))
    ("index", [Str(t)]) => {
      let i = index_of(recv, t)
      // `find` answers -1 and `index` raises. Python's is a ValueError; the
      // nearest thing PurePy models is nothing, so this is undefined.
      Some(
        if i < 0 {
          Stuck("index of a substring that is not there")
        } else {
          Val(Int(BigInt::from_int(i)))
        },
      )
    }
    ("isdigit", []) =>
      Some(Val(Bool(all_chars(recv, fn(c) { c >= '0' && c <= '9' }))))
    ("isalpha", []) =>
      Some(
        Val(
          Bool(
            all_chars(recv, fn(c) {
              (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
            }),
          ),
        ),
      )
    ("isspace", []) =>
      Some(
        Val(
          Bool(
            all_chars(recv, fn(c) {
              c == ' ' || c == '\t' || c == '\n' || c == '\r'
            }),
          ),
        ),
      )
    _ => None
  }
}

///|
/// `xs.index(x)`, `xs.count(x)`: what a list and a tuple share, which is
/// everything about them that does not mutate.
pub fn seq_method(
  recv : Array[Value],
  name : String,
  args : Array[Value],
) -> Outcome? {
  match (name, args) {
    ("count", [x]) => {
      let mut n = 0
      for y in recv {
        match eq(x, y) {
          Some(true) => n += 1
          Some(false) => ()
          // `eq` is partial on purpose, and a count that skipped the pairs it
          // does not cover would be answering a different question.
          None =>
            return Some(
              Stuck("counting " + x.kind_name() + " among " + y.kind_name()),
            )
        }
      }
      Some(Val(Int(BigInt::from_int(n))))
    }
    ("index", [x]) => {
      for i, y in recv {
        match eq(x, y) {
          Some(true) => return Some(Val(Int(BigInt::from_int(i))))
          Some(false) => ()
          None =>
            return Some(
              Stuck("comparing " + x.kind_name() + " with " + y.kind_name()),
            )
        }
      }
      Some(Stuck("index of an element that is not there"))
    }
    _ => None
  }
}

///|
/// `d.get(k)`, `d.keys()`: a dictionary, read and never written.
///
/// `keys`, `values` and `items` answer with a list rather than a view, as
/// `range` answers with a list rather than a range. Everything that consumes
/// one agrees; printing one directly does not.
pub fn dict_method(
  recv : Array[(String, Value)],
  name : String,
  args : Array[Value],
) -> Outcome? {
  match (name, args) {
    ("get", [Str(k)]) =>
      Some(
        Val(
          match lookup(recv, k) {
            Some(v) => v
            None => Value::None
          },
        ),
      )
    ("get", [Str(k), fallback]) =>
      Some(
        Val(
          match lookup(recv, k) {
            Some(v) => v
            None => fallback
          },
        ),
      )
    // Python's `get` takes any hashable key and PurePy's dictionaries are
    // keyed by strings alone, so anything else has nowhere to look.
    ("get", _) => Some(Stuck("a dict key that is not a str"))
    ("keys", []) => Some(Val(List(recv.map(fn(e) { Value::Str(e.0) }))))
    ("values", []) => Some(Val(List(recv.map(fn(e) { e.1 }))))
    ("items", []) =>
      Some(Val(List(recv.map(fn(e) { Value::Tuple([Value::Str(e.0), e.1]) }))))
    _ => None
  }
}

// ---------------------------------------------------------------------------

///|
fn all_chars(s : String, p : (Char) -> Bool) -> Bool {
  // Python's `isdigit` and friends are False for the empty string.
  if s.is_empty() {
    return false
  }
  for c in s {
    if !p(c) {
      return false
    }
  }
  true
}

///|
/// The code-point index of `needle` in `haystack`, or -1.
fn index_of(haystack : String, needle : String) -> Int {
  let hs = haystack.to_array()
  let ns = needle.to_array()
  if ns.is_empty() {
    return 0
  }
  if ns.length() > hs.length() {
    return -1
  }
  for i in 0..<=(hs.length() - ns.length()) {
    let mut same = true
    for j, c in ns {
      if hs[i + j] != c {
        same = false
        break
      }
    }
    if same {
      return i
    }
  }
  -1
}

///|
/// Non-overlapping occurrences, as Python counts them.
fn count_of(haystack : String, needle : String) -> Int {
  let hs = haystack.to_array()
  let ns = needle.to_array()
  if ns.is_empty() {
    return hs.length() + 1
  }
  let mut n = 0
  let mut i = 0
  while i + ns.length() <= hs.length() {
    let mut same = true
    for j, c in ns {
      if hs[i + j] != c {
        same = false
        break
      }
    }
    if same {
      n += 1
      i += ns.length()
    } else {
      i += 1
    }
  }
  n
}

///|
/// `s.split(sep)`: every piece between separators, empties included.
fn split_on(s : String, sep : String) -> Array[String] {
  let out : Array[String] = []
  let hs = s.to_array()
  let ns = sep.to_array()
  let piece = StringBuilder()
  let mut i = 0
  while i < hs.length() {
    let mut same = i + ns.length() <= hs.length()
    if same {
      for j, c in ns {
        if hs[i + j] != c {
          same = false
          break
        }
      }
    }
    if same {
      out.push(piece.to_string())
      piece.reset()
      i += ns.length()
    } else {
      piece.write_char(hs[i])
      i += 1
    }
  }
  out.push(piece.to_string())
  out
}

///|
/// `s.split()`: runs of whitespace, and no empty pieces at all.
fn split_space(s : String) -> Array[String] {
  let out : Array[String] = []
  let piece = StringBuilder()
  for c in s {
    if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
      if !piece.is_empty() {
        out.push(piece.to_string())
        piece.reset()
      }
    } else {
      piece.write_char(c)
    }
  }
  if !piece.is_empty() {
    out.push(piece.to_string())
  }
  out
}