import Foundation

class AMFSerializerUtil {
    private static var classes: [String: AnyClass] = [: ]

    static func getClassByAlias(_ name: String) -> AnyClass? {
        objc_sync_enter(classes)
        let clazz: AnyClass? = classes[name]
        objc_sync_exit(classes)
        return clazz
    }

    static func registerClassAlias(_ name: String, clazz: AnyClass) {
        objc_sync_enter(classes)
        classes[name] = clazz
        objc_sync_exit(classes)
    }
}

enum AMFSerializerError: Error {
    case deserialize
    case outOfIndex
}

// MARK: -
protocol AMFSerializer: ByteArrayConvertible {
    var reference: AMFReference { get set }

    @discardableResult
    func serialize(_ value: Bool) -> Self
    func deserialize() throws -> Bool

    @discardableResult
    func serialize(_ value: String) -> Self
    func deserialize() throws -> String

    @discardableResult
    func serialize(_ value: Int) -> Self
    func deserialize() throws -> Int

    @discardableResult
    func serialize(_ value: Double) -> Self
    func deserialize() throws -> Double

    @discardableResult
    func serialize(_ value: Date) -> Self
    func deserialize() throws -> Date

    @discardableResult
    func serialize(_ value: [Any?]) -> Self
    func deserialize() throws -> [Any?]

    @discardableResult
    func serialize(_ value: ASArray) -> Self
    func deserialize() throws -> ASArray

    @discardableResult
    func serialize(_ value: ASObject) -> Self
    func deserialize() throws -> ASObject

    @discardableResult
    func serialize(_ value: ASXMLDocument) -> Self
    func deserialize() throws -> ASXMLDocument

    @discardableResult
    func serialize(_ value: Any?) -> Self
    func deserialize() throws -> Any?
}

// MARK: -
/**
 AMF0Serializer

 -seealso: http: //wwwimages.adobe.com/content/dam/Adobe/en/devnet/amf/pdf/amf0-file-format-specification.pdf
 */
class AMF0Serializer: ByteArray {
    enum `Type`: UInt8 {
        case number      = 0x00
        case bool        = 0x01
        case string      = 0x02
        case object      = 0x03
        // case MovieClip   = 0x04
        case null        = 0x05
        case undefined   = 0x06
        case reference   = 0x07
        case ecmaArray   = 0x08
        case objectEnd   = 0x09
        case strictArray = 0x0a
        case date        = 0x0b
        case longString  = 0x0c
        case unsupported = 0x0d
        // case RecordSet   = 0x0e
        case xmlDocument = 0x0f
        case typedObject = 0x10
        case avmplush    = 0x11
    }

    var reference: AMFReference = AMFReference()
}

extension AMF0Serializer: 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 {
            return nil
        }
        position -= 1
        switch type {
        case .number:
            return try deserialize() as Double
        case .bool:
            return try deserialize() as Bool
        case .string:
            return try deserialize() as String
        case .object:
            return try deserialize() as ASObject
        case .null:
            position += 1
            return nil
        case .undefined:
            position += 1
            return kASUndefined
        case .reference:
            assertionFailure("TODO")
            return nil
        case .ecmaArray:
            return try deserialize() as ASArray
        case .objectEnd:
            assertionFailure()
            return nil
        case .strictArray:
            return try deserialize() as [Any?]
        case .date:
            return try deserialize() as Date
        case .longString:
            return try deserialize() as String
        case .unsupported:
            assertionFailure("Unsupported")
            return nil
        case .xmlDocument:
            return try deserialize() as ASXMLDocument
        case .typedObject:
            assertionFailure("TODO")
            return nil
        case .avmplush:
            assertionFailure("TODO")
            return nil
        }
    }

    /**
     * @see 2.2 Number Type
     */
    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()
    }

    func serialize(_ value: Int) -> Self {
        return serialize(Double(value))
    }

    func deserialize() throws -> Int {
        return Int(try deserialize() as Double)
    }

    /**
     * @see 2.3 Boolean Type
     */
    func serialize(_ value: Bool) -> Self {
        return writeBytes(Data([Type.bool.rawValue, value ? 0x01 : 0x00]))
    }

    func deserialize() throws -> Bool {
        guard try readUInt8() == Type.bool.rawValue else {
            throw AMFSerializerError.deserialize
        }
        return try readUInt8() == 0x01 ? true : false
    }

    /**
     * @see 2.4 String Type
     */
    func serialize(_ value: String) -> Self {
        let isLong: Bool = UInt32(UInt16.max) < UInt32(value.count)
        writeUInt8(isLong ? Type.longString.rawValue : Type.string.rawValue)
        return serializeUTF8(value, isLong)
    }

    func deserialize() throws -> String {
        switch try readUInt8() {
        case Type.string.rawValue:
            return try deserializeUTF8(false)
        case Type.longString.rawValue:
            return try deserializeUTF8(true)
        default:
            assertionFailure()
            return ""
        }
    }

    /**
     * 2.5 Object Type
     * typealias ECMAObject = Dictionary<String, Any?>
     */
    func serialize(_ value: ASObject) -> Self {
        writeUInt8(Type.object.rawValue)
        for (key, data) in value {
            serializeUTF8(key, false).serialize(data)
        }
        return serializeUTF8("", false).writeUInt8(Type.objectEnd.rawValue)
    }

    func deserialize() throws -> ASObject {
        var result: ASObject = ASObject()

        switch try readUInt8() {
        case Type.null.rawValue:
            return result
        case Type.object.rawValue:
            break
        default:
            throw AMFSerializerError.deserialize
        }

        while true {
            let key: String = try deserializeUTF8(false)
            guard key != "" else {
                position += 1
                break
            }
            result[key] = try deserialize()
        }

        return result
    }

    /**
     * @see 2.10 ECMA Array Type
     */
    func serialize(_ value: ASArray) -> Self {
        return self
    }

    func deserialize() throws -> ASArray {

        switch try readUInt8() {
        case Type.null.rawValue:
            return ASArray()
        case Type.ecmaArray.rawValue:
            break
        default:
            throw AMFSerializerError.deserialize
        }

        var result: ASArray = ASArray(count: Int(try readUInt32()))
        while true {
            let key: String = try deserializeUTF8(false)
            guard key != "" else {
                position += 1
                break
            }
            result[key] = try deserialize()
        }

        return result
    }

    /**
     * @see 2.12 Strict Array Type
     */
    func serialize(_ value: [Any?]) -> Self {
        writeUInt8(Type.strictArray.rawValue)
        if value.isEmpty {
            writeBytes(Data([0x00, 0x00, 0x00, 0x00]))
            return self
        }
        writeUInt32(UInt32(value.count))
        for v in value {
            serialize(v)
        }
        return self
    }

    func deserialize() throws -> [Any?] {
        guard try readUInt8() == Type.strictArray.rawValue else {
            throw AMFSerializerError.deserialize
        }
        var result: [Any?] = []
        let count: Int = Int(try readUInt32())
        for _ in 0..<count {
            result.append(try deserialize())
        }
        return result
    }

    /**
     * @see 2.13 Date Type
     */
    func serialize(_ value: Date) -> Self {
        return writeUInt8(Type.date.rawValue).writeDouble(value.timeIntervalSince1970 * 1000).writeBytes(Data([0x00, 0x00]))
    }

    func deserialize() throws -> Date {
        guard try readUInt8() == Type.date.rawValue else {
            throw AMFSerializerError.deserialize
        }
        let date: Date = Date(timeIntervalSince1970: try readDouble() / 1000)
        position += 2 // timezone offset
        return date
    }

    /**
     * @see 2.17 XML Document Type
     */
    func serialize(_ value: ASXMLDocument) -> Self {
        return writeUInt8(Type.xmlDocument.rawValue).serializeUTF8(value.description, true)
    }

    func deserialize() throws -> ASXMLDocument {
        guard try readUInt8() == Type.xmlDocument.rawValue else {
            throw AMFSerializerError.deserialize
        }
        return ASXMLDocument(data: try deserializeUTF8(true))
    }

    @discardableResult
    private func serializeUTF8(_ value: String, _ isLong: Bool) -> Self {
        let utf8: Data = Data(value.utf8)
        if isLong {
            writeUInt32(UInt32(utf8.count))
        } else {
            writeUInt16(UInt16(utf8.count))
        }
        return writeBytes(utf8)
    }

    private func deserializeUTF8(_ isLong: Bool) throws -> String {
        let length: Int = isLong ? Int(try readUInt32()) : Int(try readUInt16())
        return try readUTF8Bytes(length)
    }
}
