scala/ScalaBook/chapter-09/src/main/scala/scalabook/path/Path.scala

package scalabook.path

trait Path {
  def name: String
  def fullName: String
  def relative: Path

  def isEmpty: Boolean
  def isRoot: Boolean
  def isUNC: Boolean
  def isAbsolute: Boolean = !isRelative
  def isRelative: Boolean = !isAbsolute

  def /(that: String): Path
  def /(that: Path): Path

  def parent: Path

  def parts: List[String]
  def pathParts = parts.map(Path(_))

  def compare(that: Path) = this.fullName compare that.fullName

  override def hashCode = fullName.hashCode
  override def toString = fullName
  override def equals(any: Any) =
    any.isInstanceOf[Path] &&
      any.asInstanceOf[Path].fullName == this.fullName

  def splitName = {
    val dotPos = name.indexOf('.')
    if(-1 == dotPos || 0 == dotPos) // no extension for .special files :)
      (name, "")
    else
      (name.substring(0, dotPos), name.substring(dotPos + 1))
  }

  def extension = splitName._2
  def bareName  = splitName._1

  def isChildOf(that: Path) =
    // So that Path("/").isChildOf("") returns false
    fullName.length > that.fullName.length &&
    that.fullName.length > 0 &&
    0 == this.parent.compare(that)
}

object EmptyPath extends Path {
  def name = ""
  def fullName = ""
  def relative = this

  def isEmpty = true
  def isRoot = false
  def isUNC = false
  override def isAbsolute = false
  override def isRelative = false

  def parts = Nil
  def parent = this

  def /(that: String) = Path(that)
  def /(that: Path) = that

  override def isChildOf(that: Path) = false
}

object Path {
  type GenericPath = UnixPath
  
  implicit def string2PathWrapper(path: String) = new PathWrapper(path)
  implicit def symbol2PathWrapper(path: Symbol) = new PathWrapper(path.name)

  val isWindows = System.getProperty("os.name").toLowerCase.contains("windows")

  def isBackSlash(ch: Char) = '\\' == ch
  def isSlash(ch: Char) = '/' == ch
  def isAnySlash(ch: Char) = isSlash(ch) || isBackSlash(ch)

  def getSlashF(anySlash: Boolean) =
    if(anySlash) isAnySlash _ else isSlash _

  def countPrefixSlashes(path: String, startIndex: Int) = {
    var index = startIndex
    while(index < path.length && isSlash(path.charAt(index)))
      index += 1
    index - startIndex
  }

  def firstNonSlashIndex(path: String, startIndex: Int) = {
    var index = startIndex
    while(index < path.length && !isSlash(path.charAt(index)))
      index += 1
    index
  }

  def previousSlashIndex(path: String, endIndex: Int) = {
    var index = endIndex
    while(index > 0 && !isSlash(path.charAt(index)))
      index -= 1
    index
  }

  def lastNonSlashIndex(path: String, anySlash: Boolean) = {
    val isSlashF = getSlashF(anySlash)
    var index = path.length - 1
    while(index > 0 && isSlashF(path.charAt(index)))
      index -= 1
    index
  }

  def lastNonSlashIndex2(path: String, anySlash: Boolean) = {
    val isSlashF = getSlashF(anySlash)
    def discoverIndex(index: Int): Int =
      if(index <= 0)
        -1
      else if(isSlashF(path.charAt(index)))
        discoverIndex(index - 1)
      else
        index

    discoverIndex(path.length - 1)
  }

  def removeRedundantSlashes(path: String, startIndex: Int, endIndex: Int, anySlash: Boolean) = {
    val sb = new StringBuilder
    var index = startIndex
    var previousWasSlash = false
    val isSlashF = getSlashF(anySlash)

    while(startIndex <= index && index <= endIndex) {
      val ch = path.charAt(index)

      if(isSlashF(ch)) {
        if(!previousWasSlash) {
          sb.append('/')
          previousWasSlash = true
        }
      } else {
        sb.append(ch)
        previousWasSlash = false
      }

      index += 1
    }
    
    sb.toString
  }

  def isDriveLetter(ch: Char) =
    'a' <= ch && ch <= 'z' ||
    'A' <= ch && ch <= 'Z'

  def parseSimplePath(path: String, anySlash: Boolean) =
    new UnixPath(removeRedundantSlashes(path, 0, lastNonSlashIndex(path, anySlash), anySlash))

  def parseUnixPath(path: String): UnixPath =
    parseSimplePath(path, false)

  def parseWinPath(path: String) = {
    val len = path.length
    val ch0 = path.charAt(0)
    len match {
      case 1 =>
        parseSimplePath(path, true)
      
      case _ =>
        val ch1 = path.charAt(1)
        if(isAnySlash(ch0) && isAnySlash(ch1))
          new UNCPath(
            "//" +
            removeRedundantSlashes(
              path, 2, lastNonSlashIndex(path, true), true))
        else if(len > 2 &&
                isDriveLetter(ch0) &&
                ':' == ch1 &&
                isAnySlash(path.charAt(2))) {
          val prefix = path.substring(0, 2) // C:
          val rest = path.substring(2)
          val suffix = removeRedundantSlashes(
            rest, 0, lastNonSlashIndex(rest, true), true)
          new DriveAbsPath(prefix + suffix)
        } else
          parseSimplePath(path, true)
    }
  }

  def apply(path: String): Path =
    if("" equals path)
        EmptyPath
    else if(isWindows)
      parseWinPath(path)
    else
      parseUnixPath(path)

  def combine(a: Path, b: String): Path = combine(a, Path(b))

  def combine(a: Path, b: Path) = {
    (a.isAbsolute, b.isAbsolute) match {
      case (_, true) => error("Cannot concatenate path <%s> with absolute path <%s>".format(a, b))
      case (_, false) => Path(a.fullName + "/" + b.fullName)
    }
  }

  object UnixPath {
    def apply(path: String): Path =
      if("" equals path)
        EmptyPath
      else
        parseUnixPath(path)
  }
}