import Foundation


struct UBJsonReader
{
	private var file: FileHandle

	init(file: FileHandle)
	{
		self.file = file
	}

	func read() throws -> UBJsonToken
	{
		guard try readCharacter() == "{"
		else { throw UBReaderError.badFormat("Stream does not start with an object") }
		return try readObject()
	}

	private func parse(type: Character) throws -> UBJsonToken
	{
		let oldFormat = true
		return switch type
		{
		case "{": try readObject()
		case "[": try readArray()
		case "Z": .null
		case "N": .noop
		case "T": .bool(true)
		case "F": .bool(false)
		case "i": oldFormat
			? .int16(try file.read(as: Int16.self).bigEndian)
			: .int8(try file.read(as: Int8.self))
		case "U": .uint8(try file.read(as: UInt8.self))
		case "I": oldFormat
			? .int32(try file.read(as: Int32.self).bigEndian)
			: .int16(try file.read(as: Int16.self).bigEndian)
		case "l": .int32(try file.read(as: Int32.self).bigEndian)
		case "L": .int64(try file.read(as: Int64.self).bigEndian)
		case "d": .float32(try readBeFloatPiss())
		case "D": .float64(try readBeFloat())
		case "H": throw UBReaderError.badFormat("High-precision numbers are unsupported")
		case "C": .char(try readCharacter())
		case "S": .string(try readString())
		default: throw UBReaderError.badToken("Unexpected token \"\(type)\"")
		}
	}

	private func readObject() throws -> UBJsonToken
	{
		var items = Dictionary<String, UBJsonToken>()
		while true
		{
			let type = try readCharacter()
			var name: String
			switch type
			{
			case "S", "i":
				name = try readString(type)
			case "}":
				return .object(items)
			default:
				throw UBReaderError.badToken("Unexpected token while reading object field key")
			}
			if items.keys.contains(name) { throw UBReaderError.badFormat("Object contains overlapping keys") }
			items[name] = try parse(type: try readCharacter())
		}
	}

	private func readArray() throws -> UBJsonToken
	{
		var array = [UBJsonToken]()
		while true
		{
			let type = try readCharacter()
			switch type
			{
			case "]":
				return .array(array)
			default:
				array.append(try parse(type: type))
			}
		}
	}

	private func readBeFloatPiss() throws -> Float
	{
		guard var bytes = try? file.read(upToCount: 4)
		else { throw UBReaderError.readError("Read failure while reading float data") }
		return Float(bitPattern: bytes.withUnsafeBytes { $0.load(as: UInt32.self) }.bigEndian)
	}

	private func readBeFloat<T: BinaryFloatingPoint, U: UnsignedInteger>() throws -> T where T.RawSignificand == U
	{
		guard var bytes = try? file.read(upToCount: MemoryLayout<U>.size)
		else { throw UBReaderError.readError("Read failure while reading float data") }
		bytes.reverse()
		return T(bytes.withUnsafeBytes { $0.load(as: U.self) })
	}

	private func readCharacter() throws -> Character
	{
		guard let raw = try? file.read(as: UInt8.self),
			let uni = UnicodeScalar(Int(raw))
		else { throw UBReaderError.readError("Read failure while reading character") }
		return Character(uni)
	}

	private func readString(_ optType: Character? = nil) throws -> String
	{
		let type = optType == nil ? try readCharacter() : optType!
		var length: Int
		switch type
		{
		case "S":
			guard try readCharacter() == "i"
			else { throw UBReaderError.badToken("Malformed string") }
			fallthrough
		case "i":
			length = Int(try file.read(as: Int8.self))
		case "s":
			length = Int(try file.read(as: UInt8.self))
		default: throw UBReaderError.badToken("Unexpected token while reading string")
		}
		if length < 0 { throw UBReaderError.badToken("Negative string length") }
		if length == 0 { return "" }
		guard let data = try file.read(upToCount: length)
		else { throw UBReaderError.readError("Error reading string") }
		return String(decoding: data, as: UTF8.self)
	}
}

enum UBReaderError: Error
{
	case badFormat(_ message: String)
	case badToken(_ message: String)
	case readError(_ message: String)
	case valueError(_ message: String)
}

enum UBJsonToken
{
	case object(_ fields: Dictionary<String, UBJsonToken>)
	case array(_ items: [UBJsonToken])
	case null
	case noop
	case bool(_ value: Bool)
	case int8(_ value: Int8)
	case uint8(_ value: UInt8)
	case int16(_ value: Int16)
	case int32(_ value: Int32)
	case int64(_ value: Int64)
	case highPrecision
	case float32(_ value: Float32)
	case float64(_ value: Float64)
	case char(_ value: Character)
	case string(_ value: String)
}

extension UBJsonToken
{
	var array: [UBJsonToken]
	{
		get throws
		{
			if case .array(let items) = self { return items }
			throw UBReaderError.valueError("Not an array")
		}
	}

	func getArray(key: String) throws -> [UBJsonToken]
	{
		if case .object(let fields) = self
		{
			guard let child = fields[key]
			else { throw UBReaderError.valueError("Field \"\(key)\" not found") }
			return try child.array
		}
		throw UBReaderError.valueError("Not an object")
	}

	var int16: Int16
	{
		get throws
		{
			if case .int16(let value) = self { return value }
			throw UBReaderError.valueError("Not an int16")
		}
	}

	func getInt16Array(key: String) throws -> [Int16]
	{
		try getArray(key: key).map(
		{ i in
			if case .int8(let value) = i { return Int16(value) }
			else if case .uint8(let value) = i { return Int16(value) }
			else if case .int16(let value) = i { return value }
			else if case .int32(let value) = i
			{
				if value < Int16.min || value > Int16.max { throw UBReaderError.valueError("Value out of range") }
				return Int16(truncatingIfNeeded: value)
			}
			else if case .int64(let value) = i
			{
				if value < Int16.min || value > Int16.max { throw UBReaderError.valueError("Value out of range") }
				return Int16(truncatingIfNeeded: value)
			}
			else { throw UBReaderError.valueError("Can't read array as int16s") }
		})
	}

	var float: Float
	{
		get throws
		{
			if case .float32(let value) = self { return value }
			throw UBReaderError.valueError("Not a float32")
		}
	}

	func getFloatArray(key: String) throws -> [Float]
	{
		try getArray(key: key).map({ try $0.float })
	}

	var string: String
	{
		get throws
		{
			if case .string(let value) = self { return value }
			throw UBReaderError.valueError("Not a string")
		}
	}

	func getString(key: String, default defStr: String? = nil) throws -> String
	{
		if case .object(let fields) = self
		{
			guard let child = fields[key] else
			{
				if defStr == nil { throw UBReaderError.valueError("Field \"\(key)\" not found") }
				return defStr!
			}
			return try child.string
		}
		throw UBReaderError.valueError("Not an object")
	}
}
