package li.cil.oc.server.fs

import java.io
import java.io.FileNotFoundException
import li.cil.oc.api.fs.Mode
import net.minecraft.nbt.{NBTTagList, NBTTagCompound}
import scala.collection.mutable

trait VirtualFileSystem extends OutputStreamFileSystem {
  protected val root = new VirtualDirectory

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

  def exists(path: String) =
    root.get(segments(path)).isDefined

  def isDirectory(path: String) =
    root.get(segments(path)) match {
      case Some(obj) => obj.isDirectory
      case _ => false
    }

  def size(path: String) =
    root.get(segments(path)) match {
      case Some(obj) => obj.size
      case _ => 0L
    }

  def lastModified(path: String) =
    root.get(segments(path)) match {
      case Some(obj) => obj.lastModified
      case _ => 0L
    }

  def list(path: String) =
    root.get(segments(path)) match {
      case Some(obj: VirtualDirectory) => obj.list()
      case _ => null
    }

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

  override def delete(path: String) = {
    val parts = segments(path)
    if (parts.isEmpty) false
    else {
      root.get(parts.dropRight(1)) match {
        case Some(parent: VirtualDirectory) => parent.delete(parts.last)
        case _ => false
      }
    }
  }

  override def makeDirectory(path: String) = {
    val parts = segments(path)
    if (parts.isEmpty) false
    else {
      root.get(parts.dropRight(1)) match {
        case Some(parent: VirtualDirectory) => parent.makeDirectory(parts.last)
        case _ => false
      }
    }
  }

  override def rename(from: String, to: String) =
    if (from == "" || !exists(from)) throw new FileNotFoundException()
    else if (!exists(to)) {
      val segmentsTo = segments(to)
      root.get(segmentsTo.dropRight(1)) match {
        case Some(toParent: VirtualDirectory) =>
          val toName = segmentsTo.last
          val segmentsFrom = segments(from)
          val fromParent = root.get(segmentsFrom.dropRight(1)).get.asInstanceOf[VirtualDirectory]
          val fromName = segmentsFrom.last
          val obj = fromParent.children(fromName)

          fromParent.children -= fromName
          fromParent.lastModified = System.currentTimeMillis()

          toParent.children += toName -> obj
          toParent.lastModified = System.currentTimeMillis()

          obj.lastModified = System.currentTimeMillis()
          true
        case _ => false
      }
    }
    else false

  override def setLastModified(path: String, time: Long) =
    root.get(segments(path)) match {
      case Some(obj) if time >= 0 =>
        obj.lastModified = time
        true
      case _ => false
    }

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

  protected def openInputStream(path: String) =
    root.get(segments(path)) match {
      case Some(obj: VirtualFile) => obj.openInputStream()
      case _ => None
    }

  protected def openOutputStream(path: String, mode: Mode) = {
    val parts = segments(path)
    if (parts.isEmpty) None
    else {
      root.get(parts.dropRight(1)) match {
        case Some(parent: VirtualDirectory) => parent.openOutputStream(parts.last, mode)
        case _ => None
      }
    }
  }

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

  override def load(nbt: NBTTagCompound) = {
    if (!this.isInstanceOf[Buffered]) root.load(nbt)
    super.load(nbt) // Last to ensure streams can be re-opened.
  }

  override def save(nbt: NBTTagCompound) = {
    super.save(nbt) // First to allow flushing.
    if (!this.isInstanceOf[Buffered]) root.save(nbt)
  }

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

  protected def segments(path: String) = path.split("/").view.filter(_ != "")

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

  protected trait VirtualObject {
    def isDirectory: Boolean

    def size: Long

    var lastModified = System.currentTimeMillis()

    def load(nbt: NBTTagCompound) {
      if (nbt.hasKey("lastModified"))
        lastModified = nbt.getLong("lastModified")
    }

    def save(nbt: NBTTagCompound) {
      nbt.setLong("lastModified", lastModified)
    }

    def get(path: Iterable[String]): Option[VirtualObject] =
      if (path.isEmpty) Some(this) else None

    def canDelete: Boolean
  }

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

  protected class VirtualFile extends VirtualObject {
    val data = mutable.ArrayBuffer.empty[Byte]

    var stream: Option[VirtualFileOutputStream] = None

    def isDirectory = false

    def size = data.length

    def openInputStream() = Some(new VirtualFileInputStream(this))

    def openOutputStream(mode: Mode) =
      if (stream.isDefined) None
      else {
        if (mode == Mode.Write) {
          data.clear()
          lastModified = System.currentTimeMillis()
        }
        stream = Some(new VirtualFileOutputStream(this))
        stream
      }

    override def load(nbt: NBTTagCompound) {
      super.load(nbt)
      data.clear()
      data ++= nbt.getByteArray("data")
    }

    override def save(nbt: NBTTagCompound) {
      super.save(nbt)
      nbt.setByteArray("data", data.toArray)
    }

    override def canDelete = stream.isEmpty
  }

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

  protected class VirtualDirectory extends VirtualObject {
    val children = mutable.Map.empty[String, VirtualObject]

    def isDirectory = true

    def size = 0

    def list() = children.map {
      case (childName, child) => if (child.isDirectory) childName + "/" else childName
    }.toArray

    def makeDirectory(name: String) =
      if (children.contains(name)) false
      else {
        children += name -> new VirtualDirectory
        lastModified = System.currentTimeMillis()
        true
      }

    def delete(name: String) = {
      children.get(name) match {
        case Some(child) if child.canDelete =>
          children -= name
          lastModified = System.currentTimeMillis()
          true
        case _ => false
      }
    }

    def openOutputStream(name: String, mode: Mode) =
      children.get(name) match {
        case Some(obj: VirtualFile) => obj.openOutputStream(mode)
        case None =>
          val child = new VirtualFile
          children += name -> child
          lastModified = System.currentTimeMillis()
          child.openOutputStream(mode)
        case _ => None
      }

    override def load(nbt: NBTTagCompound) {
      super.load(nbt)
      val childrenNbt = nbt.getTagList("children")
      (0 until childrenNbt.tagCount).map(childrenNbt.tagAt).map(_.asInstanceOf[NBTTagCompound]).foreach(childNbt => {
        val child =
          if (childNbt.getBoolean("isDirectory")) new VirtualDirectory
          else new VirtualFile
        child.load(childNbt)
        children += childNbt.getString("name") -> child
      })
    }

    override def save(nbt: NBTTagCompound) {
      super.save(nbt)
      val childrenNbt = new NBTTagList()
      for ((childName, child) <- children) {
        val childNbt = new NBTTagCompound()
        childNbt.setBoolean("isDirectory", child.isDirectory)
        childNbt.setString("name", childName)
        child.save(childNbt)
        childrenNbt.appendTag(childNbt)
      }
      nbt.setTag("children", childrenNbt)
    }

    override def get(path: Iterable[String]) =
      super.get(path) orElse {
        children.get(path.head) match {
          case Some(child) => child.get(path.drop(1))
          case _ => None
        }
      }

    override def canDelete = children.isEmpty
  }

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

  protected class VirtualFileInputStream(val file: VirtualFile) extends io.InputStream {
    private var isClosed = false

    private var position = 0

    override def available() =
      if (isClosed) 0
      else math.max(file.data.length - position, 0)

    override def close() = isClosed = true

    def read() =
      if (!isClosed) {
        if (available == 0) -1
        else {
          position += 1
          file.data(position - 1)
        }
      }
      else throw new io.IOException("file is closed")

    override def read(b: Array[Byte], off: Int, len: Int) =
      if (!isClosed) {
        val count = available()
        if (count == 0) -1
        else {
          val n = math.min(len, count)
          file.data.view(position, file.data.length).copyToArray(b, off, n)
          position += n
          n
        }
      }
      else throw new io.IOException("file is closed")

    override def reset() =
      if (!isClosed) {
        position = 0
      }
      else throw new io.IOException("file is closed")

    override def skip(n: Long) =
      if (!isClosed) {
        position = math.min((position + n).toInt, Int.MaxValue)
        position
      }
      else throw new io.IOException("file is closed")
  }

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

  protected class VirtualFileOutputStream(val file: VirtualFile) extends io.OutputStream {
    private var isClosed = false

    override def close() = if (!isClosed) {
      isClosed = true
      file.stream = None
    }

    override def write(b: Array[Byte], off: Int, len: Int) =
      if (!isClosed) {
        file.data ++= b.view(off, off + len)
        file.lastModified = System.currentTimeMillis()
      }
      else throw new io.IOException("file is closed")

    override def write(b: Int) =
      if (!isClosed) {
        file.data += b.toByte
        file.lastModified = System.currentTimeMillis()
      }
      else throw new io.IOException("file is closed")
  }

}
