<commit_before>//
//  PreviewChunkCache.swift
//  MacClient
//
//  Created by <NAME> on 10/13/19.
//  Copyright © 2019 Rc2. All rights reserved.
//

import Foundation
import ReactiveSwift
import ClientCore
import Networking
import MJLLogger
import GRDB

// siftlint:disable cyclomatic_complexity

public class PreviewChunkCache {
	public enum Error: String, RawRepresentable, Swift.Error {
		case codeError
	}
	private struct ChunkInfo: Hashable, Codable {
		let chunkNumber: Int
		let type: RootChunkType
		var currentHtml: String = ""
		var inlineHtml: [String] = []
		var output: String = ""
		var outputIsValid: Bool = false
	}

	private struct SavedCacheEntry: Codable, FetchableRecord, TableRecord, PersistableRecord {
		static var databaseTableName: String = "cacheEntry"
		let fileId: Int
		let fileVersion: Int
		let chunks: [ChunkInfo]
	}
		
	let previewId: Int
	let fileId: Int
	let workspace: AppWorkspace
	private var dbQueue: DatabaseQueue?
	private var chunkInfo: [ChunkInfo] = []
	public var contentCached: Bool { return chunkInfo.count > 0 }
	public let _document: Property<RmdDocument?>
	private var document: RmdDocument {
		guard let doc = _document.value else { fatalError("previewChunk asked to use nil document") }
		return doc
	}

	public init(previewId: Int, fileId: Int, workspace: AppWorkspace, documentProperty: Property<RmdDocument?>) {
		self.previewId = previewId
		self.fileId = fileId
		self.workspace = workspace
		self._document = Property<RmdDocument?>(documentProperty)
		NotificationCenter.default.addObserver(self, selector: #selector(documentUpdated), name: .rmdDocumentUpdated, object: self.document)
		NotificationCenter.default.addObserver(self, selector: #selector(appTerminating(note:)), name: NSApplication.willTerminateNotification, object: nil)
		readCache()
	}
	
	deinit {
		saveCache()
	}
	
	/// notification handler for app termination. Saves database before exiting
	@objc func appTerminating(note: Notification?) {
		saveCache()
		dbQueue = nil
	}
	
	/// called when the document's content was updated
	@objc func documentUpdated(note: Notification) {
		guard var changedIndexes = note.userInfo?[RmdDocument.changedIndexesKey] as? [Int] else {
			Log.warn("received documentUpdated notification without changedIndexes", .app)
			return
		}
		cacheCode(changedChunks: &changedIndexes)
	}
	
	/// if specified chunk is a code chunk, stores the code output for that chunk. recaches the code so no need to do that affter this call
	public func updateCodeOutput(chunkNumber: Int, outputContent: String) {
		guard let chunk = chunkInfo[safe: chunkNumber], chunk.type == .code else {
			Log.warn("invali chunk index", .app)
			return
		}
		Log.info("updating output for chunk \(chunkNumber)")
		chunkInfo[chunkNumber].output = outputContent
		chunkInfo[chunkNumber].outputIsValid = true
		// recache output
		var changed = [chunkNumber]
		cacheCode(changedChunks: &changed)
	}
	
	/// Clears the code chache, should be called when the document has changed
	public func clearCache() {
		chunkInfo.removeAll()
	}

	/// caches the html for all code chunks and any markdown chunks that contain inline code
	/// - Parameter document: the document to cache
	public func cacheAllCode() {
		var changedChunkIndexes = [Int](0..<document.chunks.count)
		cacheCode(changedChunks: &changedChunkIndexes)
	}

	/// caches the html for all code chunks and any markdown chunks that contain inline code
	// TODO: this needs to return a SignalHandler so the code can be executed async
	/// - Parameter changedChunks: array of chunkNumbers that caller thinks changed.
	///  Any chunk where the output did not change is removed. Ones not included where the output did change are added.
	/// - Parameter document: the document to cache
	// swiftlint:disable:next cyclomatic_complexity
	public func cacheCode(changedChunks: inout [Int]) {
		// if not the same size as last time, then all code is dirty
		if document.chunks.count != chunkInfo.count {
			chunkInfo = [ChunkInfo]()
			for i in 0..<document.chunks.count {
				chunkInfo.append(ChunkInfo(chunkNumber: i, type: RootChunkType(document.chunks[i].chunkType)))
			}
		}
		// generate html for each chunk.
		for (chunkNumber, chunk) in document.chunks.enumerated() {
			if chunk.chunkType == .markdown, chunk.children.count > 0 {
				// need to generate inline html
				var inline = [String]()
				for inlineChunk in chunk.children {
					inline.append(inlineHtmlFor(chunk: inlineChunk, parent: chunk))
				}
				guard chunkInfo[chunkNumber].inlineHtml != inline else { continue }
				chunkInfo[chunkNumber].inlineHtml = inline
				if !changedChunks.contains(chunkNumber) {
					changedChunks.append(chunkNumber)
				}
				continue
			}
			guard chunk.chunkType == .code else {
				// TODO: need to track inline code chunks, or markdown chunks with inline code
				chunkInfo[chunkNumber].outputIsValid = true
				continue
			}
			let currentHtml = htmlForChunk(chunkNumber: chunkNumber)
			if chunkInfo[chunkNumber].currentHtml != currentHtml {
				if let changedIndex = changedChunks.firstIndex(where: { $0 == chunkNumber }) {
					changedChunks.remove(at: changedIndex)
				}
				if !changedChunks.contains(chunkNumber) {
					changedChunks.append(chunkNumber)
				}
			}
		}
		changedChunks.sort()
	}
	
	/// Called to notify a chunk will be execute. Invalidates the output of all code chunks that foillow
	/// - Parameter chunkIndex: the chunkIndex
	public func willExecuteChunk(chunkIndex: Int) {
		precondition(chunkInfo.indices.contains(chunkIndex))
		// invalidate all future code chunks
		for idx in chunkIndex...chunkInfo.count where chunkInfo[idx].type == .code {
			chunkInfo[idx].outputIsValid = false
		}

	}
	<commit_msg>added func to get list of invalidated chunks to allow toolbar updates<commit_after>//
//  PreviewChunkCache.swift
//  MacClient
//
//  Created by <NAME> on 10/13/19.
//  Copyright © 2019 Rc2. All rights reserved.
//

import Foundation
import ReactiveSwift
import ClientCore
import Networking
import MJLLogger
import GRDB

// siftlint:disable cyclomatic_complexity

public class PreviewChunkCache {
	public enum Error: String, RawRepresentable, Swift.Error {
		case codeError
	}
	private struct ChunkInfo: Hashable, Codable {
		let chunkNumber: Int
		let type: RootChunkType
		var currentHtml: String = ""
		var inlineHtml: [String] = []
		var output: String = ""
		var outputIsValid: Bool = false
	}

	private struct SavedCacheEntry: Codable, FetchableRecord, TableRecord, PersistableRecord {
		static var databaseTableName: String = "cacheEntry"
		let fileId: Int
		let fileVersion: Int
		let chunks: [ChunkInfo]
	}
		
	let previewId: Int
	let fileId: Int
	let workspace: AppWorkspace
	private var dbQueue: DatabaseQueue?
	private var chunkInfo: [ChunkInfo] = []
	public var contentCached: Bool { return chunkInfo.count > 0 }
	public let _document: Property<RmdDocument?>
	private var document: RmdDocument {
		guard let doc = _document.value else { fatalError("previewChunk asked to use nil document") }
		return doc
	}

	public init(previewId: Int, fileId: Int, workspace: AppWorkspace, documentProperty: Property<RmdDocument?>) {
		self.previewId = previewId
		self.fileId = fileId
		self.workspace = workspace
		self._document = Property<RmdDocument?>(documentProperty)
		NotificationCenter.default.addObserver(self, selector: #selector(documentUpdated), name: .rmdDocumentUpdated, object: self.document)
		NotificationCenter.default.addObserver(self, selector: #selector(appTerminating(note:)), name: NSApplication.willTerminateNotification, object: nil)
		readCache()
	}
	
	deinit {
		saveCache()
	}
	
	/// notification handler for app termination. Saves database before exiting
	@objc func appTerminating(note: Notification?) {
		saveCache()
		dbQueue = nil
	}
	
	/// called when the document's content was updated
	@objc func documentUpdated(note: Notification) {
		guard var changedIndexes = note.userInfo?[RmdDocument.changedIndexesKey] as? [Int] else {
			Log.warn("received documentUpdated notification without changedIndexes", .app)
			return
		}
		cacheCode(changedChunks: &changedIndexes)
	}
	
	/// if specified chunk is a code chunk, stores the code output for that chunk. recaches the code so no need to do that affter this call
	public func updateCodeOutput(chunkNumber: Int, outputContent: String) {
		guard let chunk = chunkInfo[safe: chunkNumber], chunk.type == .code else {
			Log.warn("invali chunk index", .app)
			return
		}
		Log.info("updating output for chunk \(chunkNumber)")
		chunkInfo[chunkNumber].output = outputContent
		chunkInfo[chunkNumber].outputIsValid = true
		// recache output
		var changed = [chunkNumber]
		cacheCode(changedChunks: &changed)
	}
	
	/// Clears the code chache, should be called when the document has changed
	public func clearCache() {
		chunkInfo.removeAll()
	}

	/// caches the html for all code chunks and any markdown chunks that contain inline code
	/// - Parameter document: the document to cache
	public func cacheAllCode() {
		var changedChunkIndexes = [Int](0..<document.chunks.count)
		cacheCode(changedChunks: &changedChunkIndexes)
	}

	/// caches the html for all code chunks and any markdown chunks that contain inline code
	// TODO: this needs to return a SignalHandler so the code can be executed async
	/// - Parameter changedChunks: array of chunkNumbers that caller thinks changed.
	///  Any chunk where the output did not change is removed. Ones not included where the output did change are added.
	/// - Parameter document: the document to cache
	// swiftlint:disable:next cyclomatic_complexity
	public func cacheCode(changedChunks: inout [Int]) {
		// if not the same size as last time, then all code is dirty
		if document.chunks.count != chunkInfo.count {
			chunkInfo = [ChunkInfo]()
			for i in 0..<document.chunks.count {
				chunkInfo.append(ChunkInfo(chunkNumber: i, type: RootChunkType(document.chunks[i].chunkType)))
			}
		}
		// generate html for each chunk.
		for (chunkNumber, chunk) in document.chunks.enumerated() {
			if chunk.chunkType == .markdown, chunk.children.count > 0 {
				// need to generate inline html
				var inline = [String]()
				for inlineChunk in chunk.children {
					inline.append(inlineHtmlFor(chunk: inlineChunk, parent: chunk))
				}
				guard chunkInfo[chunkNumber].inlineHtml != inline else { continue }
				chunkInfo[chunkNumber].inlineHtml = inline
				if !changedChunks.contains(chunkNumber) {
					changedChunks.append(chunkNumber)
				}
				continue
			}
			guard chunk.chunkType == .code else {
				// TODO: need to track inline code chunks, or markdown chunks with inline code
				chunkInfo[chunkNumber].outputIsValid = true
				continue
			}
			let currentHtml = htmlForChunk(chunkNumber: chunkNumber)
			if chunkInfo[chunkNumber].currentHtml != currentHtml {
				if let changedIndex = changedChunks.firstIndex(where: { $0 == chunkNumber }) {
					changedChunks.remove(at: changedIndex)
				}
				if !changedChunks.contains(chunkNumber) {
					changedChunks.append(chunkNumber)
				}
			}
		}
		changedChunks.sort()
	}
	
	/// calls supplied closure for every chunk whose output is invalid
	/// - Parameter body: closure to call
	public func forInvalidChunks(body: (Int) -> Void) {
		chunkInfo.filter({ !$0.outputIsValid }).forEach({ body($0.chunkNumber) })
	}
	
	/// Figures out the state for every chunk
	/// - Returns: Tuple of  ( [chunkNumber], [Bool]) where second array is true if it should be enabled
	public func chunksValidity() -> ([Int], [Bool]) {
		var indexes = [Int]()
		var enable = [Bool]()
			chunkInfo.forEach {
				indexes.append($0.chunkNumber)
				enable.append(!$0.outputIsValid)
		}
		return (indexes, enable)
	}
	
	/// Called to notify a chunk will be execute. Invalidates the output of all code chunks that foillow
	/// - Parameter chunkIndex: the chunkIndex
	public func willExecuteChunk(chunkIndex: Int) {
		precondition(chunkInfo.indices.contains(chunkIndex))
		// invalidate all future code chunks
		for idx in chunkIndex..<chunkInfo.count where chunkInfo[idx].type == .code {
			chunkInfo[idx].outputIsValid = false
		}

	}
	
{'fraction_non_alphanumeric': 0.09449404761904762, 'fraction_numerical': 0.002976190476190476, 'mean_word_length': 4.046725073007926, 'pattern_counts': {'":': 0, '<': 16, '<?xml version=': 0, '>': 17, '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': '2643814', 'n_tokens_mistral': 3874, 'n_tokens_neox': 3464, 'n_words': 1136}