Corral

use "files"
use "collections"

class val Locator is (Hashable & Stringable)
  """
  Encapsulation of the bundle dependency's locator string, parsed into distinct
  fields.
    locator := repo_path[.vcs_suffix][/bundle_path]
  """
  let repo_path: String
  let vcs_suffix: String
  let bundle_path: String

  new val create(loc: String) =>
    let suffixes = [".git"; ".hg"; ".bzr"; ".svn"]
    var rp = ""
    var vs = ""
    var bp = ""
    for s in suffixes.values() do
      let parts = loc.split_by(s)
      if parts.size() == 2 then try
        rp = parts(0)?
        vs = s
        let p = parts(1)?
        bp = if p.at("/") then p.trim(1) else p end
        break
      end end
    end
    if vs == "" then
      bp = loc  // With no vcs, the locator is the local bundle path
    end
    repo_path = rp
    vcs_suffix = vs
    bundle_path = bp

  fun path(): String =>
    """Returns a unique name for this locator without the vcs suffix."""
    Path.to_slash(Path.join(repo_path, bundle_path))

  fun flat_name(): String =>
    _Flattened(path())

  fun string(): String iso^ =>
    """Returns the full string for of this locator."""
    Path.to_slash(Path.join(repo_path + vcs_suffix, bundle_path)).clone()

  fun compare(that: Locator box): Compare =>
    if (repo_path != that.repo_path) then return repo_path.compare(that.repo_path) end
    if (vcs_suffix != that.vcs_suffix) then return vcs_suffix.compare(that.vcs_suffix) end
    bundle_path.compare(that.bundle_path)

  fun hash(): USize =>
    repo_path.hash() xor vcs_suffix.hash() xor bundle_path.hash()

  fun is_vcs(): Bool => vcs_suffix != ""

  fun is_local(): Bool => (repo_path == "") or repo_path.at(".") or repo_path.at("/")

  fun is_remote_vcs(): Bool => is_vcs() and not is_local()

  fun is_local_vcs(): Bool => is_vcs() and is_local()

  fun is_local_direct(): Bool => not is_vcs() and is_local()

primitive _Flattened
  fun apply(path: String): String val =>
    let dash_code: U8 = '_'
    let path_name_arr = recover val
      var acc: Array[U8] = Array[U8]
      for char in path.array().values() do
        if _is_alphanum(char) then
          acc.push(char)
        else
          try // we know we don't index out of bounds
            if acc.size() == 0 then
              acc.push(dash_code)
            elseif acc(acc.size() - 1)? != dash_code then
              acc.push(dash_code)
            end
          end
        end
      end
      consume acc
    end
    String.from_array(consume path_name_arr)

  fun _is_alphanum(c: U8): Bool =>
    (('0' <= c) and (c <= '9'))
      or
    (('A' <= c) and (c <= 'Z'))
      or
    (('a' <= c) and (c <= 'z'))
      or
    (c == '_')