@file:Suppress("DEPRECATION")

package net.corda.client.jackson.internal

import com.fasterxml.jackson.annotation.JsonAutoDetect.Value
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonCreator.Mode.DISABLED
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonInclude.Include
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.annotation.JsonValue
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParseException
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonToken
import com.fasterxml.jackson.databind.BeanDescription
import com.fasterxml.jackson.databind.BeanProperty
import com.fasterxml.jackson.databind.DeserializationConfig
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.Module
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationConfig
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.fasterxml.jackson.databind.cfg.MapperConfig
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier
import com.fasterxml.jackson.databind.deser.ContextualDeserializer
import com.fasterxml.jackson.databind.deser.std.DelegatingDeserializer
import com.fasterxml.jackson.databind.deser.std.FromStringDeserializer
import com.fasterxml.jackson.databind.introspect.AccessorNamingStrategy
import com.fasterxml.jackson.databind.introspect.AnnotatedClass
import com.fasterxml.jackson.databind.introspect.BasicClassIntrospector
import com.fasterxml.jackson.databind.introspect.POJOPropertiesCollector
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.node.IntNode
import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier
import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer
import com.fasterxml.jackson.databind.ser.std.UUIDSerializer
import com.google.common.primitives.Booleans
import net.corda.client.jackson.JacksonSupport
import net.corda.core.contracts.Amount
import net.corda.core.contracts.AttachmentConstraint
import net.corda.core.contracts.Command
import net.corda.core.contracts.CommandData
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.PrivacySalt
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TimeWindow
import net.corda.core.contracts.TransactionState
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.DigestService
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.PartialMerkleTree.PartialTree
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SecureHash.Companion.SHA2_256
import net.corda.core.crypto.SignatureMetadata
import net.corda.core.crypto.SignatureScheme
import net.corda.core.crypto.TransactionSignature
import net.corda.core.flows.StateMachineRunId
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.internal.DigitalSignatureWithCert
import net.corda.core.internal.createComponentGroups
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.SerializeAsToken
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.transactions.ContractUpgradeFilteredTransaction
import net.corda.core.transactions.ContractUpgradeWireTransaction
import net.corda.core.transactions.FilteredTransaction
import net.corda.core.transactions.NotaryChangeWireTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.ByteSequence
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.parseAsHex
import net.corda.core.utilities.toHexString
import net.corda.serialization.internal.AllWhitelist
import net.corda.serialization.internal.amqp.SerializerFactoryBuilder
import net.corda.serialization.internal.amqp.hasCordaSerializable
import net.corda.serialization.internal.amqp.registerCustomSerializers
import java.math.BigDecimal
import java.security.PublicKey
import java.security.cert.CertPath
import java.time.Instant
import java.util.Currency
import java.util.UUID

class CordaModule : SimpleModule("corda-core") {
    override fun setupModule(context: SetupContext) {
        super.setupModule(context)

        // For classes which are annotated with CordaSerializable we want to use the same set of properties as the Corda serilasation scheme.
        // To do that we use CordaSerializableClassIntrospector to first turn on field visibility for these classes (the Jackson default is
        // private fields are not included) and then we use CordaSerializableBeanSerializerModifier to remove any extra properties that Jackson
        // might pick up.
        context.setClassIntrospector(CordaSerializableClassIntrospector(context))
        context.addBeanSerializerModifier(CordaSerializableBeanSerializerModifier())

        context.addBeanDeserializerModifier(AmountBeanDeserializerModifier())

        context.setMixInAnnotations(PartyAndCertificate::class.java, PartyAndCertificateMixin::class.java)
        context.setMixInAnnotations(NetworkHostAndPort::class.java, NetworkHostAndPortMixin::class.java)
        context.setMixInAnnotations(CordaX500Name::class.java, CordaX500NameMixin::class.java)
        context.setMixInAnnotations(Amount::class.java, AmountMixin::class.java)
        context.setMixInAnnotations(AbstractParty::class.java, AbstractPartyMixin::class.java)
        context.setMixInAnnotations(AnonymousParty::class.java, AnonymousPartyMixin::class.java)
        context.setMixInAnnotations(Party::class.java, PartyMixin::class.java)
        context.setMixInAnnotations(PublicKey::class.java, PublicKeyMixin::class.java)
        context.setMixInAnnotations(ByteSequence::class.java, ByteSequenceMixin::class.java)
        context.setMixInAnnotations(SecureHash.SHA256::class.java, SecureHashMixin::class.java)
        context.setMixInAnnotations(SecureHash.HASH::class.java, SecureHashMixin::class.java)
        context.setMixInAnnotations(SecureHash::class.java, SecureHashMixin::class.java)
        context.setMixInAnnotations(SerializedBytes::class.java, SerializedBytesMixin::class.java)
        context.setMixInAnnotations(DigitalSignature.WithKey::class.java, ByteSequenceWithPropertiesMixin::class.java)
        context.setMixInAnnotations(DigitalSignatureWithCert::class.java, ByteSequenceWithPropertiesMixin::class.java)
        context.setMixInAnnotations(TransactionSignature::class.java, ByteSequenceWithPropertiesMixin::class.java)
        context.setMixInAnnotations(SignedTransaction::class.java, SignedTransactionMixin::class.java)
        context.setMixInAnnotations(WireTransaction::class.java, WireTransactionMixin::class.java)
        context.setMixInAnnotations(TransactionState::class.java, TransactionStateMixin::class.java)
        context.setMixInAnnotations(Command::class.java, CommandMixin::class.java)
        context.setMixInAnnotations(TimeWindow::class.java, TimeWindowMixin::class.java)
        context.setMixInAnnotations(PrivacySalt::class.java, PrivacySaltMixin::class.java)
        context.setMixInAnnotations(SignatureScheme::class.java, SignatureSchemeMixin::class.java)
        context.setMixInAnnotations(SignatureMetadata::class.java, SignatureMetadataMixin::class.java)
        context.setMixInAnnotations(PartialTree::class.java, PartialTreeMixin::class.java)
        context.setMixInAnnotations(NodeInfo::class.java, NodeInfoMixin::class.java)
        context.setMixInAnnotations(StateMachineRunId::class.java, StateMachineRunIdMixin::class.java)
        context.setMixInAnnotations(DigestService::class.java, DigestServiceMixin::class.java)
    }
}

private class CordaSerializableClassIntrospector(private val context: Module.SetupContext) : BasicClassIntrospector() {
    override fun constructPropertyCollector(
            config: MapperConfig<*>?,
            ac: AnnotatedClass?,
            type: JavaType,
            forSerialization: Boolean,
            mutatorPrefix: String?
    ): POJOPropertiesCollector {
        if (hasCordaSerializable(type.rawClass)) {
            // Adjust the field visibility of CordaSerializable classes on the fly as they are encountered.
            context.configOverride(type.rawClass).visibility = Value.defaultVisibility().withFieldVisibility(Visibility.ANY)
        }
        return super.constructPropertyCollector(config, ac, type, forSerialization, mutatorPrefix)
    }

    override fun constructPropertyCollector(config: MapperConfig<*>?, classDef: AnnotatedClass?, type: JavaType, forSerialization: Boolean, accNaming: AccessorNamingStrategy?): POJOPropertiesCollector {
        if (hasCordaSerializable(type.rawClass)) {
            // Adjust the field visibility of CordaSerializable classes on the fly as they are encountered.
            context.configOverride(type.rawClass).visibility = Value.defaultVisibility().withFieldVisibility(Visibility.ANY)
        }
        return super.constructPropertyCollector(config, classDef, type, forSerialization, accNaming)
    }
}

private class CordaSerializableBeanSerializerModifier : BeanSerializerModifier() {
        // We need to pass in a SerializerFactory when scanning for properties, but don't actually do any serialisation so any will do.
    private val serializerFactory = SerializerFactoryBuilder.build(AllWhitelist, javaClass.classLoader).also {
            registerCustomSerializers(it)
    }

    override fun changeProperties(config: SerializationConfig,
                                  beanDesc: BeanDescription,
                                  beanProperties: MutableList<BeanPropertyWriter>): MutableList<BeanPropertyWriter> {
        val beanClass = beanDesc.beanClass
        if (hasCordaSerializable(beanClass) && !SerializeAsToken::class.java.isAssignableFrom(beanClass)) {
            val typeInformation = serializerFactory.getTypeInformation(beanClass)
            val propertyNames = typeInformation.propertiesOrEmptyMap.mapNotNull {
                if (it.value.isCalculated) null else it.key
            }
            beanProperties.removeIf { it.name !in propertyNames }
        }
        return beanProperties
    }
}

@ToStringSerialize
@JsonDeserialize(using = NetworkHostAndPortDeserializer::class)
private interface NetworkHostAndPortMixin

private class NetworkHostAndPortDeserializer : SimpleDeserializer<NetworkHostAndPort>({ NetworkHostAndPort.parse(text) })

@JsonSerialize(using = PartyAndCertificateSerializer::class)
// TODO Add deserialization which follows the same lookup logic as Party
private interface PartyAndCertificateMixin

private class PartyAndCertificateSerializer : JsonSerializer<PartyAndCertificate>() {
    override fun serialize(value: PartyAndCertificate, gen: JsonGenerator, serializers: SerializerProvider) {
        val mapper = gen.codec as JacksonSupport.PartyObjectMapper
        if (mapper.isFullParties) {
            gen.writeObject(PartyAndCertificateJson(value.name, value.certPath))
        } else {
            gen.writeObject(value.party)
        }
    }
}

private class PartyAndCertificateJson(val name: CordaX500Name, val certPath: CertPath)

@JsonSerialize(using = SignedTransactionSerializer::class)
@JsonDeserialize(using = SignedTransactionDeserializer::class)
private interface SignedTransactionMixin

private class SignedTransactionSerializer : JsonSerializer<SignedTransaction>() {
    override fun serialize(value: SignedTransaction, gen: JsonGenerator, serializers: SerializerProvider) {
        val core = value.coreTransaction
        val stxJson = when (core) {
            is WireTransaction -> StxJson(wire = core, signatures = value.sigs)
            is FilteredTransaction -> StxJson(filtered = core, signatures = value.sigs)
            is NotaryChangeWireTransaction -> StxJson(notaryChangeWire = core, signatures = value.sigs)
            is ContractUpgradeWireTransaction -> StxJson(contractUpgradeWire = core, signatures = value.sigs)
            is ContractUpgradeFilteredTransaction -> StxJson(contractUpgradeFiltered = core, signatures = value.sigs)
            else -> throw IllegalArgumentException("Don't know about ${core.javaClass}")
        }
        gen.writeObject(stxJson)
    }
}

private class SignedTransactionDeserializer : JsonDeserializer<SignedTransaction>() {
    override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): SignedTransaction {
        val wrapper = parser.readValueAs<StxJson>()
        val core = wrapper.run { wire ?: filtered ?: notaryChangeWire ?: contractUpgradeWire ?: contractUpgradeFiltered!! }
        return SignedTransaction(core, wrapper.signatures)
    }
}

@JsonInclude(Include.NON_NULL)
private data class StxJson(
        val wire: WireTransaction? = null,
        val filtered: FilteredTransaction? = null,
        val notaryChangeWire: NotaryChangeWireTransaction? = null,
        val contractUpgradeWire: ContractUpgradeWireTransaction? = null,
        val contractUpgradeFiltered: ContractUpgradeFilteredTransaction? = null,
        val signatures: List<TransactionSignature>
) {
    init {
        val count = Booleans.countTrue(wire != null, filtered != null, notaryChangeWire != null, contractUpgradeWire != null, contractUpgradeFiltered != null)
        require(count == 1) { this }
    }
}

@JsonSerialize(using = WireTransactionSerializer::class)
@JsonDeserialize(using = WireTransactionDeserializer::class)
private interface WireTransactionMixin

private class WireTransactionSerializer : JsonSerializer<WireTransaction>() {
    @Suppress("INVISIBLE_MEMBER")
    override fun serialize(value: WireTransaction, gen: JsonGenerator, serializers: SerializerProvider) {
        gen.writeObject(WireTransactionJson(
                value.digestService,
                value.id,
                value.notary,
                value.inputs,
                value.outputs,
                value.commands,
                value.timeWindow,
                value.legacyAttachments.map { "$it-legacy" } + value.nonLegacyAttachments.map { it.toString() },
                value.references,
                value.privacySalt,
                value.networkParametersHash
        ))
    }
}

private class WireTransactionDeserializer : JsonDeserializer<WireTransaction>() {
    override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): WireTransaction {
        val wrapper = parser.readValueAs<WireTransactionJson>()
        // We're not concerned with backwards compatibility for any JSON string that was created with 4.11 and being materialised in 4.12.
        val (legacyAttachments, newerAttachments) = wrapper.attachments.partition { it.endsWith("-legacy") }
        val componentGroups = createComponentGroups(
                wrapper.inputs,
                wrapper.outputs,
                wrapper.commands,
                newerAttachments.map(SecureHash::parse),
                wrapper.notary,
                wrapper.timeWindow,
                wrapper.references,
                wrapper.networkParametersHash,
                legacyAttachments.map { SecureHash.parse(it.removeSuffix("-legacy")) }
        )
        return WireTransaction(componentGroups, wrapper.privacySalt, wrapper.digestService ?: DigestService.sha2_256)
    }
}

private class WireTransactionJson(@get:JsonInclude(Include.NON_NULL) val digestService: DigestService?,
                                  val id: SecureHash,
                                  val notary: Party?,
                                  val inputs: List<StateRef>,
                                  val outputs: List<TransactionState<*>>,
                                  val commands: List<Command<*>>,
                                  val timeWindow: TimeWindow?,
                                  val attachments: List<String>,
                                  val references: List<StateRef>,
                                  val privacySalt: PrivacySalt,
                                  val networkParametersHash: SecureHash?
)

private interface TransactionStateMixin {
    @get:JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
    val data: ContractState
    @get:JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
    val constraint: AttachmentConstraint
}

private interface CommandMixin {
    @get:JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
    val value: CommandData
}

@JsonDeserialize(using = TimeWindowDeserializer::class)
private interface TimeWindowMixin

private class TimeWindowDeserializer : JsonDeserializer<TimeWindow>() {
    override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): TimeWindow {
        return parser.readValueAs<TimeWindowJson>().run {
            when {
                fromTime != null && untilTime != null -> TimeWindow.between(fromTime, untilTime)
                fromTime != null -> TimeWindow.fromOnly(fromTime)
                untilTime != null -> TimeWindow.untilOnly(untilTime)
                else -> throw JsonParseException(parser, "Neither fromTime nor untilTime exists for TimeWindow")
            }
        }
    }
}

private data class TimeWindowJson(val fromTime: Instant?, val untilTime: Instant?)

@JsonSerialize(using = PrivacySaltSerializer::class)
@JsonDeserialize(using = PrivacySaltDeserializer::class)
private interface PrivacySaltMixin

private class PrivacySaltSerializer : JsonSerializer<PrivacySalt>() {
    override fun serialize(value: PrivacySalt, gen: JsonGenerator, serializers: SerializerProvider) {
        gen.writeString(value.bytes.toHexString())
    }
}

private class PrivacySaltDeserializer : SimpleDeserializer<PrivacySalt>({ PrivacySalt(text.parseAsHex()) })

// TODO Add a lookup function by number ID in Crypto
private val signatureSchemesByNumberID = Crypto.supportedSignatureSchemes().associateBy { it.schemeNumberID }

@JsonSerialize(using = SignatureMetadataSerializer::class)
@JsonDeserialize(using = SignatureMetadataDeserializer::class)
private interface SignatureMetadataMixin

private class SignatureMetadataSerializer : JsonSerializer<SignatureMetadata>() {
    override fun serialize(value: SignatureMetadata, gen: JsonGenerator, serializers: SerializerProvider) {
        gen.jsonObject {
            writeNumberField("platformVersion", value.platformVersion)
            writeObjectField("scheme", value.schemeNumberID.let { signatureSchemesByNumberID[it] ?: it })
        }
    }
}

private class SignatureMetadataDeserializer : JsonDeserializer<SignatureMetadata>() {
    override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): SignatureMetadata {
        val json = parser.readValueAsTree<ObjectNode>()
        val scheme = json["scheme"]
        val schemeNumberID = if (scheme is IntNode) {
            scheme.intValue()
        } else {
            Crypto.findSignatureScheme(scheme.textValue()).schemeNumberID
        }
        return SignatureMetadata(json["platformVersion"].intValue(), schemeNumberID)
    }
}

@JsonSerialize(using = PartialTreeSerializer::class)
@JsonDeserialize(using = PartialTreeDeserializer::class)
private interface PartialTreeMixin

private class PartialTreeSerializer : JsonSerializer<PartialTree>() {
    override fun serialize(value: PartialTree, gen: JsonGenerator, serializers: SerializerProvider) {
        gen.writeObject(convert(value))
    }

    private fun convert(tree: PartialTree): PartialTreeJson {
        return when (tree) {
            is PartialTree.IncludedLeaf -> PartialTreeJson(includedLeaf = tree.hash)
            is PartialTree.Leaf -> PartialTreeJson(leaf = tree.hash)
            is PartialTree.Node -> PartialTreeJson(left = convert(tree.left), right = convert(tree.right), hashAlgorithm = tree.hashAlgorithm)
            else -> throw IllegalArgumentException("Don't know how to serialize $tree")
        }
    }
}

private class PartialTreeDeserializer : JsonDeserializer<PartialTree>() {
    override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): PartialTree {
        return convert(parser.readValueAs(PartialTreeJson::class.java))
    }

    private fun convert(wrapper: PartialTreeJson): PartialTree {
        return wrapper.run {
            when {
                includedLeaf != null -> PartialTree.IncludedLeaf(includedLeaf)
                leaf != null -> PartialTree.Leaf(leaf)
                else -> PartialTree.Node(convert(left!!), convert(right!!), hashAlgorithm ?: SHA2_256)
            }
        }
    }
}

@JsonInclude(Include.NON_NULL)
private class PartialTreeJson(val includedLeaf: SecureHash? = null,
                              val leaf: SecureHash? = null,
                              val left: PartialTreeJson? = null,
                              val right: PartialTreeJson? = null,
                              val hashAlgorithm: String? = null) {
    init {
        if (includedLeaf != null) {
            require(leaf == null && left == null && right == null) { "Invalid JSON structure" }
        } else if (leaf != null) {
            require(left == null && right == null) { "Invalid JSON structure" }
        } else {
            require(left != null && right != null) { "Invalid JSON structure" }
        }
    }
}

@JsonSerialize(using = SignatureSchemeSerializer::class)
@JsonDeserialize(using = SignatureSchemeDeserializer::class)
private interface SignatureSchemeMixin

private class SignatureSchemeSerializer : JsonSerializer<SignatureScheme>() {
    override fun serialize(value: SignatureScheme, gen: JsonGenerator, serializers: SerializerProvider) {
        gen.writeString(value.schemeCodeName)
    }
}

private class SignatureSchemeDeserializer : JsonDeserializer<SignatureScheme>() {
    override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): SignatureScheme {
        return if (parser.currentToken == JsonToken.VALUE_NUMBER_INT) {
            signatureSchemesByNumberID[parser.intValue] ?: throw JsonParseException(parser, "Unable to find SignatureScheme ${parser.text}")
        } else {
            Crypto.findSignatureScheme(parser.text)
        }
    }
}

@JsonSerialize(using = SerializedBytesSerializer::class)
@JsonDeserialize(using = SerializedBytesDeserializer::class)
private class SerializedBytesMixin

private class SerializedBytesSerializer : JsonSerializer<SerializedBytes<*>>() {
    override fun serialize(value: SerializedBytes<*>, gen: JsonGenerator, serializers: SerializerProvider) {
        val deserialized = value.deserialize<Any>()
        gen.jsonObject {
            writeStringField("class", deserialized.javaClass.name)
            writeObjectField("deserialized", deserialized)
        }
    }
}

private class SerializedBytesDeserializer : JsonDeserializer<SerializedBytes<*>>() {
    override fun deserialize(parser: JsonParser, context: DeserializationContext): SerializedBytes<Any> {
        return if (parser.currentToken == JsonToken.START_OBJECT) {
            val mapper = parser.codec as ObjectMapper
            val json = parser.readValueAsTree<ObjectNode>()
            val clazz = context.findClass(json["class"].textValue())
            val pojo = mapper.convertValue(json["deserialized"], clazz)
            pojo.serialize()
        } else {
            SerializedBytes(parser.binaryValue)
        }
    }
}

@JsonDeserialize(using = JacksonSupport.PartyDeserializer::class)
private interface AbstractPartyMixin

@JsonSerialize(using = JacksonSupport.AnonymousPartySerializer::class)
@JsonDeserialize(using = JacksonSupport.AnonymousPartyDeserializer::class)
private interface AnonymousPartyMixin

@JsonSerialize(using = JacksonSupport.PartySerializer::class)
private interface PartyMixin

@ToStringSerialize
@JsonDeserialize(using = JacksonSupport.CordaX500NameDeserializer::class)
private interface CordaX500NameMixin

@JsonDeserialize(using = JacksonSupport.NodeInfoDeserializer::class)
private interface NodeInfoMixin

@ToStringSerialize
@JsonDeserialize(using = JacksonSupport.SecureHashDeserializer::class)
private interface SecureHashMixin

@JsonSerialize(using = JacksonSupport.PublicKeySerializer::class)
@JsonDeserialize(using = JacksonSupport.PublicKeyDeserializer::class)
private interface PublicKeyMixin

@JsonSerialize(using = StateMachineRunIdSerializer::class)
@JsonDeserialize(using = StateMachineRunIdDeserializer::class)
private interface StateMachineRunIdMixin

private class StateMachineRunIdSerializer : StdScalarSerializer<StateMachineRunId>(StateMachineRunId::class.java) {
    private val uuidSerializer = UUIDSerializer()

    override fun isEmpty(provider: SerializerProvider?, value: StateMachineRunId): Boolean {
        return uuidSerializer.isEmpty(provider, value.uuid)
    }

    override fun serialize(value: StateMachineRunId, gen: JsonGenerator?, provider: SerializerProvider?) {
        uuidSerializer.serialize(value.uuid, gen, provider)
    }
}

private class StateMachineRunIdDeserializer : FromStringDeserializer<StateMachineRunId>(StateMachineRunId::class.java) {
    override fun _deserialize(value: String, ctxt: DeserializationContext?): StateMachineRunId {
        return StateMachineRunId(UUID.fromString(value))
    }
}

@Suppress("unused_parameter")
@ToStringSerialize
private abstract class AmountMixin @JsonCreator(mode = DISABLED) constructor(
    quantity: Long,
    displayTokenSize: BigDecimal,
    token: Any
) {
    /**
     * This mirrors the [Amount] constructor that we want Jackson to use, and
     * requires that we also tell Jackson NOT to use [Amount]'s primary constructor.
     */
    @JsonCreator constructor(
        @JsonProperty("quantity")
        quantity: Long,

        @JsonDeserialize(using = TokenDeserializer::class)
        @JsonProperty("token")
        token: Any
    ) : this(quantity, Amount.getDisplayTokenSize(token), token)
}

/**
 * Implements polymorphic deserialization for [Amount.token]. Kotlin must
 * be able to determine the concrete [Amount] type at runtime, or it will
 * fall back to using [Currency].
 */
private class TokenDeserializer(private val tokenType: Class<*>) : JsonDeserializer<Any>(), ContextualDeserializer {
    @Suppress("unused")
    constructor() : this(Currency::class.java)

    override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): Any = parser.readValueAs(tokenType)

    override fun createContextual(ctxt: DeserializationContext, property: BeanProperty?): TokenDeserializer {
        if (property == null) return this
        return TokenDeserializer(property.type.rawClass.let { type ->
            if (type == Any::class.java) Currency::class.java else type
        })
    }
}

/**
 * Intercepts bean-based deserialization for the generic [Amount] type.
 */
private class AmountBeanDeserializerModifier : BeanDeserializerModifier() {
    override fun modifyDeserializer(config: DeserializationConfig, description: BeanDescription, deserializer: JsonDeserializer<*>): JsonDeserializer<*> {
        val modified = super.modifyDeserializer(config, description, deserializer)
        return if (Amount::class.java.isAssignableFrom(description.beanClass)) {
            AmountDeserializer(modified)
        } else {
            modified
        }
    }
}

private class AmountDeserializer(delegate: JsonDeserializer<*>) : DelegatingDeserializer(delegate) {
    override fun newDelegatingInstance(newDelegatee: JsonDeserializer<*>) = AmountDeserializer(newDelegatee)

    override fun deserialize(parser: JsonParser, context: DeserializationContext?): Any {
        return if (parser.currentToken() == JsonToken.VALUE_STRING) {
            /*
             * This is obviously specific to Amount<Currency>, and is here to
             * preserve the original deserializing behaviour for this case.
             */
            Amount.parseCurrency(parser.text)
        } else {
            /*
             * Otherwise continue deserializing our Bean as usual.
             */
            _delegatee.deserialize(parser, context)
        }
    }
}

@JsonSerialize(using = DigestServiceSerializer::class)
@JsonDeserialize(using = DigestServiceDeserializer::class)
private interface DigestServiceMixin

private class DigestServiceSerializer : JsonSerializer<DigestService>() {
    override fun serialize(value: DigestService, gen: JsonGenerator, serializers: SerializerProvider) {
        gen.writeObject(DigestServiceJson(value.hashAlgorithm))
    }
}

private class DigestServiceDeserializer : JsonDeserializer<DigestService>() {
    override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): DigestService {
        val wrapper = parser.readValueAs<DigestServiceJson>()
        return DigestService(wrapper.hashAlgorithm)
    }
}

private class DigestServiceJson(val hashAlgorithm: String)

@JsonDeserialize(using = JacksonSupport.OpaqueBytesDeserializer::class)
private interface ByteSequenceMixin {
    @Suppress("unused")
    @JsonValue
    fun copyBytes(): ByteArray
}

@JsonSerialize
@JsonDeserialize
private interface ByteSequenceWithPropertiesMixin {
    @Suppress("unused")
    @JsonValue(false)
    fun copyBytes(): ByteArray
}
