// SPDX-FileCopyrightText: 2026 John Burwell and contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

import Foundation

enum BedrockLevelMetadataDecoder {
    nonisolated static func decode(fromLevelDatAt url: URL) -> WorldMetadata? {
        guard let data = try? Data(contentsOf: url) else {
            return nil
        }

        return decode(fromLevelDatData: data)
    }

    nonisolated static func decode(fromLevelDatData data: Data) -> WorldMetadata? {
        guard data.count > 8 else {
            return nil
        }

        let payloadLength = Int(readInt32LE(from: data, offset: 4))
        let payloadStart = 8
        let payloadEnd = min(data.count, payloadStart + max(0, payloadLength))
        guard payloadEnd > payloadStart else {
            return nil
        }

        let payload = data.subdata(in: payloadStart..<payloadEnd)
        var decoder = BedrockNBTDecoder(data: payload)
        guard let root = try? decoder.decodeRootCompound() else {
            return nil
        }

        return worldMetadata(from: root)
    }

    nonisolated private static func worldMetadata(from root: [String: NBTValue]) -> WorldMetadata {
        let gameModeCode = root["GameType"]?.intValue
        let difficultyCode = root["Difficulty"]?.intValue
        let spawn = formattedSpawn(from: root)
        let lastPlayedRaw = root["LastPlayed"]?.longValue
        let lastPlayedDate = lastPlayedRaw.map(dateFromBedrockTimestamp(_:))

        return WorldMetadata(
            gameMode: gameModeCode.map(gameModeName(for:)),
            difficulty: difficultyCode.map(difficultyName(for:)),
            seed: root["RandomSeed"]?.stringifiedScalar,
            lastPlayedDate: lastPlayedDate,
            lastOpenedWithVersion: versionString(from: root["lastOpenedWithVersion"] ?? root["MinimumCompatibleClientVersion"]),
            inventoryVersion: versionString(from: root["InventoryVersion"]),
            cheatsEnabled: root["cheatsEnabled"]?.boolValue,
            commandsEnabled: root["commandsEnabled"]?.boolValue,
            educationFeaturesEnabled: root["educationFeaturesEnabled"]?.boolValue ?? root["eduLevel"]?.boolValue,
            coordinatesShown: root["showcoordinates"]?.boolValue,
            keepInventory: root["keepinventory"]?.boolValue,
            mobGriefingEnabled: root["mobgriefing"]?.boolValue,
            daylightCycleEnabled: root["dodaylightcycle"]?.boolValue,
            weatherCycleEnabled: root["doweathercycle"]?.boolValue,
            spawn: spawn,
            storageVersion: root["StorageVersion"]?.stringifiedScalar,
            networkVersion: root["NetworkVersion"]?.stringifiedScalar
        )
    }

    nonisolated private static func gameModeName(for value: Int) -> String {
        switch value {
        case 0:
            return "Survival"
        case 1:
            return "Creative"
        case 2:
            return "Adventure"
        case 3:
            return "Spectator"
        default:
            return String(value)
        }
    }

    nonisolated private static func difficultyName(for value: Int) -> String {
        switch value {
        case 0:
            return "Peaceful"
        case 1:
            return "Easy"
        case 2:
            return "Normal"
        case 3:
            return "Hard"
        default:
            return String(value)
        }
    }

    nonisolated private static func formattedSpawn(from root: [String: NBTValue]) -> String? {
        guard
            let x = root["SpawnX"]?.intValue,
            let y = root["SpawnY"]?.intValue,
            let z = root["SpawnZ"]?.intValue
        else {
            return nil
        }

        return "\(x), \(y), \(z)"
    }

    nonisolated private static func versionString(from value: NBTValue?) -> String? {
        guard let value else {
            return nil
        }

        switch value {
        case .string(let string):
            let trimmed = string.trimmingCharacters(in: .whitespacesAndNewlines)
            return trimmed.isEmpty ? nil : trimmed
        case .list(_, let values):
            let components = values.compactMap(\.intValue).map(String.init)
            return components.isEmpty ? nil : components.joined(separator: ".")
        case .intArray(let values):
            return values.isEmpty ? nil : values.map(String.init).joined(separator: ".")
        case .byteArray(let values):
            return values.isEmpty ? nil : values.map(String.init).joined(separator: ".")
        default:
            return value.stringifiedScalar
        }
    }

    nonisolated private static func dateFromBedrockTimestamp(_ value: Int64) -> Date {
        if value > 1_000_000_000_000 {
            return Date(timeIntervalSince1970: TimeInterval(value) / 1000)
        }

        return Date(timeIntervalSince1970: TimeInterval(value))
    }

    nonisolated private static func readInt32LE(from data: Data, offset: Int) -> Int32 {
        let range = offset..<(offset + 4)
        return data.subdata(in: range).withUnsafeBytes { buffer in
            Int32(littleEndian: buffer.loadUnaligned(as: Int32.self))
        }
    }
}

private enum NBTValue: Hashable, Sendable {
    case byte(Int8)
    case short(Int16)
    case int(Int32)
    case long(Int64)
    case float(Float)
    case double(Double)
    case byteArray([Int8])
    case string(String)
    case list(UInt8, [NBTValue])
    case compound([String: NBTValue])
    case intArray([Int32])
    case longArray([Int64])

    nonisolated var intValue: Int? {
        switch self {
        case .byte(let value):
            return Int(value)
        case .short(let value):
            return Int(value)
        case .int(let value):
            return Int(value)
        case .long(let value):
            return Int(value)
        default:
            return nil
        }
    }

    nonisolated var longValue: Int64? {
        switch self {
        case .byte(let value):
            return Int64(value)
        case .short(let value):
            return Int64(value)
        case .int(let value):
            return Int64(value)
        case .long(let value):
            return value
        default:
            return nil
        }
    }

    nonisolated var boolValue: Bool? {
        intValue.map { $0 != 0 }
    }

    nonisolated var stringifiedScalar: String? {
        switch self {
        case .string(let value):
            return value
        case .byte(let value):
            return String(value)
        case .short(let value):
            return String(value)
        case .int(let value):
            return String(value)
        case .long(let value):
            return String(value)
        default:
            return nil
        }
    }
}

private struct BedrockNBTDecoder {
    private let data: Data
    private var offset = 0

    nonisolated init(data: Data) {
        self.data = data
    }

    nonisolated mutating func decodeRootCompound() throws -> [String: NBTValue] {
        let rootType = try readUInt8()
        guard rootType == 10 else {
            throw BedrockNBTError.invalidRootTag
        }

        _ = try readString()
        return try readCompoundPayload()
    }

    nonisolated private mutating func readCompoundPayload() throws -> [String: NBTValue] {
        var result: [String: NBTValue] = [:]

        while true {
            let tagType = try readUInt8()
            if tagType == 0 {
                return result
            }

            let name = try readString()
            result[name] = try readTagPayload(type: tagType)
        }
    }

    nonisolated private mutating func readTagPayload(type: UInt8) throws -> NBTValue {
        switch type {
        case 1:
            return .byte(try readInt8())
        case 2:
            return .short(try readInt16())
        case 3:
            return .int(try readInt32())
        case 4:
            return .long(try readInt64())
        case 5:
            return .float(try readFloat())
        case 6:
            return .double(try readDouble())
        case 7:
            let count = try readCount()
            return .byteArray(try (0..<count).map { _ in try readInt8() })
        case 8:
            return .string(try readString())
        case 9:
            let listType = try readUInt8()
            let count = try readCount()
            let values = try (0..<count).map { _ in try readTagPayload(type: listType) }
            return .list(listType, values)
        case 10:
            return .compound(try readCompoundPayload())
        case 11:
            let count = try readCount()
            return .intArray(try (0..<count).map { _ in try readInt32() })
        case 12:
            let count = try readCount()
            return .longArray(try (0..<count).map { _ in try readInt64() })
        default:
            throw BedrockNBTError.unsupportedTag(type)
        }
    }

    nonisolated private mutating func readCount() throws -> Int {
        let count = try readInt32()
        guard count >= 0 else {
            throw BedrockNBTError.invalidLength
        }
        return Int(count)
    }

    nonisolated private mutating func readUInt8() throws -> UInt8 {
        guard offset + 1 <= data.count else {
            throw BedrockNBTError.outOfBounds
        }

        defer { offset += 1 }
        return data[offset]
    }

    nonisolated private mutating func readInt8() throws -> Int8 {
        Int8(bitPattern: try readUInt8())
    }

    nonisolated private mutating func readInt16() throws -> Int16 {
        try readScalar(Int16.self)
    }

    nonisolated private mutating func readInt32() throws -> Int32 {
        try readScalar(Int32.self)
    }

    nonisolated private mutating func readInt64() throws -> Int64 {
        try readScalar(Int64.self)
    }

    nonisolated private mutating func readFloat() throws -> Float {
        let raw = try readScalar(UInt32.self)
        return Float(bitPattern: raw)
    }

    nonisolated private mutating func readDouble() throws -> Double {
        let raw = try readScalar(UInt64.self)
        return Double(bitPattern: raw)
    }

    nonisolated private mutating func readString() throws -> String {
        let length = Int(try readUInt16())
        guard offset + length <= data.count else {
            throw BedrockNBTError.outOfBounds
        }

        let stringData = data.subdata(in: offset..<(offset + length))
        offset += length
        return String(decoding: stringData, as: UTF8.self)
    }

    nonisolated private mutating func readUInt16() throws -> UInt16 {
        try readScalar(UInt16.self)
    }

    nonisolated private mutating func readScalar<T: FixedWidthInteger>(_ type: T.Type) throws -> T {
        let byteCount = MemoryLayout<T>.size
        guard offset + byteCount <= data.count else {
            throw BedrockNBTError.outOfBounds
        }

        let value = data.subdata(in: offset..<(offset + byteCount)).withUnsafeBytes { buffer in
            T(littleEndian: buffer.loadUnaligned(as: T.self))
        }
        offset += byteCount
        return value
    }
}

private enum BedrockNBTError: Error {
    case invalidRootTag
    case invalidLength
    case outOfBounds
    case unsupportedTag(UInt8)
}
