import Foundation
import Speech
import AVFoundation
import AudioToolbox
import RxSwift
import os.log
import ExceptionCatcher

class Recorder {
    
    let engine = AVAudioEngine()
    var fileRef: ExtAudioFileRef? = nil
    let recognizer = SFSpeechRecognizer(locale: Locale(identifier: "ru_RU"))
    let request = SFSpeechAudioBufferRecognitionRequest()
    var recognitionTask: SFSpeechRecognitionTask?
    var endRecognitionTimer: Timer?
    var result: String = ""
    
    let recordingSettings: [String:Any] = [
        AVFormatIDKey:kAudioFormatMPEG4AAC_HE,
        AVSampleRateKey:44100.0,
        AVNumberOfChannelsKey:2,
        //AVEncoderBitRateKey:320*1024,
        //AVLinearPCMBitDepthKey:16,
        AVEncoderAudioQualityKey:AVAudioQuality.max.rawValue
    ]
    
    init() {
        self.request.contextualStrings = ["61", "161", "761", "123", "750", "777", "799"]
    }
    
    func microphoneAvailable() -> Bool {
        // FIXME:
        return true
    }
    
    func requestPermissions() -> Single<Void> {
        return Single<Void>.create { observer in
            AVAudioSession.sharedInstance().requestRecordPermission { allowed in
                if allowed {
                    SFSpeechRecognizer.requestAuthorization { status in
                        switch status {
                        case .authorized:
                            observer(.success(()))
                            break
                        case .denied:
                            let error = CocoaError.error("Access error", reason: "Access to speech recognition is denied", suggestion: "Please give permission to use speech recognition in system settings")
                            observer(.error(error))
                            break
                        case .restricted:
                            let error = CocoaError.error("Access error", reason: "Speech recognition is restricted on this device")
                            observer(.error(error))
                            break
                        case .notDetermined:
                            let error = CocoaError.error("Access error", reason: "Speech recognition status is not yet determined")
                            observer(.error(error))
                            break
                        @unknown default:
                            let error = CocoaError.error("Access error", reason: "Unknown error accessing speech recognizer")
                            observer(.error(error))
                            break
                        }
                    }
                } else {
                    let error = CocoaError.error("Access error", reason: "Access to microphone is denied", suggestion: "Please give permission to use microphone in system settings")
                    observer(.error(error))
                }
            }
            
            return Disposables.create()
        }
    }
    
    func startRecording(to file: URL) -> Single<String> {
        guard self.microphoneAvailable() else {
            return Single.error(CocoaError.error("Recording error", reason: "Microphone not found"))
        }
        
        return Single<String>.create { observer in
            guard let aac = AVAudioFormat(settings: self.recordingSettings) else {
                observer(.error(CocoaError.error("Recording error", reason: "Format not supported")))
                return Disposables.create()
            }
            
            ExtAudioFileCreateWithURL(file as CFURL, kAudioFileM4AType, aac.streamDescription, nil, AudioFileFlags.eraseFile.rawValue, &self.fileRef)
            guard let fileRef = self.fileRef else {
                observer(.error(CocoaError.error(CocoaError.Code.fileWriteUnknown)))
                return Disposables.create()
            }
            
            do {
                try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default, options: [])
                try AVAudioSession.sharedInstance().setActive(true)
                
                let inFormat = self.engine.inputNode.outputFormat(forBus: 0)
                ExtAudioFileSetProperty(fileRef, kExtAudioFileProperty_ClientDataFormat, UInt32(MemoryLayout<AudioStreamBasicDescription>.size), inFormat.streamDescription)
                
                try ExceptionCatcher.catch {
                    self.engine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: inFormat) { buffer, time in
                        self.request.append(buffer)
                        ExtAudioFileWrite(fileRef, buffer.frameLength, buffer.audioBufferList)
                    }
                }
                
                self.recognitionTask = self.recognizer!.recognitionTask(with: self.request) { result, error in
                    if let transcription = result?.bestTranscription {
                        self.result = transcription.formattedString
                        self.endRecognitionTimer?.invalidate()
                        self.endRecognitionTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { timer in
                            self.finishRecording()
                            observer(.success(self.result))
                        }
                    }
                }
                
                self.endRecognitionTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { timer in
                    self.finishRecording()
                    observer(.success(self.result))
                }
                
                self.engine.prepare()
                try self.engine.start()
            } catch {
                observer(.error(error))
            }
            
            return Disposables.create {
                self.cancelRecording()
            }
        }
    }
    
    func cancelRecording() {
        self.finishRecording()
        self.endRecognitionTimer?.invalidate()
        self.endRecognitionTimer = nil
    }
    
    func finishRecording() {
        guard self.engine.isRunning else { return }
        
        self.engine.stop()
        self.engine.inputNode.removeTap(onBus: 0)
        self.request.endAudio()
        self.recognitionTask?.cancel()
        try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
        try? AVAudioSession.sharedInstance().setCategory(.soloAmbient)
        
        if let fileRef = self.fileRef {
            ExtAudioFileDispose(fileRef)
        }
    }
    
    func stopRecording() {
        self.finishRecording()
        self.endRecognitionTimer?.fire()
        self.endRecognitionTimer = nil
    }
}
