package ru.dbotthepony.kstarbound

import com.google.common.collect.ImmutableMap
import com.google.gson.stream.JsonReader
import ru.dbotthepony.kstarbound.io.StarboundPak
import ru.dbotthepony.kstarbound.util.sbIntern
import java.io.BufferedInputStream
import java.io.File
import java.io.FileNotFoundException
import java.io.InputStream
import java.io.InputStreamReader
import java.io.Reader
import java.nio.ByteBuffer
import java.util.stream.Stream

fun interface ISBFileLocator {
	fun locate(path: String): IStarboundFile
	fun exists(path: String): Boolean = locate(path).exists

	/**
	 * @throws IllegalStateException if file is a directory
	 * @throws FileNotFoundException if file does not exist
	 */
	fun read(path: String) = locate(path).read()

	/**
	 * @throws IllegalStateException if file is a directory
	 * @throws FileNotFoundException if file does not exist
	 */
	fun readDirect(path: String) = locate(path).readDirect()

	/**
	 * @throws IllegalStateException if file is a directory
	 * @throws FileNotFoundException if file does not exist
	 */
	fun open(path: String) = locate(path).open()

	/**
	 * @throws IllegalStateException if file is a directory
	 * @throws FileNotFoundException if file does not exist
	 */
	fun reader(path: String) = locate(path).reader()

	/**
	 * @throws IllegalStateException if file is a directory
	 * @throws FileNotFoundException if file does not exist
	 */
	@Deprecated("This does not reflect json patches")
	fun jsonReader(path: String) = locate(path).jsonReader()
}

/**
 * Two files should be considered equal if they have same absolute path
 */
interface IStarboundFile : ISBFileLocator {
	val exists: Boolean
	val isDirectory: Boolean

	/**
	 * null if root
	 */
	val parent: IStarboundFile?
	val isFile: Boolean

	/**
	 * null if not a directory
	 */
	val children: Map<String, IStarboundFile>?
	val name: String

	fun orNull(): IStarboundFile? = if (exists) this else null

	operator fun get(name: String): IStarboundFile? {
		return children?.get(name)
	}

	fun explore(): Stream<IStarboundFile> {
		val children = children ?: return Stream.of(this)
		return Stream.concat(Stream.of(this), children.values.stream().flatMap { it.explore() })
	}

	fun explore(visitor: (IStarboundFile) -> Unit) {
		visitor(this)
		children?.values?.forEach { it.explore(visitor) }
	}

	fun computeFullPath(): String {
		var path = name
		var parent = parent

		while (parent != null) {
			path = parent.name + "/" + path
			parent = parent.parent
		}

		return path
	}

	fun computeDirectory(): String {
		var path = ""
		var parent = parent

		while (parent != null) {
			if (path == "")
				path = parent.name
			else
				path = parent.name + "/" + path

			parent = parent.parent
		}

		return path
	}

	override fun locate(path: String): IStarboundFile {
		@Suppress("name_shadowing")
		val path = path.trim()

		if (path == "" || path == ".") {
			return this
		}

		if (path == "..") {
			return parent ?: NonExistingFile(computeFullPath() + "/..")
		}

		val split = path.lowercase().split("/")
		var file = this

		for (splitIndex in split.indices) {
			if (split[splitIndex].isEmpty()) {
				continue
			}

			val children = file.children ?: return NonExistingFile(name = split.last(), fullPath = computeFullPath() + "/" + path)
			val find = children[split[splitIndex]]

			if (find is StarboundPak.SBDirectory) {
				file = find
			} else if (find is StarboundPak.SBFile) {
				if (splitIndex + 1 != split.size) {
					return NonExistingFile(name = split.last(), fullPath = computeFullPath() + "/" + path)
				}

				return find
			} else {
				break
			}
		}

		return NonExistingFile(name = split.last(), fullPath = computeFullPath() + "/" + path)
	}

	/**
	 * @throws IllegalStateException if file is a directory
	 * @throws FileNotFoundException if file does not exist
	 */
	fun open(): InputStream

	/**
	 * @throws IllegalStateException if file is a directory
	 * @throws FileNotFoundException if file does not exist
	 */
	fun reader(): Reader = InputStreamReader(BufferedInputStream(open()))

	/**
	 * @throws IllegalStateException if file is a directory
	 * @throws FileNotFoundException if file does not exist
	 */
	@Deprecated("Careful! This does not reflect json patches")
	fun jsonReader(): JsonReader = JsonReader(reader()).also { it.isLenient = true }

	/**
	 * @throws IllegalStateException if file is a directory
	 * @throws FileNotFoundException if file does not exist
	 */
	fun read(): ByteBuffer {
		val stream = open()
		val read = stream.readAllBytes()
		stream.close()
		return ByteBuffer.wrap(read)
	}

	/**
	 * @throws IllegalStateException if file is a directory
	 * @throws FileNotFoundException if file does not exist
	 */
	fun readToString(): String {
		val stream = open()
		val read = stream.readAllBytes()
		stream.close()
		return String(read, charset = Charsets.UTF_8)
	}

	/**
	 * @throws IllegalStateException if file is a directory
	 * @throws FileNotFoundException if file does not exist
	 */
	fun readDirect(): ByteBuffer {
		val read = read()
		val buf = ByteBuffer.allocateDirect(read.capacity())

		read.position(0)

		for (i in 0 until read.capacity()) {
			buf.put(read[i])
		}

		buf.position(0)

		return buf
	}

	/**
	 * non existent file, without any context
	 */
	companion object : IStarboundFile {
		override val exists: Boolean
			get() = false
		override val isDirectory: Boolean
			get() = false
		override val parent: IStarboundFile?
			get() = null
		override val isFile: Boolean
			get() = false
		override val children: Map<String, IStarboundFile>?
			get() = null
		override val name: String
			get() = ""

		override fun open(): InputStream {
			throw FileNotFoundException()
		}
	}
}

class NonExistingFile(
	override val name: String,
	override val parent: IStarboundFile? = null,
	val fullPath: String? = null
) : IStarboundFile {
	override val isDirectory: Boolean
		get() = false
	override val isFile: Boolean
		get() = false
	override val children: Map<String, IStarboundFile>?
		get() = null

	override val exists: Boolean
		get() = false

	override fun open(): InputStream {
		throw FileNotFoundException("File ${fullPath ?: computeFullPath()} does not exist")
	}
}

fun getPathFolder(path: String): String {
	return path.substringBeforeLast('/')
}

fun getPathFilename(path: String): String {
	return path.substringAfterLast('/')
}

class PhysicalFile(val real: File, override val parent: PhysicalFile? = null) : IStarboundFile {
	override val exists: Boolean
		get() = real.exists()
	override val isDirectory: Boolean
		get() = real.isDirectory

	override val isFile: Boolean
		get() = real.isFile
	override val children: Map<String, PhysicalFile>?
		get() {
			return real.list()?.associate { it to PhysicalFile(File(it), this) }
		}
	override val name: String
		get() = real.name

	private val fullPatch by lazy { super.computeFullPath().sbIntern() }
	private val directory by lazy { super.computeDirectory().sbIntern() }

	override fun computeFullPath(): String {
		return fullPatch
	}

	override fun computeDirectory(): String {
		return directory
	}

	override fun open(): InputStream {
		return BufferedInputStream(real.inputStream())
	}

	override fun equals(other: Any?): Boolean {
		return other is IStarboundFile && computeFullPath() == other.computeFullPath()
	}

	override fun hashCode(): Int {
		return computeFullPath().hashCode()
	}

	override fun toString(): String {
		return "PhysicalFile[$real]"
	}
}
