<commit_before>//
//  SpotijackSession.swift
//  LibSpotijack
//
//  Created by <NAME> on 2017-08-02.
//  Copyright © 2017 <NAME>. All rights reserved.
//

import Cocoa
import ScriptingBridge
import Result

public final class SpotijackSession {
    //MARK: Properties - Application Bridges
    // Access level is internal for testing purposes.
    internal var spotifyBridge: SpotifyApplication
    internal var spotifyApplication: NSRunningApplication

    internal var audioHijackBridge: AudioHijackApplication
    internal var audioHijackApplication: NSRunningApplication

    /// A scripting bridge interface to the Spotijack session in Audio Hijack Pro.
    /// Accessing this property will make Audio Hijack Pro start hijacking Spotify.
    internal var spotijackSessionBridge: Result<AudioHijackSession> {
        return audioHijackBridge.flatMap { ah in
            let sessions = ah.sessions!() as! [AudioHijackSession] // Should never fail

            if let session = sessions.first(where: { $0.name == "Spotijack" }) {
                session.startHijackingRelaunch!(.yes)
                return .ok(session)
            } else {
                return .fail(SpotijackSessionError.spotijackSessionNotFound)
            }
        }
    }

    //MARK: Properties - State
    // Internal mute state used to track changes. Does not affect the actual mute
    // state in AHP.
    private var _isMuted: Bool = false {
        didSet {
            if _isMuted != oldValue {
                notiCenter.post(MuteStateDidChange(sender: self, newMuteState: _isMuted))
            }
        }
    }

    /// Queries Audio Hijack Pro to determine if the Spotijack session is muted.
    /// Returns false and posts a `DidEncounterError` notification if Audio
    /// Hijack Pro can not be queried.
    public var isMuted: Bool {
        // Setting this property updates the internal mute state and AHP.
        // Accessing it does not change the internal mute state.
        get {
            switch spotijackSessionBridge.map({ $0.speakerMuted! }) {
            case .ok(let status):
                return status
            case .fail(let error):
                notiCenter.post(DidEncounterError(sender: self, error: error))
                return false
            }
        }

        set {
            let result = spotijackSessionBridge.map { session in
                session.setSpeakerMuted!(newValue)
            }

            switch result {
            case .ok:
                _isMuted = newValue
            case .fail(let error):
                notiCenter.post(DidEncounterError(sender: self, error: error))
            }
        }
    }

    private var _isRecording = false {
        didSet {
            if _isRecording != oldValue {
                notiCenter.post(RecordingStateDidChange(sender: self, isRecording: _isRecording))
            }
        }
    }
    /// Queries AHP to determine if the Spotijack session is recording. Returns
    /// false and posts a `DidEncounterError` message if AHP can not be queried.
    public var isRecording: Bool {
        get {
            switch spotijackSessionBridge.map({ $0.recording! }) {
            case .ok(let status):
                return status
            case .fail(let error):
                notiCenter.post(DidEncounterError(sender: self, error: error))
                return false
            }
        }

        set {
            let result = spotijackSessionBridge.map { session in
                newValue ? session.startRecording!() : session.stopRecording!()
            }

            switch result {
            case .ok:
                _isRecording = newValue
            case .fail(let error):
                notiCenter.post(DidEncounterError(sender: self, error: error))
            }
        }
    }

    private var _currentTrack: StaticSpotifyTrack? = nil {
        didSet {
            if _currentTrack != oldValue {
                notiCenter.post(TrackDidChange(sender: self, newTrack: _currentTrack))
            }

            // Start a new recording if Spotijack is controlling the current
            // recording session.
            if _currentTrack != oldValue,
                isSpotijacking == true
            {
                startNewRecording()
            }
        }
    }

    /// Returns the currently playing track in Spotify. Can return `nil` if no
    /// track is playing or if Spotify can not be accessed. For the latter, a
    /// `DidEncounterError` notification is also posted.
    public var currentTrack: StaticSpotifyTrack? {
        let track = spotifyBridge.map { (spotify) -> StaticSpotifyTrack? in
            if let currentTrack = spotify.currentTrack {
                return StaticSpotifyTrack(from: currentTrack)
            } else {
                return nil
            }
        }

        switch track {
        case .ok(let value):
            return value
        case .fail(let error):
            notiCenter.post(DidEncounterError(sender: self, error: error))
            return nil
        }
    }

    private var _applicationPollingTimer: Timer? = nil
    /// Returns `true` if SpotijackSessionManager is polling Spotify and AHP.
    public var isPolling: Bool { return _applicationPollingTimer?.isValid ?? false }

    /// Returns `true` if SpotijackSessionManager is actively controlling recording.
    public private(set) var isSpotijacking: Bool = false

    //MARK: Lifecycle
    internal init(spotifyApplication: NSRunningApplication, spotifyBridge: SpotifyApplication, audioHijackApplication: NSRunningApplication, audioHijackBridge: AudioHijackApplication) {
        self.spotifyApplication = spotifyApplication
        self.spotifyBridge = spotifyBridge
        self.audioHijackApplication = audioHijackApplication
        self.audioHijackBridge = audioHijackBridge
    }

    deinit {
        // Trigger KVO observer removal for NSRunningApplication instances
        spotifyApplication = nil
        audioHijackApplication = nil
    }

    //MARK: KVO Methods
    override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        guard context == &self.context,
            let keyPath = keyPath,
            let object = object,
            let change = change
            else {
                return
        }

        // Handle termination of an application
        if keyPath == #keyPath(NSRunningApplication.isTerminated),<commit_msg>Rewrite init for SpotijackSession to use tuple<commit_after>//
//  SpotijackSession.swift
//  LibSpotijack
//
//  Created by <NAME> on 2017-08-02.
//  Copyright © 2017 <NAME>. All rights reserved.
//

import Cocoa
import ScriptingBridge
import Result

typealias SBRunningApplicationPair<T: SBApplicationProtocol> = (app: NSRunningApplication, bridge: T)

public final class SpotijackSession {
    //MARK: Properties - Application Bridges
    // Access level is internal for testing purposes.
    internal var spotifyBridge: SpotifyApplication
    internal var spotifyApplication: NSRunningApplication

    internal var audioHijackBridge: AudioHijackApplication
    internal var audioHijackApplication: NSRunningApplication

    /// A scripting bridge interface to the Spotijack session in Audio Hijack Pro.
    /// Accessing this property will make Audio Hijack Pro start hijacking Spotify.
    internal var spotijackSessionBridge: Result<AudioHijackSession> {
        return audioHijackBridge.flatMap { ah in
            let sessions = ah.sessions!() as! [AudioHijackSession] // Should never fail

            if let session = sessions.first(where: { $0.name == "Spotijack" }) {
                session.startHijackingRelaunch!(.yes)
                return .ok(session)
            } else {
                return .fail(SpotijackSessionError.spotijackSessionNotFound)
            }
        }
    }

    //MARK: Properties - State
    // Internal mute state used to track changes. Does not affect the actual mute
    // state in AHP.
    private var _isMuted: Bool = false {
        didSet {
            if _isMuted != oldValue {
                notiCenter.post(MuteStateDidChange(sender: self, newMuteState: _isMuted))
            }
        }
    }

    /// Queries Audio Hijack Pro to determine if the Spotijack session is muted.
    /// Returns false and posts a `DidEncounterError` notification if Audio
    /// Hijack Pro can not be queried.
    public var isMuted: Bool {
        // Setting this property updates the internal mute state and AHP.
        // Accessing it does not change the internal mute state.
        get {
            switch spotijackSessionBridge.map({ $0.speakerMuted! }) {
            case .ok(let status):
                return status
            case .fail(let error):
                notiCenter.post(DidEncounterError(sender: self, error: error))
                return false
            }
        }

        set {
            let result = spotijackSessionBridge.map { session in
                session.setSpeakerMuted!(newValue)
            }

            switch result {
            case .ok:
                _isMuted = newValue
            case .fail(let error):
                notiCenter.post(DidEncounterError(sender: self, error: error))
            }
        }
    }

    private var _isRecording = false {
        didSet {
            if _isRecording != oldValue {
                notiCenter.post(RecordingStateDidChange(sender: self, isRecording: _isRecording))
            }
        }
    }
    /// Queries AHP to determine if the Spotijack session is recording. Returns
    /// false and posts a `DidEncounterError` message if AHP can not be queried.
    public var isRecording: Bool {
        get {
            switch spotijackSessionBridge.map({ $0.recording! }) {
            case .ok(let status):
                return status
            case .fail(let error):
                notiCenter.post(DidEncounterError(sender: self, error: error))
                return false
            }
        }

        set {
            let result = spotijackSessionBridge.map { session in
                newValue ? session.startRecording!() : session.stopRecording!()
            }

            switch result {
            case .ok:
                _isRecording = newValue
            case .fail(let error):
                notiCenter.post(DidEncounterError(sender: self, error: error))
            }
        }
    }

    private var _currentTrack: StaticSpotifyTrack? = nil {
        didSet {
            if _currentTrack != oldValue {
                notiCenter.post(TrackDidChange(sender: self, newTrack: _currentTrack))
            }

            // Start a new recording if Spotijack is controlling the current
            // recording session.
            if _currentTrack != oldValue,
                isSpotijacking == true
            {
                startNewRecording()
            }
        }
    }

    /// Returns the currently playing track in Spotify. Can return `nil` if no
    /// track is playing or if Spotify can not be accessed. For the latter, a
    /// `DidEncounterError` notification is also posted.
    public var currentTrack: StaticSpotifyTrack? {
        let track = spotifyBridge.map { (spotify) -> StaticSpotifyTrack? in
            if let currentTrack = spotify.currentTrack {
                return StaticSpotifyTrack(from: currentTrack)
            } else {
                return nil
            }
        }

        switch track {
        case .ok(let value):
            return value
        case .fail(let error):
            notiCenter.post(DidEncounterError(sender: self, error: error))
            return nil
        }
    }

    private var _applicationPollingTimer: Timer? = nil
    /// Returns `true` if SpotijackSessionManager is polling Spotify and AHP.
    public var isPolling: Bool { return _applicationPollingTimer?.isValid ?? false }

    /// Returns `true` if SpotijackSessionManager is actively controlling recording.
    public private(set) var isSpotijacking: Bool = false

    //MARK: Lifecycle
    internal init(spotify: SBRunningApplicationPair<SpotifyApplication>, audioHijack: SBRunningApplicationPair<AudioHijackApplication>) {
        self.spotifyApplication = spotify.app
        self.spotifyBridge = spotify.bridge
        self.audioHijackApplication = audioHijack.app
        self.audioHijackBridge = audioHijack.bridge
    }

    deinit {
        // Trigger KVO observer removal for NSRunningApplication instances
        spotifyApplication = nil
        audioHijackApplication = nil
    }

    //MARK: KVO Methods
    override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        guard context == &self.context,
            let keyPath = keyPath,
            let object = object,
            let change = change
            else {
                return
        }

        // Handle termination of an application
        if keyPath == #keyPath(NSRunningApplication.isTerminated),
{'fraction_non_alphanumeric': 0.07373166259168704, 'fraction_numerical': 0.0022921760391198046, 'mean_word_length': 1.980191256830601, 'pattern_counts': {'":': 0, '<': 12, '<?xml version=': 0, '>': 14, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, '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': '6419849', 'n_tokens_mistral': 3500, 'n_tokens_neox': 3309, 'n_words': 1018}