import Foundation

/**
 The message content without authentication.
 */
struct Message: Equatable, Hashable {
    
    /// The type of message being sent.
    let messageType: MessageType
    
    /**
     * The random nonce created by the remote
     *
     * This nonce is a random number created by the remote, different for each unlock request.
     * It is set for all message types.
     */
    let clientChallenge: UInt32
    
    /**
     * A random number to sign by the remote
     *
     * This nonce is set by the server after receiving an initial message.
     * It is set for the message types `challenge`, `request`, and `response`.
     */
    let serverChallenge: UInt32
    
    /**
     * The response status for the previous message.
     *
     * It is set only for messages from the server, e.g. the `challenge` and `response` message types.
     * Must be set to `MessageAccepted` for other messages.
     */
    let result: MessageResult
    
    init(messageType: MessageType, clientChallenge: UInt32, serverChallenge: UInt32, result: MessageResult) {
        self.messageType = messageType
        self.clientChallenge = clientChallenge
        self.serverChallenge = serverChallenge
        self.result = result
    }
    
    /**
     Decode message content from data.
     
     The data consists of two `UInt32` encoded in little endian format
     - Warning: The sequence must contain at least 8 bytes, or the function will crash.
     - Parameter data: The sequence containing the bytes.
     */
    init(decodeFrom data: Data) throws {
        guard data.count == Message.size else {
            print("Invalid message size \(data.count)")
            throw MessageResult.invalidMessageSizeFromDevice
        }
        guard let messageType = MessageType(rawValue: data.first!) else {
            print("Invalid message type \(data.first!)")
            throw MessageResult.invalidMessageTypeFromDevice
        }
        self.messageType = messageType
        self.clientChallenge = UInt32(data: data.dropFirst().prefix(UInt32.byteSize))
        self.serverChallenge = UInt32(data: data.dropFirst(UInt32.byteSize+1).prefix(UInt32.byteSize))
        guard let result = MessageResult(rawValue: data.last!) else {
            print("Invalid message result \(data.last!)")
            throw MessageResult.unknownMessageResultFromDevice
        }
        self.result = result
    }
    
    /// The message content encoded to data
    var encoded: Data {
        messageType.encoded + clientChallenge.encoded + serverChallenge.encoded + result.encoded
    }
}

extension Message: Codable {

    enum CodingKeys: Int, CodingKey {
        case messageType = 1
        case clientChallenge = 2
        case serverChallenge = 3
        case result = 4
    }
}

extension Message {
    
    init(error: MessageResult, type: MessageType) {
        self.init(messageType: type, clientChallenge: 0, serverChallenge: 0, result: error)
    }
    
    static func initial() -> Message {
        .init(
            messageType: .initial,
            clientChallenge: .random(),
            serverChallenge: 0,
            result: .messageAccepted)
    }
    
    func with(result: MessageResult) -> Message {
        .init(
            messageType: messageType.responseType,
            clientChallenge: clientChallenge,
            serverChallenge: serverChallenge,
            result: result)
    }
    
    /**
     Create the message to respond to this challenge
     */
    func requestMessage() -> Message {
        .init(
            messageType: .request,
            clientChallenge: clientChallenge,
            serverChallenge: serverChallenge,
            result: .messageAccepted)
    }
}

extension Message: CustomStringConvertible {
    
    var description: String {
        "\(messageType)(\(clientChallenge)->\(serverChallenge), \(result))"
    }
}
