import Foundation
import WebSocketKit
import Vapor

let maximumPlayersPerTable = 4

typealias TableId = String
typealias TableName = String

final class TableManagement: DiskWriter {
    
    /// All tables indexed by their id
    private var tables = [TableId : ManageableTable]()
    
    /// The handle to the file where the tables are persisted
    var storageFile: FileHandle
    
    /// The url to the file where the tables are persisted
    let storageFileUrl: URL
    
    /**
     Load the tables from a file in the storage folder
     - Parameter storageFolder: The url to the folder where the table file is stored
     - Throws: Errors when the file could not be read
     */
    init(storageFolder: URL) throws {
        let url = storageFolder.appendingPathComponent("tables.txt")
        
        storageFileUrl = url
        storageFile = try Self.prepareFile(at: url)
        
        var entries = [TableId : (name: TableName, isPublic: Bool, players: [PlayerName])]()
        var redundantEntries = 0
        try readLinesFromDisk().forEach { line in
            // Each line has parts: ID | NAME | PLAYER, PLAYER, ...
            let parts = line.components(separatedBy: ":")
            guard parts.count == 4 else {
                print("Invalid line in table file")
                return
            }
            let id = parts[0]
            let name = parts[1]
            let isPublic = parts[2] == "public"
            let players = parts[3].components(separatedBy: ",")
            if name == "" {
                entries[id] = nil
                redundantEntries += 2 // One for creation, one for deletion
                return
            }
            if entries[id] != nil {
                redundantEntries += 1
            }
            entries[id] = (name, isPublic, players)
        }
        entries.forEach { id, tableData in
            tables[id] = WaitingTable(id: id, name: tableData.name, isPublic: tableData.isPublic, players: tableData.players)
        }
        let totalEntries = entries.count + redundantEntries
        let percentage = entries.count * 100 / totalEntries
        print("Loaded \(tables.count) tables from \(totalEntries) entries (\(percentage) % useful)")
        if percentage < 80  && redundantEntries > 10 {
            try optimizeTableFile()
        }
    }

    private func optimizeTableFile() throws {
        print("Optimizing tables file...")
        let lines = tables.values.map(entry).joined(separator: "\n") + "\n"
        try replaceFile(data: lines)
        print("Done.")
    }

    private func entry(for table: ManageableTable) -> String {
        let visible = table.isPublic ? "public" : "private"
        let players = table.playerNames
            .joined(separator: ",")
        return [table.id, table.name, visible, players].joined(separator: ":")
    }
    
    /**
     Writes the table info to disk.
     
     Currently only the id, name, visibility and players are stored, all other information is lost.
     - Parameter table: The changed table information to persist
     - Returns: `true`, if the entry was written, `false` on error
     */
    @discardableResult
    private func writeTableToDisk(table: ManageableTable) -> Bool {
        let entry = entry(for: table)
        return writeToDisk(line: entry)
    }
    
    /**
     Writes the deletion of a table to disk.
     
     The deletion is written as a separate entry and appended to the file, in order to reduce disk I/O.
     - Parameter tableId: The id of the deleted table
     - Returns: `true`, if the entry was written, `false` on error
     */
    @discardableResult
    private func writeTableDeletionEntry(tableId: TableId) -> Bool {
        let entry = [tableId, "", "", ""]
            .joined(separator: ":")
        return writeToDisk(line: entry)
    }
    
    /**
     Create a new table with optional players.
     - Parameter name: The name of the table
     - Parameter players: The player creating the table
     - Parameter isPublic: Indicates that this is a game joinable by everyone
     - Returns: The table id
     */
    func createTable(named name: TableName, player: PlayerName, isPublic: Bool) -> TableInfo {
        let table = WaitingTable(newTable: name, isPublic: isPublic, creator: player)
        tables[table.id] = table
        writeTableToDisk(table: table)
        return table.tableInfo(forPlayer: player)
    }
    
    /// A list of all public tables
    var publicTableList: [PublicTableInfo] {
        tables.values.filter { $0.isPublic }.map { $0.publicInfo }
    }

    /**
     Get the table info for a player
     - Parameter player: The name of the player
     - Returns: The table info, if the player has joined a table
     */
    func tableInfo(player: PlayerName) -> TableInfo? {
        currentTable(for: player)?.tableInfo(forPlayer: player)
    }

    private func currentTable(for player: PlayerName) -> ManageableTable? {
        tables.values.first(where: { $0.playerNames.contains(player) })
    }
    
    /**
     Join a table.
     - Parameter tableId: The table to join
     - Parameter player: The name of the player who wants to join.
     - Returns: The result of the join operation
     */
    func join(tableId: TableId, player: PlayerName) -> Result<TableInfo, JoinTableResult> {
        if let existing = currentTable(for: player) {
            guard existing.id == tableId else {
                return .failure(.alreadyJoinedOtherTable)
            }
            return .success(existing.tableInfo(forPlayer: player))
        }
        guard let table = tables[tableId] else {
            return .failure(.tableNotFound)
        }
        guard let joinableTable = table as? WaitingTable else {
            return .failure(.tableIsFull)
        }
        guard joinableTable.add(player: player) else {
            return .failure(.tableIsFull)
        }
        writeTableToDisk(table: table)
        joinableTable.sendUpdateToAllPlayers()
        return .success(joinableTable.tableInfo(forPlayer: player))
    }
    
    /**
     A player leaves the table it previously joined
     - Parameter player: The name of the player
     */
    func leaveTable(player: PlayerName) {
        guard let oldTable = currentTable(for: player) else {
            return
        }
        /// `player.canStartGame` is automatically set to false, because table is not full
        let table = WaitingTable(oldTable: oldTable, removing: player)
        tables[table.id] = table
        table.sendUpdateToAllPlayers()
        writeTableToDisk(table: table)
    }
    
    func connect(player: PlayerName, using socket: WebSocket) -> Bool {
        guard let table = currentTable(for: player) else {
            return false
        }
        return table.connect(player: player, using: socket)
    }
    
    func disconnect(player: PlayerName) {
        guard let table = currentTable(for: player) else {
            return
        }
        table.disconnect(player: player)
    }
    
    func performAction(player: PlayerName, action: PlayerAction) -> PlayerActionResult {
        guard let table = currentTable(for: player) else {
            print("Player \(player) wants to \(action.id), but no table joined")
            return .noTableJoined
        }
        let (result, newTable) = table.perform(action: action, forPlayer: player)
        guard result == .success else {
            return result
        }
        guard let newTable = newTable else {
            table.sendUpdateToAllPlayers()
            return .success
        }
        tables[newTable.id] = newTable
        newTable.sendUpdateToAllPlayers()
        if newTable is FinishedTable || newTable is DealingTable {
            writeTableToDisk(table: newTable)
        }
        return .success
    }

    func select(game: GameType, player: PlayerName) -> PlayerActionResult {
        guard let aTable = currentTable(for: player) else {
            print("Player \(player) wants to play \(game.rawValue), but no table joined")
            return .noTableJoined
        }
        guard let table = aTable as? BiddingTable else {
            return .tableStateInvalid
        }
        let (result, newTable) = table.select(game: game, player: player)
        guard result == .success else {
            return result
        }
        guard let newTable = newTable else {
            print("Game selected by \(player), but no playing table \(table.name) created")
            table.sendUpdateToAllPlayers()
            return result
        }
        tables[newTable.id] = newTable
        newTable.sendUpdateToAllPlayers()
        return .success
    }

    func play(card: Card, player: PlayerName) -> PlayerActionResult {
        guard let table = currentTable(for: player) else {
            return .noTableJoined
        }
        let (result, newTable) = table.play(card: card, player: player)
        guard result == .success else {
            return result
        }
        guard let newTable = newTable else {
            table.sendUpdateToAllPlayers()
            return .success
        }
        tables[newTable.id] = newTable
        newTable.sendUpdateToAllPlayers()
        return .success
    }
}
