import Foundation
import MtProtoKitDynamic

struct ImageResource {
    let datacenterId: Int
    let volumeId: Int64
    let localId: Int32
    let secret: Int64
    let fileReference: Data?
    
    var resourceId: String {
        return "telegram-cloud-file-\(self.datacenterId)-\(self.volumeId)-\(self.localId)-\(self.secret)"
    }
}

private final class Buffer {
    var data: UnsafeMutableRawPointer?
    var _size: UInt = 0
    private var capacity: UInt = 0
    private let freeWhenDone: Bool
    
    var size: Int {
        return Int(self._size)
    }
    
    deinit {
        if self.freeWhenDone {
            free(self.data)
        }
    }
    
    init(memory: UnsafeMutableRawPointer?, size: Int, capacity: Int, freeWhenDone: Bool) {
        self.data = memory
        self._size = UInt(size)
        self.capacity = UInt(capacity)
        self.freeWhenDone = freeWhenDone
    }
    
    init() {
        self.data = nil
        self._size = 0
        self.capacity = 0
        self.freeWhenDone = true
    }
    
    convenience init(data: Data?) {
        self.init()
        
        if let data = data {
            data.withUnsafeBytes { bytes in
                self.appendBytes(bytes, length: UInt(data.count))
            }
        }
    }

    func makeData() -> Data {
        return self.withUnsafeMutablePointer { pointer, size -> Data in
            if let pointer = pointer {
                return Data(bytes: pointer.assumingMemoryBound(to: UInt8.self), count: Int(size))
            } else {
                return Data()
            }
        }
    }
    
    var description: String {
        get {
            var string = ""
            if let data = self.data {
                var i: UInt = 0
                let bytes = data.assumingMemoryBound(to: UInt8.self)
                while i < _size && i < 8 {
                    string += String(format: "%02x", Int(bytes.advanced(by: Int(i)).pointee))
                    i += 1
                }
                if i < _size {
                    string += "...\(_size)b"
                }
            } else {
                string += "<null>"
            }
            return string
        }
    }
    
    func appendBytes(_ bytes: UnsafeRawPointer, length: UInt) {
        if self.capacity < self._size + length {
            self.capacity = self._size + length + 128
            if self.data == nil {
                self.data = malloc(Int(self.capacity))!
            }
            else {
                self.data = realloc(self.data, Int(self.capacity))!
            }
        }
        
        memcpy(self.data?.advanced(by: Int(self._size)), bytes, Int(length))
        self._size += length
    }
    
    func appendBuffer(_ buffer: Buffer) {
        if self.capacity < self._size + buffer._size {
            self.capacity = self._size + buffer._size + 128
            if self.data == nil {
                self.data = malloc(Int(self.capacity))!
            }
            else {
                self.data = realloc(self.data, Int(self.capacity))!
            }
        }
        
        memcpy(self.data?.advanced(by: Int(self._size)), buffer.data, Int(buffer._size))
    }
    
    func appendInt32(_ value: Int32) {
        var v = value
        self.appendBytes(&v, length: 4)
    }
    
    func appendInt64(_ value: Int64) {
        var v = value
        self.appendBytes(&v, length: 8)
    }
    
    func appendDouble(_ value: Double) {
        var v = value
        self.appendBytes(&v, length: 8)
    }
    
    func withUnsafeMutablePointer<R>(_ f: (UnsafeMutableRawPointer?, UInt) -> R) -> R {
        return f(self.data, self._size)
    }
}

private class BufferReader {
    private let buffer: Buffer
    private(set) var offset: UInt = 0
    
    init(_ buffer: Buffer) {
        self.buffer = buffer
    }
    
    func reset() {
        self.offset = 0
    }
    
    func skip(_ count: Int) {
        self.offset = min(self.buffer._size, self.offset + UInt(count))
    }
    
    func readInt32() -> Int32? {
        if self.offset + 4 <= self.buffer._size {
            let value: Int32 = buffer.data!.advanced(by: Int(self.offset)).assumingMemoryBound(to: Int32.self).pointee
            self.offset += 4
            return value
        }
        return nil
    }
    
    func readInt64() -> Int64? {
        if self.offset + 8 <= self.buffer._size {
            let value: Int64 = buffer.data!.advanced(by: Int(self.offset)).assumingMemoryBound(to: Int64.self).pointee
            self.offset += 8
            return value
        }
        return nil
    }
    
    func readDouble() -> Double? {
        if self.offset + 8 <= self.buffer._size {
            let value: Double = buffer.data!.advanced(by: Int(self.offset)).assumingMemoryBound(to: Double.self).pointee
            self.offset += 8
            return value
        }
        return nil
    }
    
    func readBytesAsInt32(_ count: Int) -> Int32? {
        if count == 0 {
            return 0
        }
        else if count > 0 && count <= 4 || self.offset + UInt(count) <= self.buffer._size {
            var value: Int32 = 0
            memcpy(&value, self.buffer.data?.advanced(by: Int(self.offset)), count)
            self.offset += UInt(count)
            return value
        }
        return nil
    }
    
    func readBuffer(_ count: Int) -> Buffer? {
        if count >= 0 && self.offset + UInt(count) <= self.buffer._size {
            let buffer = Buffer()
            buffer.appendBytes((self.buffer.data?.advanced(by: Int(self.offset)))!, length: UInt(count))
            self.offset += UInt(count)
            return buffer
        }
        return nil
    }
}

private func serializeBytes(_ value: Buffer, buffer: Buffer, boxed: Bool) {
    if boxed {
        buffer.appendInt32(-1255641564)
    }
    
    var length: Int32 = Int32(value.size)
    var padding: Int32 = 0
    if (length >= 254)
    {
        var tmp: UInt8 = 254
        buffer.appendBytes(&tmp, length: 1)
        buffer.appendBytes(&length, length: 3)
        padding = (((length % 4) == 0 ? length : (length + 4 - (length % 4)))) - length;
    }
    else
    {
        buffer.appendBytes(&length, length: 1)
        
        let e1 = (((length + 1) % 4) == 0 ? (length + 1) : ((length + 1) + 4 - ((length + 1) % 4)))
        padding = (e1) - (length + 1)
    }
    
    if value.size != 0 {
        buffer.appendBytes(value.data!, length: UInt(length))
    }
    
    var i: Int32 = 0
    var tmp: UInt8 = 0
    while i < padding {
        buffer.appendBytes(&tmp, length: 1)
        i += 1
    }
}

private func roundUp(_ numToRound: Int, multiple: Int) -> Int {
    if multiple == 0 {
        return numToRound
    }
    
    let remainder = numToRound % multiple
    if remainder == 0 {
        return numToRound
    }
    
    return numToRound + multiple - remainder
}

private func parseBytes(_ reader: BufferReader) -> Buffer? {
    if let tmp = reader.readBytesAsInt32(1) {
        var paddingBytes: Int = 0
        var length: Int = 0
        if tmp == 254 {
            if let len = reader.readBytesAsInt32(3) {
                length = Int(len)
                paddingBytes = roundUp(length, multiple: 4) - length
            }
            else {
                return nil
            }
        }
        else {
            length = Int(tmp)
            paddingBytes = roundUp(length + 1, multiple: 4) - (length + 1)
        }
        
        let buffer = reader.readBuffer(length)
        reader.skip(paddingBytes)
        return buffer
    }
    return nil
}

private class Keychain: NSObject, MTKeychain {
    var dict: [String: Data] = [:]
    
    func setObject(_ object: Any!, forKey aKey: String!, group: String!) {
        let data = NSKeyedArchiver.archivedData(withRootObject: object)
        self.dict[group + ":" + aKey] = data
    }
    
    func object(forKey aKey: String!, group: String!) -> Any! {
        if let data = self.dict[group + ":" + aKey] {
            return NSKeyedUnarchiver.unarchiveObject(with: data as Data)
        }
        return nil
    }
    
    func removeObject(forKey aKey: String!, group: String!) {
        self.dict.removeValue(forKey: group + ":" + aKey)
    }
    
    func dropGroup(_ group: String!) {
    }
}

private final class ParsedFile: NSObject {
    let data: Data?
    
    init(data: Data?) {
        self.data = data
        
        super.init()
    }
}

func fetchImageWithAccount(proxyConnection: AccountProxyConnection?, account: StoredAccountInfo, resource: ImageResource, completion: @escaping (Data?) -> Void) -> () -> Void {
    MTLogSetEnabled(true)
    MTLogSetLoggingFunction({ str, args in
        //let string = NSString(format: str! as NSString, args!)
        print("MT: \(str!)")
    })
    
    let serialization = Serialization()
    
    var apiEnvironment = MTApiEnvironment()
    
    apiEnvironment.apiId = BuildConfig.shared().apiId
    apiEnvironment.langPack = "ios"
    apiEnvironment.layer = NSNumber(value: Int(serialization.currentLayer()))
    apiEnvironment.disableUpdates = true
    apiEnvironment = apiEnvironment.withUpdatedLangPackCode("en")
    
    if let proxy = proxyConnection {
        apiEnvironment = apiEnvironment.withUpdatedSocksProxySettings(MTSocksProxySettings(ip: proxy.host, port: UInt16(clamping: proxy.port), username: proxy.username, password: proxy.password, secret: proxy.secret))
    }
    
    let context = MTContext(serialization: serialization, apiEnvironment: apiEnvironment, isTestingEnvironment: account.isTestingEnvironment, useTempAuthKeys: false)!
    
    let seedAddressList: [Int: [String]]
    
    if account.isTestingEnvironment {
        seedAddressList = [
            1: ["149.154.175.10"],
            2: ["149.154.167.40"]
        ]
    } else {
        seedAddressList = [
            1: ["149.154.175.50", "2001:b28:f23d:f001::a"],
            2: ["149.154.167.50", "2001:67c:4e8:f002::a"],
            3: ["149.154.175.100", "2001:b28:f23d:f003::a"],
            4: ["149.154.167.91", "2001:67c:4e8:f004::a"],
            5: ["149.154.171.5", "2001:b28:f23f:f005::a"]
        ]
    }
    
    for (id, ips) in seedAddressList {
        context.setSeedAddressSetForDatacenterWithId(id, seedAddressSet: MTDatacenterAddressSet(addressList: ips.map { MTDatacenterAddress(ip: $0, port: 443, preferForMedia: false, restrictToTcp: false, cdn: false, preferForProxy: false, secret: nil) }))
    }
    
    let keychain = Keychain()
    context.keychain = keychain
    
    context.performBatchUpdates({
        for (id, info) in account.datacenters {
            if !info.addressList.isEmpty {
                var addressList: [MTDatacenterAddress] = []
                for address in info.addressList {
                    addressList.append(MTDatacenterAddress(ip: address.host, port: UInt16(clamping: address.port), preferForMedia: address.isMedia, restrictToTcp: false, cdn: false, preferForProxy: false, secret: address.secret))
                }
                context.updateAddressSetForDatacenter(withId: Int(id), addressSet: MTDatacenterAddressSet(addressList: addressList), forceUpdateSchemes: true)
            }
        }
    })
    
    for (id, info) in account.datacenters {
        context.updateAuthInfoForDatacenter(withId: Int(id), authInfo: MTDatacenterAuthInfo(authKey: info.masterKey.data, authKeyId: info.masterKey.id, saltSet: [], authKeyAttributes: [:], mainTempAuthKey: nil, mediaTempAuthKey: nil))
    }
    
    let mtProto = MTProto(context: context, datacenterId: resource.datacenterId, usageCalculationInfo: nil)!
    mtProto.useTempAuthKeys = context.useTempAuthKeys
    mtProto.checkForProxyConnectionIssues = false
    
    let requestService = MTRequestMessageService(context: context)!
    mtProto.add(requestService)
    
    let request = MTRequest()
    
    let buffer = Buffer()
    buffer.appendInt32(-475607115) //upload.getFile
    
    buffer.appendInt32(-539317279) //InputFileLocation.inputFileLocation
    buffer.appendInt64(resource.volumeId)
    buffer.appendInt32(resource.localId)
    buffer.appendInt64(resource.secret)
    
    serializeBytes(Buffer(data: resource.fileReference), buffer: buffer, boxed: false)
    
    buffer.appendInt32(0)
    buffer.appendInt32(32 * 1024)
    
    request.setPayload(buffer.makeData(), metadata: "getFile", responseParser: { response in
        let reader = BufferReader(Buffer(data: response))
        guard let signature = reader.readInt32() else {
            return ParsedFile(data: nil)
        }
        guard signature == 157948117 else {
            return ParsedFile(data: nil)
        }
        reader.skip(4) //type
        reader.skip(4) //mtime
        guard let bytes = parseBytes(reader) else {
            return ParsedFile(data: nil)
        }
        return ParsedFile(data: bytes.makeData())
    })
    
    request.dependsOnPasswordEntry = false
    request.shouldContinueExecutionWithErrorContext = { errorContext in
        guard let _ = errorContext else {
            return true
        }
        return true
    }
    
    request.completed = { (boxedResponse, timestamp, error) -> () in
        if let _ = error {
            completion(nil)
        } else {
            if let result = boxedResponse as? ParsedFile {
                completion(result.data)
            } else {
                completion(nil)
            }
        }
    }
    
    requestService.add(request)
    mtProto.resume()
    
    let internalId = request.internalId
    return {
        requestService.removeRequest(byInternalId: internalId)
        context.performBatchUpdates({})
        mtProto.stop()
    }
}
