//
//  LKFContext.swift
//  lkfc
//
//  Created by Alexander Holzer on 06.06.18.
//  Copyright © 2018 Alexander Holzer. All rights reserved.
//

import Foundation
@_implementationOnly import LibLKF

public enum LKFOutputFormat {
    case utf8
    case ascii
    case standard
}

public struct LKFContextTraceOptions {
    var mbdsLevel : Int32 = 2
    var kaskoreLevel : Int32 = 2
    var intscorLevel : Int32 = 2
    var aufnr : String?
}

public enum LKFContextValue : UInt32 {
    case plausCount = 10
    case plausWorkCount = 11
    case scorCount = 20
    case scorWorkCount = 21
    case executionCalled = 99
}

public protocol LKFContextExecution {
    func callback (name:String, count: UInt32) -> Int
}

public class LKFContext {
    
    var ptr : OpaquePointer?
    var _flags : Int32 = 0
    var _model: UInt32 = 0
    var _period: String? = nil
    var _fileVersion: String?
    
    var allowCommands = true
    
    var _outputFormat = LKFOutputFormat.standard
    
    public var lastError : LKFError?
    
    public var createOutputForTest = false
    
    lazy public var metaData : LKFMetaData = LKFMetaData(context: self)
    
    var compareCache: LKFCompareCache?
    
    var packageOutputFiles : [LKFOutputFilePackaged]?
    
    var _stammProvider : LKFContextStammProvider?
    
    var _aufenthalte = [UInt32:LKFAufenthalt]()
    
    var intAbtCount : UInt32 = 0
    
    var logger : LKFLog?
    
    public var isBM : Bool {
        get {
            return (_flags & 1) != 0
        }
        set {
            if newValue {
                _flags |= 1
            }
            else {
                _flags &= ~1
            }
        }
    }
    
    var traceLogger : ((String) -> ())? = nil
    var executionLogger : ((String,UInt32) -> Int32)? = nil
    var executionCallback : LKFContextExecution?
    
    public var metaDataContext : LKFMetaDataFilesContext?
    
    public var computed = false
    
    private var _recordTypes : LKFRecordTypes?
    private var _errorDefList : LKFErrorDefList?
    private var _recordTypeData: LKFContextDataRecordTypes?
    
    var changeGroup: LKFContextChangeGroup?
    
    public var isValid : Bool {
        return ptr != nil
    }
    
    public var recordTypes : LKFRecordTypes? {
        if let _recordTypes {
            return _recordTypes
        }
        if ptr != nil {
            let rt = LKFRecordTypes(lkfContext: self)
            self._recordTypes = rt
            return rt
        }
        return nil
    }
    
    public func recordType (for type: String) -> LKFRecordType? {
        return recordTypes?.recordTypes[type]
    }
    
    public var errorDefList : LKFErrorDefList? {
        if let _errorDefList {
            return _errorDefList
        }
        if ptr != nil {
            let el = LKFErrorDefList(lkfContext: self)
            self._errorDefList = el
            return el
        }
        return nil
    }
    
    public func getRecordTypeData (lkfAufenthalt: LKFAufenthalt? = nil, all: Bool = false, create: Bool = false) -> LKFContextDataRecordTypes {
        
        if let lkfAufenthalt {
            return LKFContextDataRecordTypes(lkfContext: self, lkfAufenthalt: lkfAufenthalt, all: all)
        }
        
        if !create, let _recordTypeData {
            return _recordTypeData
        }
        _recordTypeData = LKFContextDataRecordTypes(lkfContext: self, lkfAufenthalt: nil, all: all)
        return _recordTypeData!
    }

    public convenience init (source: LKFContext, aufenthalte:[LKFAufenthalt]? = nil, copyAddLinesFlags: Int32 = 0, logger: LKFLog? = nil) throws {
        
        try self.init(metaDataContext: source.metaDataContext, stammProvider: source._stammProvider)
        
        self.flags = source.flags
        self.logger = logger
        
        _ = self.setContextParameters(period: source.period)
        
        /*
        if copyAddLinesFlags > 0 {
            self.addLines(source.contents (addLinesFlags: copyAddLinesFlags, aufenthalte: false))
        }
        */
        
        if let sourcePtr = source.ptr, let targetPtr = ptr, copyAddLinesFlags & 1 > 0 {
            LibLKF.lkf_copy_addlines(targetPtr, sourcePtr, 1)
        }
        
        for a in aufenthalte ?? source.aufenthalte() {
            try self.insertFrom(aufenthalt: a)
        }
        
        if let sourcePtr = source.ptr, let targetPtr = ptr, copyAddLinesFlags > 1 {
            LibLKF.lkf_copy_addlines(targetPtr, sourcePtr, copyAddLinesFlags & ~1)
        }
    }
    
    public init(model: UInt32, path: String? = nil, stammProvider:LKFContextStammProvider? = nil, logger: LKFLog? = nil) throws {
        _ = LKFGlobal.current
        _model = model
        self.logger = logger
        _stammProvider = stammProvider;
        if let path, let url = URL(string: path) {
            metaDataContext = LKFMetaDataFilesContext(model: model, name: "", baseUrl: url)
        }
        if (_model > 0) {
            try initialize(model: model, path: path)
        }
    }
    
    public init (metaDataContext: LKFMetaDataFilesContext?, stammProvider:LKFContextStammProvider? = nil, logger: LKFLog? = nil) throws {
        _ = LKFGlobal.current
        
        self.logger = logger
        
        guard let metaDataContext else {
            self.metaDataContext = nil
            _model = 0
            _stammProvider = stammProvider
            return
        }
        
        self.metaDataContext = metaDataContext
        _model = metaDataContext.model
        _stammProvider = stammProvider

        try initialize(metaDataContext: metaDataContext)
    }
    
    public init (error: Error) {
        if let lkfError = error as? LKFError {
            self.lastError = lkfError
        }
        else {
            self.lastError = LKFError(error: error)
        }
    }
    
    public func changeModel (metaDataContext: LKFMetaDataFilesContext, stammProvider:LKFContextStammProvider? = nil) throws {
        
        if !metaDataContext.equal(to: self.metaDataContext) {
            if let metaDataPtr =  metaDataContext.stammPtr {
                self.metaDataContext = metaDataContext
                self._model = metaDataContext.model
                LibLKF.lkf_set_modelljahr_stamm(ptr!, metaDataPtr)
                computed = false
                if let stammProvider {
                    _stammProvider = stammProvider
                }
                return
            }
            throw LKFError("path not set")
        }
    }
        
    public init() {
        _ = LKFGlobal.current
        metaDataContext = nil
    }
    
    deinit {
        if ptr != nil {
            if let logger {
                logger.debug ("LKFContext deinit - lkf_free_context")
            }
            lkf_free_context(ptr!)
            ptr = nil
        }
    }
    
    public func initialize(metaDataContext: LKFMetaDataFilesContext) throws {
            
        let stammPtr = try metaDataContext.load()
        let errorString = UnsafeMutablePointer<CChar>.allocate(capacity:1024)
        
        ptr = LibLKF.lkf_alloc_context_stamm (stammPtr,_stammProvider?.fileCallback, _stammProvider?.callback, errorString)
        
        let text = String(cString: errorString)
        errorString.deallocate()

        if !text.isEmpty {
            logger?.error ("cannot initialize metadata", details: text)
            throw LKFError(message:text)
        }
        
        logger?.debug( "LKFContext.initialize with \(metaDataContext.label)")
        
        LibLKF.lkf_set_contextflags(ptr, flags)
        if let period = _period {
            LibLKF.lkf_set_contextparametersFromString (ptr!, (period as NSString).utf8String, flags)
        }
    }
    
    public func initialize(model: UInt32, path: String?) throws {
        
        guard let path else {
            throw LKFError("no path - cannot initialize LKFContext")
        }
        
        let errorString = UnsafeMutablePointer<CChar>.allocate(capacity:1024)
        let unsafePath = (path as NSString).utf8String
        
        if let stammProvider = _stammProvider {
            ptr = LibLKF.lkf_alloc_context3(model, unsafePath, stammProvider.fileCallback, stammProvider.callback, errorString)
        }
        else {
            ptr = LibLKF.lkf_alloc_context(model, unsafePath, errorString)
        }

        let text = String(cString: errorString)
        errorString.deallocate()

        if ptr == nil {
            if text.contains("OutOfMemory") {
                throw LKFOutOfMemoryError(text)
            }
            if !text.isEmpty {
                throw LKFError(text)
            }
            throw LKFError("cannot alloc context")
        }
        else if !text.isEmpty {
            throw LKFError(message:text)
        }
        
        LibLKF.lkf_set_contextflags(ptr, flags)
        if let period = _period {
            LibLKF.lkf_set_contextparametersFromString (ptr!, (period as NSString).utf8String, flags)
        }
    }
    
    public var analyseDataEnabled : Bool {
        get {
            return createOutputForTest
        }
    }
    
    public var flags: Int32 {
        get {
            if model > 0 && outputFormat != .standard {
                if outputFormat == .ascii && model > 2016 {
                    return _flags | (1<<5)
                }
                else if outputFormat == .utf8 && model < 2017 {
                    return _flags | (1<<5)
                }
            }
            return _flags
        }
        set {
            self._flags = newValue
            if ptr != nil {
                LibLKF.lkf_set_contextflags(ptr!, flags)
            }
        }
    }
    
    public var outputFormat : LKFOutputFormat {
        get {
            return _outputFormat
        }
        set {
            _outputFormat = newValue
            if ptr != nil {
                LibLKF.lkf_set_contextflags(ptr!, flags)
            }
        }
    }
    
    public var model: UInt32 {
        return _model
    }
    
    public var period: String {
        return _period ?? ""
    }
    
    public var year : Int {
        if let ptr {
            return Int(LibLKF.lkf_get_PeriodYear(ptr))
        }
        return 0
    }
/*
    public var path: String? {
        return _path
    }
*/
    public func setContextParameters(period: String) -> Bool {
        _period = period

        if period.count < 5, let year = Int32(_period!) {
            _period = "\(year)"
            if ptr != nil {
                LibLKF.lkf_set_contextparameters(ptr!, year, 1, 1, 74 /*J*/, flags)
            }
            return true
        }
        else {
            let lineBuffer = (period as NSString).utf8String
            return LibLKF.lkf_set_contextparametersFromString(ptr!, lineBuffer, flags) > 0
        }
    }
    
    public var KANR : String {
        get {
            if ptr != nil && LibLKF.lkf_get_context_ka_anz (ptr!) > 0 {
                var value1 : UInt32 = 0
                var value2 : UInt32 = 0
                return withUnsafeMutablePointer(to: &value1) { ptr1 in
                    return withUnsafeMutablePointer(to: &value2) { ptr2 in
                        let r : String
                        if let kanrString = LibLKF.lkf_get_context_ka(ptr!, 0, ptr1, ptr1) {
                            r = String(cString:kanrString)
                        }
                        else {
                            r = "K000"
                        }
                        return r
                    }
                }
            }
            return "K000"
        }
    }
    
    public func setContextParameters(year: Int32) {
        _period = "\(year)"
        if let ptr = ptr {
            LibLKF.lkf_set_contextparameters(ptr, year, 1, 1, 74 /*J*/, flags)
        }
    }
    
    public func aufenthalte() -> [LKFAufenthalt] {
        var arr = [LKFAufenthalt]()

        for i in 0..<getAufenthaltCount() {
            arr.append(self[i])
        }

        return arr
    }
    
    public func aufenthalte(matching filter: LKFFilter) -> [LKFAufenthalt] {
        var arr = [LKFAufenthalt]()

        for i in 0..<getAufenthaltCount() {
            if filter.isIncluded(contact: self[i]) {
                arr.append(self[i])
            }
        }

        return arr
    }
    
    public func insertFrom (aufenthalt: LKFAufenthalt) throws {
        try addLines(aufenthalt.getOutput())
    }
    
    public func applyCommandLine (line: String) {

        let commands = line.dropFirst().split(separator: ";")
        for command in commands {
            
            let kv = command.split(separator: "=")
            let value = kv.count > 1 ? kv[1] : ""
            switch (kv[0]) {
            case "model":
                if let model = UInt32(value) {
                    _model = model
                    if let metaDataContext  {
                        do {
                            try initialize(model: _model, path: metaDataContext.baseUrl.path())
                        }
                        catch {
                            lastError = LKFError.from(error: error)
                        }
                    }
                }
                break
            case "period", "year":
                if !value.isEmpty {
                    _ = setContextParameters(period: String(value))
                }
                break
            case "flags":
                if let flags = Int32(value) {
                    self.flags = flags
                }
                break
            case "path":
                if !value.isEmpty {
                    if let url = URL(string: String(value)) {
                        self.metaDataContext = LKFMetaDataFilesContext (model: _model, name: "", baseUrl: url)
                        if _model > 0 {
                            do {
                                try initialize(metaDataContext: metaDataContext!)
                            }
                            catch {
                                lastError = LKFError.from(error: error)
                            }
                        }
                    }
                }
                break
            case "fileVersion":
                _fileVersion = String(value)
                break
            default:
                break
            }
            
        }
        
    }
    
    public func GetErrorText() -> String? {
        guard let estr = LibLKF.lkf_context_get_errortext(ptr!) else {
            return nil
        }
        let s = String(cString: estr)
        if s.count == 0 {
             return nil
        }
        return s
    }
    
    public func GetOutOfMemoryError() -> LKFOutOfMemoryError? {
        guard let cstr = LibLKF.lkf_context_get_outOfMemoryText(ptr) else {
            return nil
        }
        
        return LKFOutOfMemoryError("OutOfMemory: " + String(cString: cstr))
    }
    
    public func GetOutOfMemoryError(or: LKFError) -> LKFError {
        return GetOutOfMemoryError() ?? or
    }
    
    public func addLine (line: String) throws {

        let firstChar = line.first
        if firstChar == nil || firstChar == "\0" {
            return // ignore empty line
        }
        
        if allowCommands && line.hasPrefix("#") {
            applyCommandLine(line: line)
            return
        }
        allowCommands = false
        
        if ptr == nil {
            throw LKFError("no context")
        }
        
        let lineBuffer = (line as NSString).utf8String
        if LibLKF.lkf_add_line(ptr!, lineBuffer) < 0 {
           
            if let oom = GetOutOfMemoryError() {
                throw oom
            }
            
            if let estr = GetErrorText() {
                throw LKFError(estr)
            }
        }
    }
    
    public func addwindowsCP1252 (line: String) -> Bool {
        if ptr == nil {
            return false
        }
        
        if let data = line.data(using: .windowsCP1252) {
            let array = [UInt8](data)
            if LibLKF.lkf_add_line(ptr!, array) < 0 {
                return false
            }
            return true
        }
        return false
    }

    public func addLineIdent (line: String) -> String? {

        let firstChar = line.first
        if firstChar == nil || firstChar == "\0" {
            return nil
        }
        
        if allowCommands && line.hasPrefix("#") {
            applyCommandLine(line: line)
            return "cmd"
        }
        allowCommands = false
        
        guard let ptr else {
            return nil
        }
        
        let lineBuffer = (line as NSString).utf8String
        let cline = UnsafeMutablePointer<CChar>.allocate(capacity:1024)
        defer {
            cline.deallocate()
        }
        if LibLKF.lkf_add_xlineIdent (ptr, lineBuffer, cline) == 0 {
            return String(cString: cline)
        }
        return nil
    }
    
    public func replace (line: String, ident: String) -> Bool {
        guard let ptr else {
            return false
        }
        return LibLKF.lkf_replace_xline(ptr, (line as NSString).utf8String, (ident as NSString).utf8String) == 0
    }
    
    public func deleteLine (ident: String) -> Bool {
        guard let ptr else {
            return false
        }
        return LibLKF.lkf_delete_xline (ptr, (ident as NSString).utf8String) == 0
    }

    
    public func addLines (_ lines: [String]) throws {
        for s in lines {
            try addLine(line: s)
        }
    }
    
    public func load (file: URL, encoding: String.Encoding, enableComparing: Bool = false) throws {
        let fileContents = try String(contentsOf: file, encoding: encoding)
        let lines = fileContents.components(separatedBy: .newlines)
        try load (lines: lines, enableComparing: enableComparing)
    }
    
    public func load (file: URL, encodings: [String.Encoding] = [.utf8, .ascii, .windowsCP1252], enableComparing: Bool = false) throws -> String.Encoding {
        
        var err: Error? = nil
        
        for encoding in encodings {
            do {
                let fileContents = try String(contentsOf: file, encoding: encoding)
                let lines = fileContents.components(separatedBy: .newlines)
                try load (lines: lines, enableComparing: enableComparing)
                return encoding
            }
            catch {
                if err == nil {
                    err = error
                }
            }
        }
        
        throw err ?? LKFError("no encoding")
    }
    
    public static func createLKFFileProperties (file: URL) throws -> LKFFileProperties {
        return try LKFFileProperties(file: file)
    }
    
    public func load (file: URL) throws {        
        try load (file: file, encoding: .ascii, enableComparing: false)
    }
    
    public func load (data: Data) throws {
        guard let buf = String(data: data, encoding: .utf8) else {
            guard let bufascii = String(data: data, encoding: .ascii) else {
                throw LKFError(message: "invalid format")
            }
            try load (lines: bufascii.components(separatedBy: .newlines))
            return
        }
        try load (lines: buf.components(separatedBy: .newlines), enableComparing: false)
    }
    
    public func load (lines: [String], importFilter: LKFFileImportFilter? = nil, enableComparing: Bool = false, progress:((_:Int)->Bool)? = nil) throws {
        if enableComparing {
            compareCache = LKFCompareCache(context: self)
            
            if let progress {
                var lc = 0
                for s in lines {
                    if s.first != "C" {
                        
                        if let importFilter, s.starts(with: "X01|"), !importFilter.isIncluded(line: s) {
                            continue
                        }
                        
                        try? addLine(line: s)
                        lc += 1
                        if (lc % 1000) == 0 {
                            if progress(lc) {
                                return
                            }
                        }
                    }
                    compareCache!.sourceLineAdded(line: s)
                }
                if (lc%1000) != 0 {
                    _ = progress(lc)
                }

            }
            else {
                for s in lines {
                    if s.first != "C" {
                        if let importFilter, s.starts(with: "X01|"), !importFilter.isIncluded(line: s) {
                            continue
                        }
                        try addLine(line: s)
                    }
                    compareCache!.sourceLineAdded(line: s)
                }
            }
        }
        else if let progress {
            var lc = 0
            for s in lines {
                if let importFilter, s.starts(with: "X01|"), !importFilter.isIncluded(line: s) {
                    continue
                }
                try addLine(line: s)
                lc += 1
                if (lc % 1000) == 0 {
                    if progress(lc) {
                        return
                    }
                }
            }
            if (lc%1000) != 0 {
                _ = progress(lc)
            }
        }
        else {
            for s in lines {
                if let importFilter, s.starts(with: "X01|"), !importFilter.isIncluded(line: s) {
                    continue
                }
                try addLine(line: s)
            }
        }
    }
    
    public var preLineCount : UInt32 {
        let c = LibLKF.lkf_getaddlineanz(ptr!)
        for i in 0..<c {
            let s = getAddLine(at: i)
            if !s.starts(with: "K01") {
                return i
            }
        }
        return c
    }
    
    public var postLineCount : UInt32 {
        return LibLKF.lkf_getaddlineanz(ptr!) - preLineCount
    }

    public var getAddLineCount : UInt32 {
        return LibLKF.lkf_getaddlineanz(ptr!)
    }

    public func getAddLine (at index: UInt32) -> String {
        let lineBuffer = UnsafeMutablePointer<CChar>.allocate(capacity:1024)
        LibLKF.lkf_getaddline(ptr!, index, lineBuffer)
        let s = String(cString: lineBuffer)
        lineBuffer.deallocate()
        return s
    }
    
    public func getAddLine (at index: UInt32, lineBuffer: UnsafeMutablePointer<CChar>, identBuffer: UnsafeMutablePointer<CChar>) -> String {
        LibLKF.lkf_getaddlineAndIdent(ptr!, index, lineBuffer, identBuffer)
        let s = String(cString: lineBuffer)
        return s
    }
    
    public func computeIntensiv() -> UInt32 {
        if LibLKF.lkf_int_scoring(ptr!) != 0 {
            intAbtCount = 0
        }
        else {
            intAbtCount = LibLKF.lkf_int_getIntAufenthaltCount(ptr!)
        }
        return intAbtCount
    }
    
    public func getIntAbtCount() -> UInt32 {
        return intAbtCount
    }
    
    public func getIntAbt(atIndex: UInt32) -> LKFIntAbtResult? {
        if atIndex < intAbtCount {
            return LKFIntAbtResult(context: self, index: atIndex)
        }
        return nil
    }
    
    var ecb : @convention(c) (Optional<OpaquePointer>, Optional<UnsafePointer<Int8>>, UInt32) -> Int32 =
    { (context, name, count) -> Int32 in
        if let context = context, let current = executionDictionary[context], let executionLogger = current.executionLogger, let name = name {
            return executionLogger (String(cString:name), count)
        }
        return 0
    }
    
    static var executionDictionary = [OpaquePointer:LKFContext]()

    
    public func execute (caseCount: UInt32, to: @escaping (String, UInt32) -> Int32) -> Bool {
        guard let contextPtr = ptr else {
            return false
        }
        
        logger?.debug ("LKFContext.execute (caseCount=\(caseCount))")
        
        let analyser = analyseDataEnabled ? LKFDataAnalyser(context: self) : nil
        LKFContext.executionDictionary[contextPtr] = self
        executionLogger = to
        let check_res = LibLKF.lkf_check_abortable (contextPtr, 0, ecb, UInt32(1000))
        
        if check_res != 0 {
            let errorText = LibLKF.lkf_context_get_errortext(contextPtr)
            if let et = errorText {
                lastError = LKFError(message: String(cString:et))
            }
            else {
                lastError = LKFError(message: "lkf_check returned \(check_res)")
            }
            LKFContext.executionDictionary.removeValue(forKey: contextPtr)
            return false
        }
        
        let res = LibLKF.lkf_scoring_abortable (contextPtr, ecb, UInt32(1000))
        
        if res < 0 {
            let errorText = LibLKF.lkf_context_get_errortext(ptr!)
            if let et = errorText {
                lastError = LKFError(message: String(cString:et))
            }
            else {
                lastError = LKFError(message: "lkf_scoring returned \(res)")
            }
            LKFContext.executionDictionary.removeValue(forKey: contextPtr)
            return false
        }
        
        if let analyser = analyser {
            analyser.applyResults()
        }
        if let compareCache = compareCache {
            compareCache.applyResult()
        }
        LKFContext.executionDictionary.removeValue(forKey: contextPtr)
        computed = true
        return true
    }
    
    public func getValue(field: LKFContextValue) -> Int32 {
        guard let contextPtr = ptr else {
            return 0
        }
        return LibLKF.lkf_context_getValue(contextPtr, field.rawValue)
    }
    
    public func execute() throws {
        
        if ptr == nil {
            throw LKFError("cannot execute - no lkf context")
        }
        
        let analyser = analyseDataEnabled ? LKFDataAnalyser(context: self) : nil
        
        
        logger?.debug ("LKFContext.execute")
        
        let check_res = LibLKF.lkf_check(ptr!, 0)
        
        if check_res != 0 {
            let errorText = LibLKF.lkf_context_get_errortext(ptr!)
            
            if check_res == LKF_OUTOFMEMORY_UINTERROR, let oome = GetOutOfMemoryError() {
                throw oome
            }
            else if let et = errorText {
                throw LKFPlausCheckError(message: String(cString:et))
            }
            else {
                throw LKFPlausCheckError(message: "lkf_check returned \(check_res)")
            }
        }
        
        let v : UnsafePointer<Int8>? = nil
        let nullPointer = OpaquePointer(v)
        let res = LibLKF.lkf_scoring(ptr!, nullPointer)
        
        if res < 0 {
            let errorText = LibLKF.lkf_context_get_errortext(ptr!)
            if res == LKF_OUTOFMEMORY_UINTERROR || res == LKF_OUTOFMEMORY_ERROR, let oome = GetOutOfMemoryError() {
                throw oome
            }
            else if let et = errorText {
                throw LKFScoringError(message: String(cString:et))
            }
            else {
                throw LKFScoringError(message: "lkf_scoring returned \(res)")
            }
        }
        
        if let analyser = analyser {
            analyser.applyResults()
        }
        if let compareCache = compareCache {
            compareCache.applyResult()
        }
        computed = true
    }
    
    public func getErrors(fields:[LKFErrorField]) -> [String] {
        let errorFile = LKFOutputErrorFile(context: self)
        errorFile.setErrorFormat(facility:0, fields:fields)
        return errorFile.getOutput()
    }
    
    public func getError(errorIndex: UInt32, fields:[LKFErrorField]) -> [LKFErrorField:String] {
        
        let lineBuffer = UnsafeMutablePointer<CChar>.allocate(capacity:1024)
        
        
        _ = fields.map({ $0.rawValue }).withUnsafeBufferPointer { unsafe in
            LibLKF.lkf_getSatzartErrorFields (ptr!, errorIndex, UInt32(fields.count), unsafe.baseAddress, 1024, lineBuffer)
        }
        
        let r = String(cString: lineBuffer, encoding: .utf8)
        lineBuffer.deallocate()
        
        guard let comp = r?.components(separatedBy:";") else {
            return [:]
            
        }
            
        let fieldCount = min(fields.count, comp.count)
        var res = [LKFErrorField:String]()
        for i in 0..<fieldCount {
            res[fields[i]] = comp[i].trimmingCharacters(in: .whitespacesAndNewlines)
        }            
        return res
    }
    
    public func getErrors() -> [String] {
        return getErrors(fields:[.code,.aufnr,.abtfcent,.feld_lang,.text])
    }

    public func getWarnings(fields:[LKFErrorField]) -> [String] {
        let errorFile = LKFOutputErrorFile(context: self)
        errorFile.setErrorFormat(facility:0, fields:fields)
        errorFile.errorTypes = 65280
        return errorFile.getOutput()
    }

    public func getWarnings() -> [String] {
        return getWarnings(fields:[.code,.aufnr,.abtfcent,.feld_lang,.text])
    }

    public func getInfos(fields:[LKFErrorField]) -> [String] {
        let errorFile = LKFOutputErrorFile(context: self)
        errorFile.setErrorFormat(facility:0, fields:fields)
        errorFile.errorTypes = 1048576 * 2
        return errorFile.getOutput()
    }

    public func getFatal(fields:[LKFErrorField]) -> [String] {
        let errorFile = LKFOutputErrorFile(context: self)
        errorFile.setErrorFormat(facility:0, fields:fields)
        errorFile.errorTypes = 1
        return errorFile.getOutput()
    }
    public func getInfos() -> [String] {
        return getInfos(fields:[.code,.aufnr,.abtfcent,.feld_lang,.text])
    }

    public var fatalCount : UInt32 {
        guard let ptr = ptr else {
            return 0
        }
        return LibLKF.lkf_context_getSatzartErrorAnz(ptr, 8)
    }
    
    public var errorCount : UInt32 {
        var acceptedErrors : Int32 = 0
        guard let ptr = ptr else {
            return 0
        }
        return withUnsafeMutablePointer(to: &acceptedErrors) { ptr1 in
            return LibLKF.lkf_context_GetErrorAnz(ptr, 0, ptr1) + LibLKF.lkf_context_getSatzartErrorAnz(ptr, 4)
        }
    }
    
    public var warningCount : UInt32 {
        var acceptedErrors : Int32 = 0
        guard let ptr = ptr else {
            return 0
        }
        return withUnsafeMutablePointer(to: &acceptedErrors) { ptr1 in
            return LibLKF.lkf_context_GetWarningAnz(ptr, 0, ptr1) + LibLKF.lkf_context_getSatzartErrorAnz(ptr, 2)
        }
    }

    public var infoCount : UInt32 {
        var acceptedInfos : Int32 = 0
        guard let ptr = ptr else {
            return 0
        }
        return withUnsafeMutablePointer(to: &acceptedInfos) { ptr1 in
            return LibLKF.lkf_context_GetHinweisAnz(ptr, 0, ptr1) + LibLKF.lkf_context_getSatzartErrorAnz(ptr, 1)
        }
    }

    /*
    public var acceptedErrorCount : Int32 {
        var acceptedErrors : Int32 = 0
        withUnsafeMutablePointer(to: &acceptedErrors) { ptr1 in
            _ = LibLKF.lkf_context_GetErrorAnz(ptr!, 0, ptr1)
            return acceptedErrors
        }
    }
    */
    public func getErrorsAndWarnings(facility:UInt32 = 7, errorTypes: UInt32 = 65536*16384-1, fields:[LKFErrorField] = [.code,.aufnr,.feld_lang, .text]) -> [String] {
        let errorFile = LKFOutputErrorFile(context: self)
        errorFile.errorTypes = errorTypes
        errorFile.setErrorFormat(facility: facility, fields:fields)
        return errorFile.getOutput()
    }
    
    public func errorOutput () -> [String]? {
        return errorOutput(fields:nil)
    }
    
    public func errorOutput (fields: [LKFErrorField]?) -> [String]? {
        let errorFile = LKFOutputErrorFile(context: self)
        errorFile.setErrorFormat(facility:1, fields:fields)
        errorFile.setErrorFormat(facility:0, fields:fields)
        return errorFile.getOutput()
    }

    public func warningOutput () -> [String]? {
        return warningOutput(fields: nil)
    }

    public func warningOutput (fields: [LKFErrorField]?) -> [String]? {
        let errorFile = LKFOutputWarningFile(context: self)
        errorFile.setErrorFormat(facility:1, fields:fields)
        errorFile.setErrorFormat(facility:0, fields:fields)
        return errorFile.getOutput()
    }

    public func errorsWith (code: String, errorTypes: UInt32, fields:[LKFErrorField]? = [.code,.aufnr,.feld_lang])  -> [String]? {
        let errorFile = LKFOutputErrorFile(context: self)
        errorFile.errorTypes = errorTypes
        errorFile.setErrorFormat(facility:1, fields:fields)
        errorFile.setErrorFormat(facility:0, fields:fields)
        let errors = errorFile.getOutput()
        
        var returnedLines : [String]? = nil
        
        for s in errors {
            if s.starts(with: code) {
                if returnedLines == nil {
                    returnedLines = [s]
                }
                else {
                    returnedLines!.append(s)
                }
            }
        }
        
        return returnedLines
    }
    
    public func errorsWith (code: String, fields:[LKFErrorField]? = [.code,.aufnr,.feld_lang]) -> [String]? {
        return errorsWith(code: code, errorTypes: 65536*16384-1, fields: fields)
    }
    
    static public func unloadStamm () {
        LibLKF.lkf_unloadStamm()
    }
    
    public func executePackaged(packageSize: Int, file: URL, encodings: [String.Encoding] = [.ascii, .utf8, .windowsCP1252], enableComparing: Bool) throws  {
        
        let lines : [String]
        var fileContents : String?
        var encodingError : Error?
        
        for encoding in encodings {
            do {
                fileContents = try String(contentsOf: file, encoding: encoding)
                break
            }
            catch {
                encodingError = error
            }
        }
            
        guard let fileContents else {
            throw encodingError ?? LKFError("cannot read file")
        }
        lines = fileContents.components(separatedBy: .newlines)

        if enableComparing {
            compareCache = LKFCompareCache(context: self)
        }
        let analyser = analyseDataEnabled ? LKFDataAnalyser(context: self) : nil
        
        if packageOutputFiles != nil {
            for p in packageOutputFiles! {
                p.executionBegin()
            }
        }
        
        var x01_count = 0
        var hasSkipped = false
        
        for s in lines {
            
            let firstChar = s.first
            if firstChar == nil || firstChar == "\0" {
                continue
            }
            
            if allowCommands && s.hasPrefix("#") {
                applyCommandLine(line: s)
                continue
            }
            else {
                allowCommands = false
                if ptr == nil {
                    throw LKFError("no context")
                }
            }
            
            if let firstChar = s.first {
                if firstChar == "X" || firstChar == "M" || firstChar == "I" {
                    if x01_count < packageSize {
                        if s.starts(with: "X01") || s.starts(with: "M01") {
                            x01_count += 1
                        }
                    }
                    if x01_count == packageSize {
                        hasSkipped = true
                        continue
                    }
                    
                    try addLine(line: s)
                    if compareCache != nil {
                        compareCache!.sourceLineAdded(line: s)
                    }
                }
                else {
                    try addLine(line: s)
                    if compareCache != nil {
                        compareCache!.sourceLineAdded(line: s)
                    }
                }
            }
        }
        
        if ptr == nil {
            throw LKFError("no context")
        }
        logger?.debug ("LKFContext.executePackaged")
        LibLKF.lkf_context_plaus_package(ptr!, 1)
        let ret = LibLKF.lkf_context_scoring(ptr!, 0);
     
        if (ret < 0)
        {
             if let e = GetErrorText() {
                throw LKFError("error executing scoring: " + e)
            }
            throw LKFError("error executing scoring")
        }
        
        if let analyser = analyser {
            analyser.applyResults()
        }
        if let compareCache = compareCache {
            compareCache.applyResult()
        }

        if packageOutputFiles != nil {
            for p in packageOutputFiles! {
                p.packageExecuted()
            }
        }
        
        LibLKF.lkf_reset_aufenthalt_package(ptr!)
        
        var firstPackage = true
        x01_count = 0

        if hasSkipped {
            for s in lines {
                if let firstChar = s.first {
                    if firstChar == "X" || firstChar == "M" || firstChar == "I" {
                        if x01_count < packageSize {
                            if s.starts(with: "X01") || s.starts(with: "M01") {
                                x01_count += 1
                            }
                            if x01_count < packageSize {
                                if firstPackage {
                                    continue
                                }
                            }
                            else if firstPackage {
                                firstPackage = false
                                x01_count = 0
                            } else {
                                LibLKF.lkf_context_plaus_package(ptr!, 1)
                                let ret = LibLKF.lkf_context_scoring(ptr!, 0);

                                if (ret < 0)
                                {
                                    if let e = GetErrorText() {
                                       throw LKFError("error executing scoring: " + e)
                                   }
                                   throw LKFError("error executing scoring")
                                                        }
                                if let analyser = analyser {
                                    analyser.applyResults()
                                }
                                if let compareCache = compareCache {
                                    compareCache.applyResult()
                                }

                                if packageOutputFiles != nil {
                                    for p in packageOutputFiles! {
                                        p.packageExecuted()
                                    }
                                }
                                LibLKF.lkf_reset_aufenthalt_package(ptr!)
                                x01_count = 0
                            }
                        }
                        try addLine(line: s)
                        if compareCache != nil {
                            compareCache!.sourceLineAdded(line: s)
                        }
                    }
                    else {
                        continue
                    }
                }
            }
            if x01_count > 0 {
                LibLKF.lkf_context_plaus_package(ptr!, 1)
                let ret = LibLKF.lkf_context_scoring(ptr!, 0);
                if (ret < 0)
                {
                    if let e = GetErrorText() {
                       throw LKFError("error executing scoring: " + e)
                   }
                   throw LKFError("error executing scoring")
                }
                if let analyser = analyser {
                    analyser.applyResults()
                }
                if let compareCache = compareCache {
                    compareCache.applyResult()
                }

                if packageOutputFiles != nil {
                    for p in packageOutputFiles! {
                        p.packageExecuted()
                    }
                }
                LibLKF.lkf_reset_aufenthalt_package(ptr!)
            }
        }

        LibLKF.lkf_check_nombds(ptr!, 0)
        
        if packageOutputFiles != nil {
            for p in packageOutputFiles! {
                p.executionEnd()
            }
        }
        computed = true
    }
    
    public func executeStat () throws {
        
        guard let ptr else {
            throw LKFError("executeStat: no context ptr")
        }
        
        let ret = LibLKF.lkf_stat(ptr)
        if ret != 0 {
            if let oom = GetOutOfMemoryError() {
                throw oom
            }
            throw LKFError("lkf_stat returned \(ret)")
        }

    }
    
    public func write (file: URL, lines:[String]) throws {
        var s = ""
        for l in lines
        {
            s.append(l)
            s.append("\n")
        }
        
        
        if let data = s.data(using: .utf8) {
            try data.write(to: file, options: [])
        }
        else {
            throw LKFError(message: "cannot create data")
        }
    }
    
    public func add(file: LKFOutputFilePackaged) {
        if packageOutputFiles == nil {
            packageOutputFiles = []
        }
        packageOutputFiles!.append(file)
    }
    
    public func createOutput() -> [String] {
        let dataFile = LKFOutputDataFile(context: self)
        dataFile.createOutputForTest = createOutputForTest
        return dataFile.getOutput()
    }
    
    public func data (using encoding: String.Encoding = .utf8) throws -> Data? {
            let dataFile = LKFOutputDataFile(context: self)
            return try dataFile.getData(using: encoding)
        }
    
    public func getLineCount() -> UInt32 {
        
        guard let ptr = ptr else {
            return 0
        }
        
        var lineCount = LibLKF.lkf_getaddlineanz(ptr)
        
        let caseCount = LibLKF.lkf_get_aufenthalt_anz(ptr)
        for i in 0..<caseCount {
            let aufenthalt = LKFAufenthalt(context:self, index:i)
            lineCount += aufenthalt.lineCount
        }
        return lineCount
    }
    
    public func getAufenthaltCount() -> UInt32 {
        guard let ptr = ptr else {
            return 0
        }
        return LibLKF.lkf_get_aufenthalt_anz(ptr)
    }
    
    public func getResultLine(forIndex: UInt32) -> String {
        if forIndex < LibLKF.lkf_get_aufenthalt_anz(ptr!) {
            let aufenthalt = self[forIndex]
            return aufenthalt.getResultLine()
        }
        return ""
    }
    
    public func writeData (path: URL) throws {
        let dataFile = LKFOutputDataFile(context: self)
        dataFile.createOutputForTest = createOutputForTest
        let file = path.appendingPathComponent(dataFile.defaultfileName, isDirectory: false)
        try write(file:file, lines:createOutput())
    }
    
    public func write (output:LKFOutputFile, file: URL) throws {
        try write(file:file, lines:output.getOutput())
    }
    
    public subscript(index:UInt32) -> LKFAufenthalt {
        if let aufenthalt = _aufenthalte[index] {
            return aufenthalt
        }
        
        let aufenthalt = LKFAufenthalt (context: self, index: index)
        _aufenthalte[index] = aufenthalt
        return aufenthalt
    }
    
    public func add(aufenthalte: [LKFAufenthalt]) throws {
        try aufenthalte.forEach { try add(aufenthalt: $0) }
    }
    
    public func add(aufenthalt: LKFAufenthalt, options: LKFAufenthaltAddOptions? = nil) throws {
       try addLines(aufenthalt.getOutput())
    }
    
    public var first : LKFAufenthalt {
        return self[0]
    }
    
    public func setContextModelParameter (parameterIndex: Int32, value: String) -> Int32 {
        return LibLKF.lkf_context_set_modelparameter (ptr!, parameterIndex,  (value as NSString).utf8String)
    }
    
    public func Stat() -> LKFStat {
        return LKFStat(context: self)
    }
    
    public func CaseStat() -> LKFCaseStat {
        return LKFCaseStat(context: self)
    }
    
    public func AmbStat() -> LKFAmbStat {
        return LKFAmbStat(stat: Stat())
    }
    
    var callback : @convention(c) (Optional<OpaquePointer>, Optional<UnsafePointer<Int8>>) -> Void =
    { (context, line) -> Void in
        if let context = context, let current = traceDictionary[context], let traceLogger = current.traceLogger, let line = line {
            traceLogger(String(cString:line))
        }
    }
    
    static var traceDictionary = [OpaquePointer:LKFContext]()
    
    public func tracingStart (options: LKFContextTraceOptions?, to: @escaping (String) -> Void)
    {
        self.traceLogger = to
        LibLKF.lkf_set_debug_tracecb(ptr!, callback, 0)
        LKFContext.traceDictionary[ptr!] = self
        
        if let options = options {
            LibLKF.lkf_trace_setLevel (ptr!, Int32(1), options.mbdsLevel)
            LibLKF.lkf_trace_setLevel (ptr!, Int32(2), options.intscorLevel)
            LibLKF.lkf_trace_setLevel (ptr!, Int32(3), options.kaskoreLevel)
        
            if let aufnr = options.aufnr {
                let unsafeAufNr = (aufnr as NSString).utf8String
                LibLKF.lkf_trace_setAufenthalt(ptr!, unsafeAufNr)
            }
        }
    }
    
    public func tracingEnd () {
        LKFContext.traceDictionary.removeValue(forKey: ptr!)
        self.traceLogger = nil
        LibLKF.lkf_finish_debug_tracecb(ptr!)
    }
    
    public func getScorField (field: UInt32) -> Double {
        guard let ptr = ptr else {
            return 0
        }
        return LibLKF.lkf_get_context_scorvalue (ptr, 0, field)
    }
    
    public var points : Double {
        return getScorField(field: 34)
    }
    
    public func melKey (for mel: String) -> Int64 {
        if let ptr = ptr {
            return LibLKF.lkf_get_melLongKey(ptr, mel.cString(using: .utf8))
        }
        return 0
    }
    
    public func diagKey (for diag: String) -> Int64 {
        if let ptr = ptr {
            return LibLKF.lkf_get_diagLongKey(ptr, diag.cString(using: .utf8))
        }
        return 0
    }

    public func lineWith (ident: String, lineBuffer: UnsafeMutablePointer<CChar>) -> String? {
        if LibLKF.lkf_getaddlineWithIdent(ptr, ident.cString(using: .utf8), lineBuffer) == 0 {
            return String(cString: lineBuffer)
        }
        return nil
    }
    
    public func lineWith (ident: String) -> String? {
        let lineBuffer = UnsafeMutablePointer<CChar>.allocate(capacity:1024)
        defer {
            lineBuffer.deallocate()
        }
        if let s = lineWith(ident: ident, lineBuffer: lineBuffer) {
            return s
        }
        return nil
    }
    
    public func neonatologieLimit (for diag: String) -> UInt32 {
        return LibLKF.lkf_test_get_neonatologie_limit(diag.cString(using: .utf8), model)
    }
    
    public func add (change: LKFContextChange) {
        guard let changeGroup else {
            let changeGroup = LKFContextChangeGroup(name: "current")
            changeGroup.add(change: change)
            self.changeGroup = changeGroup
            return
        }
        changeGroup.add(change: change)
    }
    
    public func getIdent (recordTypeId: String, index: UInt32) throws -> String {
        let lineBuffer = UnsafeMutablePointer<CChar>.allocate(capacity:256)
        
        defer {
            lineBuffer.deallocate()
        }
        
        let result = LibLKF.lkf_getLineIdent(ptr!, recordTypeId.cString(using: .utf8), index, lineBuffer, 256)
        
        if result < 0 {
            throw LKFError("invalid record type \(recordTypeId)")
        }
        
        guard let s = String(cString: lineBuffer, encoding: .utf8) else {
            throw LKFError("cannot encode ident result for \(recordTypeId)")
        }
        
        return s
    }
}

public class LKFAufenthaltAddOptions {
    
}
