<commit_before>// 
// Copyright 2021 New Vector Ltd
//
// 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.
//

import Foundation
import DSWaveformImage

enum VoiceMessageAttachmentCacheManagerError: Error {
    case invalidEventId
    case invalidAttachmentType
    case decryptionError(Error)
    case preparationError(Error)
    case conversionError(Error)
    case invalidNumberOfSamples
    case samplingError
}

/**
 Swift optimizes the callbacks to be the same instance. Wrap them so we can store them in an array.
 */
private class CompletionWrapper {
    let completion: (Result<(URL, [Float]), Error>) -> Void
    
    init(_ completion: @escaping (Result<(URL, [Float]), Error>) -> Void) {
        self.completion = completion
    }
}

class VoiceMessageAttachmentCacheManager {
    
    static let sharedManager = VoiceMessageAttachmentCacheManager()
    
//    private let workQueue: DispatchQueue
    
    private var completionCallbacks = [String: [CompletionWrapper]]()
    private var samples = [String: [Int: [Float]]]()
    private var finalURLs = [String: URL]()
    
//    private init() {
//        workQueue = DispatchQueue(label: "io.element.VoiceMessageAttachmentCacheManager.queue", qos: .userInitiated)
//    }
    
    func loadAttachment(_ attachment: MXKAttachment, numberOfSamples: Int, completion: @escaping (Result<(URL, [Float]), Error>) -> Void) {
        guard attachment.type == MXKAttachmentTypeVoiceMessage else {
            completion(Result.failure(VoiceMessageAttachmentCacheManagerError.invalidAttachmentType))
            return
        }
        
        guard let identifier = attachment.eventId else {
            completion(Result.failure(VoiceMessageAttachmentCacheManagerError.invalidEventId))
            return
        }
        
        guard numberOfSamples > 0 else {
            completion(Result.failure(VoiceMessageAttachmentCacheManagerError.invalidNumberOfSamples))
            return
        }
        
        if let finalURL = finalURLs[identifier], let samples = samples[identifier]?[numberOfSamples] {
            completion(Result.success((finalURL, samples)))
            return
        }
        
//        workQueue.async {
            self.enqueueLoadAttachment(attachment, identifier: identifier, numberOfSamples: numberOfSamples, completion: completion)
//        }
    }
    
    private func enqueueLoadAttachment(_ attachment: MXKAttachment, identifier: String, numberOfSamples: Int, completion: @escaping (Result<(URL, [Float]), Error>) -> Void) {
        if var callbacks = completionCallbacks[identifier] {
            callbacks.append(CompletionWrapper(completion))
            completionCallbacks[identifier] = callbacks
            return
        } else {
            completionCallbacks[identifier] = [CompletionWrapper(completion)]
        }
        
        func sampleFileAtURL(_ url: URL) {
            let analyser = WaveformAnalyzer(audioAssetURL: url)
            analyser?.samples(count: numberOfSamples, completionHandler: { samples in
                // Dispatch back from the WaveformAnalyzer's internal queue
                DispatchQueue.main.async {
//                self.workQueue.async {
                    guard let samples = samples else {
                        self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.samplingError)
                        return
                    }
                    
                    if var existingSamples = self.samples[identifier] {
                        existingSamples[numberOfSamples] = samples
                    } else {
                        self.samples[identifier] = [numberOfSamples: samples]
                    }
                    
                    self.invokeSuccessCallbacksForIdentifier(identifier, url: url, samples: samples)
                }
            })
        }
        
        if let finalURL = finalURLs[identifier] {
            sampleFileAtURL(finalURL)<commit_msg>#4090 - Use a dedicated dispatch queue for process<commit_after>// 
// Copyright 2021 New Vector Ltd
//
// 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.
//

import Foundation
import DSWaveformImage

enum VoiceMessageAttachmentCacheManagerError: Error {
    case invalidEventId
    case invalidAttachmentType
    case decryptionError(Error)
    case preparationError(Error)
    case conversionError(Error)
    case invalidNumberOfSamples
    case samplingError
}

/**
 Swift optimizes the callbacks to be the same instance. Wrap them so we can store them in an array.
 */
private class CompletionWrapper {
    let completion: (Result<(URL, [Float]), Error>) -> Void
    
    init(_ completion: @escaping (Result<(URL, [Float]), Error>) -> Void) {
        self.completion = completion
    }
}

class VoiceMessageAttachmentCacheManager {
    
    static let sharedManager = VoiceMessageAttachmentCacheManager()
    
    private let workQueue: DispatchQueue
    
    private var completionCallbacks = [String: [CompletionWrapper]]()
    private var samples = [String: [Int: [Float]]]()
    private var finalURLs = [String: URL]()
    
    private init() {
        workQueue = DispatchQueue(label: "io.element.VoiceMessageAttachmentCacheManager.queue", qos: .userInitiated)
    }
    
    func loadAttachment(_ attachment: MXKAttachment, numberOfSamples: Int, completion: @escaping (Result<(URL, [Float]), Error>) -> Void) {
        guard attachment.type == MXKAttachmentTypeVoiceMessage else {
            completion(Result.failure(VoiceMessageAttachmentCacheManagerError.invalidAttachmentType))
            return
        }
        
        guard let identifier = attachment.eventId else {
            completion(Result.failure(VoiceMessageAttachmentCacheManagerError.invalidEventId))
            return
        }
        
        guard numberOfSamples > 0 else {
            completion(Result.failure(VoiceMessageAttachmentCacheManagerError.invalidNumberOfSamples))
            return
        }
        
        if let finalURL = finalURLs[identifier], let samples = samples[identifier]?[numberOfSamples] {
            completion(Result.success((finalURL, samples)))
            return
        }
        
        workQueue.async {
            self.enqueueLoadAttachment(attachment, identifier: identifier, numberOfSamples: numberOfSamples, completion: completion)
        }
    }
    
    private func enqueueLoadAttachment(_ attachment: MXKAttachment, identifier: String, numberOfSamples: Int, completion: @escaping (Result<(URL, [Float]), Error>) -> Void) {
        if var callbacks = completionCallbacks[identifier] {
            callbacks.append(CompletionWrapper(completion))
            completionCallbacks[identifier] = callbacks
            return
        } else {
            completionCallbacks[identifier] = [CompletionWrapper(completion)]
        }
        
        func sampleFileAtURL(_ url: URL) {
            let analyser = WaveformAnalyzer(audioAssetURL: url)
            analyser?.samples(count: numberOfSamples, completionHandler: { samples in
                // Dispatch back from the WaveformAnalyzer's internal queue
                DispatchQueue.main.async {
                    guard let samples = samples else {
                        self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.samplingError)
                        return
                    }
                    
                    if var existingSamples = self.samples[identifier] {
                        existingSamples[numberOfSamples] = samples
                    } else {
                        self.samples[identifier] = [numberOfSamples: samples]
                    }
                    
                    self.invokeSuccessCallbacksForIdentifier(identifier, url: url, samples: samples)
                }
            })
        }
        
        if let finalURL = finalURLs[identifier] {
            sampleFileAtURL(finalURL)
{'fraction_non_alphanumeric': 0.0796746155560508, 'fraction_numerical': 0.0024515266324938714, 'mean_word_length': 2.430810397553517, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 21, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2406203', 'n_tokens_mistral': 2196, 'n_tokens_neox': 2111, 'n_words': 607}