import Foundation

class AMFReference {
    var strings: [String] = []
    var objects: [Any] = []

    func getString(_ index: Int) throws -> String {
        if strings.count <= index {
            throw AMFSerializerError.outOfIndex
        }
        return strings[index]
    }

    func getObject(_ index: Int) throws -> Any {
        if objects.count <= index {
            throw AMFSerializerError.outOfIndex
        }
        return objects[index]
    }

    func indexOf<T: Equatable>(_ value: T) -> Int? {
        for (index, data) in objects.enumerated() {
            if let data: T = data as? T, data == value {
                return index
            }
        }
        return nil
    }

    func indexOf(_ value: [Int32]) -> Int? {
        return nil
    }

    func indexOf(_ value: [UInt32]) -> Int? {
        return nil
    }

    func indexOf(_ value: [Double]) -> Int? {
        return nil
    }

    func indexOf(_ value: [Any?]) -> Int? {
        return nil
    }

    func indexOf(_ value: ASObject) -> Int? {
        for (index, data) in objects.enumerated() {
            if let data: ASObject = data as? ASObject, data.description == value.description {
                return index
            }
        }
        return nil
    }

    func indexOf(_ value: String) -> Int? {
        return strings.index(of: value)
    }
}

// MARK: -
/**
 AMF3 Serializer

 - seealso: http: //wwwimages.adobe.com/www.adobe.com/content/dam/Adobe/en/devnet/amf/pdf/amf-file-format-spec.pdf
 */
class AMF3Serializer: ByteArray {
    enum `Type`: UInt8 {
        case undefined    = 0x00
        case null         = 0x01
        case boolFalse    = 0x02
        case boolTrue     = 0x03
        case integer      = 0x04
        case number       = 0x05
        case string       = 0x06
        case xml          = 0x07
        case date         = 0x08
        case array        = 0x09
        case object       = 0x0A
        case xmlString    = 0x0B
        case byteArray    = 0x0C
        case vectorInt    = 0x0D
        case vectorUInt   = 0x0E
        case vectorNumber = 0x0F
        case vectorObject = 0x10
        case dictionary   = 0x11
    }

    var reference: AMFReference = AMFReference()
}

extension AMF3Serializer: AMFSerializer {
    // MARK: AMFSerializer
    @discardableResult
    func serialize(_ value: Any?) -> Self {
        if value == nil {
            return writeUInt8(Type.null.rawValue)
        }
        switch value {
        case let value as Int:
            return serialize(Double(value))
        case let value as UInt:
            return serialize(Double(value))
        case let value as Int8:
            return serialize(Double(value))
        case let value as UInt8:
            return serialize(Double(value))
        case let value as Int16:
            return serialize(Double(value))
        case let value as UInt16:
            return serialize(Double(value))
        case let value as Int32:
            return serialize(Double(value))
        case let value as UInt32:
            return serialize(Double(value))
        case let value as Float:
            return serialize(Double(value))
        case let value as Double:
            return serialize(Double(value))
        case let value as Date:
            return serialize(value)
        case let value as String:
            return serialize(value)
        case let value as Bool:
            return serialize(value)
        case let value as ASArray:
            return serialize(value)
        case let value as ASObject:
            return serialize(value)
        default:
            return writeUInt8(Type.undefined.rawValue)
        }
    }

    func deserialize() throws -> Any? {
        guard let type: Type = Type(rawValue: try readUInt8()) else {
            throw AMFSerializerError.deserialize
        }
        position -= 1
        switch type {
        case .undefined:
            position += 1
            return kASUndefined
        case .null:
            position += 1
            return nil
        case .boolFalse:
            return try deserialize() as Bool
        case .boolTrue:
            return try deserialize() as Bool
        case .integer:
            return try deserialize() as Int
        case .number:
            return try deserialize() as Double
        case .string:
            return try deserialize() as String
        case .xml:
            return try deserialize() as ASXMLDocument
        case .date:
            return try deserialize() as Date
        case .array:
            return try deserialize() as ASArray
        case .object:
            return try deserialize() as ASObject
        case .xmlString:
            return try deserialize() as ASXML
        case .byteArray:
            return try deserialize() as ByteArray
        case .vectorInt:
            return try deserialize() as [Int32]
        case .vectorUInt:
            return try deserialize() as [UInt32]
        case .vectorNumber:
            return try deserialize() as [Double]
        case .vectorObject:
            return try deserialize() as [Any?]
        case .dictionary:
            assertionFailure("Unsupported")
            return nil
        }
    }

    /**
     - seealso: 3.4 false Type
     - seealso: 3.5 true type
     */
    @discardableResult
    func serialize(_ value: Bool) -> Self {
        return writeUInt8(value ? Type.boolTrue.rawValue: Type.boolFalse.rawValue)
    }

    func deserialize() throws -> Bool {
        switch try readUInt8() {
        case Type.boolTrue.rawValue:
            return true
        case Type.boolFalse.rawValue:
            return false
        default:
            throw AMFSerializerError.deserialize
        }
    }

    /**
     - seealso: 3.6 integer type
     */
    @discardableResult
    func serialize(_ value: Int) -> Self {
        return writeUInt8(Type.integer.rawValue).serializeU29(value)
    }

    func deserialize() throws -> Int {
        guard try readUInt8() == Type.integer.rawValue else {
            throw AMFSerializerError.deserialize
        }
        return try deserializeU29()
    }

    /**
     - seealso: 3.7 double type
     */
    @discardableResult
    func serialize(_ value: Double) -> Self {
        return writeUInt8(Type.number.rawValue).writeDouble(value)
    }

    func deserialize() throws -> Double {
        guard try readUInt8() == Type.number.rawValue else {
            throw AMFSerializerError.deserialize
        }
        return try readDouble()
    }

    /**
     - seealso: 3.8 String type
     */
    @discardableResult
    func serialize(_ value: String) -> Self {
        return writeUInt8(Type.string.rawValue).serializeUTF8(value)
    }

    func deserialize() throws -> String {
        guard try readUInt8() == Type.string.rawValue else {
            throw AMFSerializerError.deserialize
        }
        return try deserializeUTF8()
    }

    /**
     - seealso: 3.9 XML type
     */
    @discardableResult
    func serialize(_ value: ASXMLDocument) -> Self {
        writeUInt8(Type.xml.rawValue)
        if let index: Int = reference.indexOf(value) {
            return serializeU29(index << 1)
        }
        reference.objects.append(value)
        let utf8: Data = Data(value.description.utf8)
        return serialize(utf8.count << 1 | 0x01).writeBytes(utf8)
    }

    func deserialize() throws -> ASXMLDocument {
        guard try readUInt8() == Type.xml.rawValue else {
            throw AMFSerializerError.deserialize
        }
        let refs: Int = try deserializeU29()
        if (refs & 0x01) == 0 {
            guard let document: ASXMLDocument = try reference.getObject(refs >> 1) as? ASXMLDocument else {
                throw AMFSerializerError.deserialize
            }
            return document
        }
        let document: ASXMLDocument = ASXMLDocument(data: try readUTF8Bytes(refs >> 1))
        reference.objects.append(document)
        return document
    }

    /**
     - seealso: 3.10 Date type
     */
    @discardableResult
    func serialize(_ value: Date) -> Self {
        writeUInt8(Type.date.rawValue)
        if let index: Int = reference.indexOf(value) {
            return serializeU29(index << 1)
        }
        reference.objects.append(value)
        return serializeU29(0x01).writeDouble(value.timeIntervalSince1970 * 1000)
    }

    func deserialize() throws -> Date {
        guard try readUInt8() == Type.date.rawValue else {
            throw AMFSerializerError.deserialize
        }
        let refs: Int = try deserializeU29()
        if (refs & 0x01) == 0 {
            guard let date: Date = try reference.getObject(refs >> 1) as? Date else {
                throw AMFSerializerError.deserialize
            }
            return date
        }
        let date: Date = Date(timeIntervalSince1970: try readDouble() / 1000)
        reference.objects.append(date)
        return date
    }

    /**
     - seealso: 3.11 Array type
     */
    @discardableResult
    func serialize(_ value: ASArray) -> Self {
        writeUInt8(Type.array.rawValue)
        if let index: Int = reference.indexOf(value) {
            return serializeU29(index << 1)
        }
        reference.objects.append(value)
        serialize(value.length << 1 | 0x01)
        for (key, value) in value.dict {
            serialize(key).serialize(value)
        }
        serialize("")
        for value in value.data {
            serialize(value)
        }
        return self
    }

    func deserialize() throws -> ASArray {
        guard try readUInt8() == Type.array.rawValue else {
            throw AMFSerializerError.deserialize
        }
        return ASArray()
    }

    /**
     - seealso: 3.12 Object type
     - note: ASObject = Dictionary<String, Any?>
     */
    @discardableResult
    func serialize(_ value: ASObject) -> Self {
        writeUInt8(Type.object.rawValue)
        if let index: Int = reference.indexOf(value) {
            return serializeU29(index << 1)
        }
        reference.objects.append(value)
        for (key, value) in value {
            serialize(key).serialize(value)
        }
        return serialize("")
    }

    func deserialize() throws -> ASObject {
        guard try readUInt8() == Type.object.rawValue else {
            throw AMFSerializerError.deserialize
        }
        return ASObject()
    }

    /**
     - seealso: 3.13 XML type
     */
    @discardableResult
    func serialize(_ value: ASXML) -> Self {
        writeUInt8(Type.xmlString.rawValue)
        if let index: Int = reference.indexOf(value) {
            return serializeU29(index << 1)
        }
        reference.objects.append(value)
        let utf8: Data = Data(value.description.utf8)
        return serialize(utf8.count << 1 | 0x01).writeBytes(utf8)
    }

    func deserialize() throws -> ASXML {
        guard try readUInt8() == Type.xml.rawValue else {
            throw AMFSerializerError.deserialize
        }
        let refs: Int = try deserializeU29()
        if (refs & 0x01) == 0 {
            guard let xml: ASXML = try reference.getObject(refs >> 1) as? ASXML else {
                throw AMFSerializerError.deserialize
            }
            return xml
        }
        let xml: ASXML = ASXML(data: try readUTF8Bytes(refs >> 1))
        reference.objects.append(xml)
        return xml
    }

    /**
     - seealso: 3.14 ByteArray type
     - note: flash.utils.ByteArray = lf.ByteArray
     */
    @discardableResult
    func serialize(_ value: ByteArray) -> Self {
        return self
    }

    func deserialize() throws -> ByteArray {
        return ByteArray()
    }

    /**
     - seealso: 3.15 Vector Type, vector-int-type
     */
    @discardableResult
    func serialize(_ value: [Int32]) -> Self {
        writeUInt8(Type.vectorInt.rawValue)
        if let index: Int = reference.indexOf(value) {
            return serializeU29(index << 1)
        }
        reference.objects.append(value)
        serializeU29(value.count << 1 | 0x01).writeUInt8(0x00)
        for v in value {
            writeInt32(v)
        }
        return self
    }

    func deserialize() throws -> [Int32] {
        guard try readUInt8() == Type.vectorInt.rawValue else {
            throw AMFSerializerError.deserialize
        }
        return []
    }

    /**
     - seealso: 3.15 Vector Type, vector-uint-type
     */
    @discardableResult
    func serialize(_ value: [UInt32]) -> Self {
        writeUInt8(Type.vectorUInt.rawValue)
        if let index: Int = reference.indexOf(value) {
            return serializeU29(index << 1)
        }
        reference.objects.append(value)
        serializeU29(value.count << 1 | 0x01).writeUInt8(0x00)
        for v in value {
            writeUInt32(v)
        }
        return self
    }

    func deserialize() throws -> [UInt32] {
        guard try readUInt8() == Type.vectorUInt.rawValue else {
            throw AMFSerializerError.deserialize
        }
        return []
    }

    /**
     - seealso: 3.15 Vector Type, vector-number-type
     */
    @discardableResult
    func serialize(_ value: [Double]) -> Self {
        writeUInt8(Type.vectorNumber.rawValue)
        if let index: Int = reference.indexOf(value) {
            return serializeU29(index << 1)
        }
        reference.objects.append(value)
        serializeU29(value.count << 1 | 0x01).writeUInt8(0x00)
        for v in value {
            writeDouble(v)
        }
        return self
    }

    func deserialize() throws -> [Double] {
        guard try readUInt8() == Type.vectorNumber.rawValue else {
            throw AMFSerializerError.deserialize
        }
        return []
    }

    /**
     - seealso: 3.15 Vector Type, vector-object-type
     */
    @discardableResult
    func serialize(_ value: [Any?]) -> Self {
        writeUInt8(Type.vectorObject.rawValue)
        if let index: Int = reference.indexOf(value) {
            return serializeU29(index << 1)
        }
        reference.objects.append(value)
        serializeU29(value.count << 1 | 0x01).serializeUTF8("*")
        for v in value {
            serialize(v)
        }
        return self
    }

    func deserialize() throws -> [Any?] {
        guard try readUInt8() == Type.array.rawValue else {
            throw AMFSerializerError.deserialize
        }
        return []
    }

    /**
     - seealso: 1.3.1 Variable Length Unsigned 29-bit Integer Encoding
     */
    @discardableResult
    private func serializeU29(_ value: Int) -> Self {
        if value < Int(Int32.min) || Int(Int32.max) < value {
            return serialize(Double(value))
        }
        let value: UInt32 = UInt32(value)
        switch UInt32(0) {
        case value & 0xFFFFFF80:
            return writeUInt8(UInt8(value & 0x7f))
        case value & 0xFFFFC000:
            return writeUInt8(UInt8(value >> 7 | 0x80))
                .writeUInt8(UInt8(value & 0x7F))
        case value & 0xFFE00000:
            return writeUInt8(UInt8(value >> 14 | 0x80))
                .writeUInt8(UInt8(value >> 7 | 0x80))
                .writeUInt8(UInt8(value & 0x7F))
        default:
            return writeUInt8(UInt8(value >> 22 | 0x80))
                .writeUInt8(UInt8(value >> 15 | 0x80))
                .writeUInt8(UInt8(value >> 8 | 0x80))
                .writeUInt8(UInt8(value & 0xFF))
        }
    }

    private func deserializeU29() throws -> Int {
        var count: Int = 1
        var result: Int = 0
        var byte: UInt8 = try readUInt8()

        while byte & 0x80 != 0 && count < 4 {
            result <<= 7
            result |= Int(byte & 0x7F)
            byte = try readUInt8()
            count += 1
        }

        if count < 4 {
            result <<= 7
            result |= Int(byte)
        } else {
            result <<= 8
            result |= Int(byte)
        }

        return result
    }

    /**
     - seealso: 1.3.2 Strings and UTF-8
     */
    @discardableResult
    private func serializeUTF8(_ value: String) -> Self {
        if value.isEmpty {
            return serializeU29(0x01)
        }
        if let index: Int = reference.indexOf(value) {
            return serializeU29(index << 1)
        }
        let utf8: Data = Data(value.utf8)
        reference.strings.append(value)
        return serializeU29(utf8.count << 1 | 0x01).writeBytes(utf8)
    }

    private func deserializeUTF8() throws -> String {
        let ref: Int = try deserializeU29()
        if (ref & 0x01) == 0 {
            return try reference.getString(ref >> 1)
        }
        let string: String = try readUTF8Bytes(length)
        reference.strings.append(string)
        return string
    }
}
