import Foundation
import SwiftUI

enum ConnectionError {
    case serverNotReached
    case deviceDisconnected
}

extension ConnectionError: CustomStringConvertible {

    var description: String {
        switch self {
        case .serverNotReached:
            return "Server unavailable"
        case .deviceDisconnected:
            return "Device disconnected"
        }
    }
}

enum RejectionCause {
    case invalidCounter
    case invalidTime
    case invalidAuthentication
    case timeout
    case missingKey
}

extension RejectionCause: CustomStringConvertible {

    var description: String {
        switch self {
        case .invalidCounter:
            return "Invalid counter"
        case .invalidTime:
            return "Invalid time"
        case .invalidAuthentication:
            return "Invalid authentication"
        case .timeout:
            return "Device not responding"
        case .missingKey:
            return "No key to verify message"
        }
    }
}

enum ClientState {

    /// There is no key stored locally on the client. A new key must be generated before use.
    case noKeyAvailable

    /// The device status is being requested
    case requestingStatus

    /// The remote device is not connected (no socket opened)
    case deviceNotAvailable(ConnectionError)

    /// The device is connected and ready to receive a message
    case ready
    
    /// The message is being transmitted and a response is awaited
    case waitingForResponse
    
    /// The transmitted message was rejected (multiple possible reasons)
    case messageRejected(RejectionCause)

    case responseRejected(RejectionCause)

    /// The device responded that the opening action was started
    case openSesame

    case internalError(String)
    
    var canSendKey: Bool {
        switch self {
        case .ready, .openSesame, .messageRejected:
            return true
        default:
            return false
        }
    }
    
    init(keyResult: MessageResult) {
        switch keyResult {
        case .messageAuthenticationFailed:
            self = .messageRejected(.invalidAuthentication)
        case .messageTimeMismatch:
            self = .messageRejected(.invalidTime)
        case .messageCounterInvalid:
            self = .messageRejected(.invalidCounter)
        case .deviceTimedOut:
            self = .messageRejected(.timeout)
        case .messageAccepted:
            self = .openSesame
        case .noBodyData, .invalidMessageData, .textReceived, .unexpectedSocketEvent:
            self = .internalError(keyResult.description)
        case .deviceNotConnected:
            self = .deviceNotAvailable(.deviceDisconnected)
        case .operationInProgress:
            self = .waitingForResponse
        case .deviceConnected:
            self = .ready
        }
    }

    var actionText: String {
        "Unlock"
    }

    var requiresDescription: Bool {
        switch self {
        case .deviceNotAvailable, .messageRejected, .internalError, .responseRejected:
            return true
        default:
            return false
        }
    }
    
    var color: Color {
        switch self {
        case .noKeyAvailable:
            return Color(red: 50/255, green: 50/255, blue: 50/255)
        case .deviceNotAvailable:
            return Color(red: 150/255, green: 90/255, blue: 90/255)
        case .messageRejected, .responseRejected:
            return Color(red: 160/255, green: 30/255, blue: 30/255)
        case .internalError:
            return Color(red: 100/255, green: 0/255, blue: 0/255)
        case .ready:
            return Color(red: 115/255, green: 140/255, blue: 90/255)
        case .requestingStatus, .waitingForResponse:
            return Color(red: 160/255, green: 170/255, blue: 110/255)
        case .openSesame:
            return Color(red: 65/255, green: 110/255, blue: 60/255)
        }
    }
    
    var allowsAction: Bool {
        switch self {
        case .requestingStatus, .deviceNotAvailable, .waitingForResponse, .noKeyAvailable:
            return false
        default:
            return true
        }
    }
}

extension ClientState: Equatable {
    
}

extension ClientState: CustomStringConvertible {

    var description: String {
        switch self {
        case .noKeyAvailable:
            return "No key set."
        case .requestingStatus:
            return "Checking device status"
        case .deviceNotAvailable(let status):
            return status.description
        case .ready:
            return "Ready"
        case .waitingForResponse:
            return "Unlocking..."
        case .messageRejected(let cause):
            return cause.description
        case .openSesame:
            return "Unlocked"
        case .internalError(let e):
            return "Error: \(e)"
        case .responseRejected(let cause):
            switch cause {
            case .invalidAuthentication:
                return "Device message not authenticated"
            default:
                return cause.description
            }
        }
    }
}

// MARK: Coding

extension ClientState {

    var encoded: Data {
        Data([code])
    }

    private var code: UInt8 {
        switch self {
        case .noKeyAvailable:
            return 1
        case .requestingStatus:
            return 2
        case .deviceNotAvailable(let connectionError):
            switch connectionError {
            case .serverNotReached:
                return 3
            case .deviceDisconnected:
                return 4
            }
        case .ready:
            return 5
        case .waitingForResponse:
            return 6
        case .messageRejected(let rejectionCause):
            switch rejectionCause {
            case .invalidCounter:
                return 7
            case .invalidTime:
                return 8
            case .invalidAuthentication:
                return 9
            case .timeout:
                return 10
            case .missingKey:
                return 11
            }
        case .responseRejected(let rejectionCause):
            switch rejectionCause {
            case .invalidCounter:
                return 12
            case .invalidTime:
                return 13
            case .invalidAuthentication:
                return 14
            case .timeout:
                return 15
            case .missingKey:
                return 16
            }
        case .openSesame:
            return 17
        case .internalError(_):
            return 18
        }
    }

    init(code: UInt8) {
        switch code {
        case 1:
            self = .noKeyAvailable
        case 2:
            self = .requestingStatus
        case 3:
            self = .deviceNotAvailable(.serverNotReached)
        case 4:
            self = .deviceNotAvailable(.deviceDisconnected)
        case 5:
            self = .ready
        case 6:
            self = .waitingForResponse
        case 7:
            self = .messageRejected(.invalidCounter)
        case 8:
            self = .messageRejected(.invalidTime)
        case 9:
            self = .messageRejected(.invalidAuthentication)
        case 10:
            self = .messageRejected(.timeout)
        case 11:
            self = .messageRejected(.missingKey)
        case 12:
            self = .responseRejected(.invalidCounter)
        case 13:
            self = .responseRejected(.invalidTime)
        case 14:
            self = .responseRejected(.invalidAuthentication)
        case 15:
            self = .responseRejected(.timeout)
        case 16:
            self = .responseRejected(.missingKey)
        case 17:
            self = .openSesame
        case 18:
            self = .internalError("")
        default:
            self = .internalError("Unknown code \(code)")
        }
    }
}
