/**
 * Copyright 2024 Beijing Baolande Software Corporation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Runtime Library Exception to the Apache 2.0 License:
 *
 * As an exception, if you use this Software to compile your source code and
 * portions of this Software are embedded into the binary product as a result,
 * you may redistribute such product without providing attribution as would
 * otherwise be required by Sections 4(a), 4(b) and 4(d) of the License.
 */

package redis_sdk.client.api

public open class RedisCommand {
    private let commandType: CommandType

    private let commandTypeArg: CommandArg

    private let commandArgs: CommandArgs

    private var supportResponse: Bool

    private var response: ?RedisMessage = None<RedisMessage>

    private var exception: ?Exception = None<Exception>

    private let mutex = Mutex()

    private let monitor = synchronized (mutex) {
        mutex.condition()
    }

    public init(commandType: CommandType) {
        this.commandType = commandType
        this.commandTypeArg = BytesArg(commandType.getBytes())
        this.commandArgs = CommandArgs()
        this.supportResponse = true
    }

    public init(commandType: CommandType, supportResponse!: Bool) {
        this.commandType = commandType
        this.commandTypeArg = BytesArg(commandType.getBytes())
        this.commandArgs = CommandArgs()
        this.supportResponse = supportResponse
    }

    public init(commandType: CommandType, commandArgs: CommandArgs) {
        this.commandType = commandType
        this.commandTypeArg = BytesArg(commandType.getBytes())
        this.commandArgs = commandArgs
        this.supportResponse = true
    }

    public init(commandType: CommandType, commandArgs: CommandArgs, supportResponse!: Bool) {
        this.commandType = commandType
        this.commandTypeArg = BytesArg(commandType.getBytes())
        this.commandArgs = commandArgs
        this.supportResponse = supportResponse
    }

    public init(commandType: CommandType, commandArgs: CommandArgs, compositeArg: CompositeArg) {
        this.commandType = commandType
        this.commandTypeArg = BytesArg(commandType.getBytes())
        this.commandArgs = commandArgs
        compositeArg.buildArgs(this.commandArgs)
        this.supportResponse = true
    }

    public init(commandType: CommandType, commandArgs: CommandArgs, compositeArg: CompositeArg, supportResponse!: Bool) {
        this.commandType = commandType
        this.commandTypeArg = BytesArg(commandType.getBytes())
        this.commandArgs = commandArgs
        compositeArg.buildArgs(this.commandArgs)
        this.supportResponse = supportResponse
    }

    public func getCommandType(): CommandType {
        return commandType
    }

    public func getCommandArgs(): CommandArgs {
        return commandArgs
    }

    public func getEncodeSize() {
        let count = commandArgs.getArgsSize() + 1
        let countBytes = count.toString().toArray()
        return 1 + countBytes.size + RedisConstants.CRLF.size + // 参数个数
            commandTypeArg.getEncodeSize() + // 命令名称
            commandArgs.getEncodeSize() // 命令参数
    }

    public open func encode(buffer: ByteBuffer): Unit {
        buffer.put(RedisConstants.ASTERISK_BYTE)
        let count = commandArgs.getArgsSize() + 1
        writeInteger(count, buffer)
        buffer.put(RedisConstants.CRLF)

        commandTypeArg.encode(buffer)
        commandArgs.encode(buffer)
    }

    public func setResponse(response: RedisMessage) {
        this.response = response
        synchronized(mutex) {
            monitor.notifyAll()
        }
    }

    public func setException(exception: Exception) {
        this.exception = exception
        synchronized(mutex) {
            monitor.notifyAll()
        }
    }

    public func getResponse(): ?RedisMessage {
        if (!supportResponse) {
            throw RedisException("The command ${commandType.name()} does not have a response")
        }

        if (let Some(exception) <- exception) {
            if (let Some(redisException) <- (exception as RedisException)) {
                throw redisException
            } else {
                throw RedisException(exception.message, exception)
            }
        }

        return this.response
    }

    public func waitForResponse(duration: Duration): RedisMessage {
        if (!supportResponse) {
            throw RedisException("The command ${commandType.name()} does not have a response")
        }

        let endTime = DateTime.now().toUnixTimeStamp().toMilliseconds() + duration.toMilliseconds()
        var remainingTime = duration.toMilliseconds()
        synchronized(mutex) {
            while (true) {
                if (let Some(message) <- response) {
                    return message
                }
                if (let Some(exception) <- exception) {
                    if (let Some(redisException) <- (exception as RedisException)) {
                        throw redisException
                    } else {
                        throw RedisException(exception.message, exception)
                    }
                }
                if (remainingTime <= 0) {
                    break
                }

                monitor.wait(timeout: Duration.millisecond * remainingTime)
                remainingTime = endTime - DateTime.now().toUnixTimeStamp().toMilliseconds()
            }
        }

        throw RedisTimeoutException("Unable to obtain response in ${duration}")
    }

    private static func writeInteger(value: Int64, buffer: ByteBuffer) {
        buffer.put(value.toString().toArray())
    }

    public func clear() {
        synchronized(mutex) {
            response = None
            exception = None
        }
    }
}

public open class ParameterizedRedisCommand<T> <: RedisCommand {
    private let builder: ResponseBuilder<T>

    public init(commandType: CommandType, builder: ResponseBuilder<T>) {
        super(commandType)
        this.builder = builder
    }

    public init(commandType: CommandType, commandArgs: CommandArgs, builder: ResponseBuilder<T>) {
        super(commandType, commandArgs)
        this.builder = builder
    }

    public init(
        commandType: CommandType,
        commandArgs: CommandArgs,
        compositeArg: CompositeArg,
        builder: ResponseBuilder<T>
    ) {
        super(commandType, commandArgs, compositeArg)
        this.builder = builder
    }

    public func getBuilder() {
        return builder
    }

    public func buildResponse(waitResponseTimeout: Duration): T {
        let redisMessage = this.waitForResponse(waitResponseTimeout)
        return builder.build(redisMessage)
    }
}