//
//  LKFAufenthalt.swift
//  lkfc
//
//  Created by Alexander Holzer on 07.06.18.
//  Copyright © 2018 Alexander Holzer. All rights reserved.
//

import Foundation
@_implementationOnly import LibLKF

public enum LKFAufenthaltFields : UInt32 {
    case flag = 1
    case kanr = 2
    case aufnr = 3
    case gebdat = 4
    case alter = 5
    case geschl = 6
    case staatb = 7
    case staatw = 8
    case plzw = 9
    case aufart = 10
    case neugeb = 11
    case altersgruppe = 12
    case aufid = 13
    case gesvd = 14
    case gesvdSpez = 53
    case gesvdMinus = 52
    case entart = 15
    case entdat = 16
    case hd = 17
    case kost = 20
    case gruppe = 21
    case knoten = 22
    case vdu = 23
    case vdo = 24
    case vdmw = 25
    case ptk = 26
    case plk = 27
    case scutk = 28
    case sculk = 29
    case vdzu = 30
    case intzu = 31
    case melzu = 32
    case spez = 33
    case ges = 34
    case ldfpk = 35
    case scuk = 36
    case art = 61
    case aufdat = 40
    case id = 103
    case aufabt = 39
    case entabt = 38
}

public enum LKFAufenthaltAcceptanceState {
    case notAcceptable
    case accepted
    case acceptable
}

public class LKFAufenthalt {
    
    let ptr: OpaquePointer
    weak public var context: LKFContext?
    public var lastTrace : [String]?
    public var _line : LKFLine?
    public var _commentLine : LKFLine?
    
    private var _computed : Bool = true
    
    public var changedAufenthalt : LKFContextChangeAufenthalt?
        
    private var _result: LKFAufenthaltResult?
    
    public var result : LKFAufenthaltResult {
        if let _result {
            return _result
        }
        return getResult()
    }
    
    init (context: LKFContext, index: UInt32) {
        self.context = context
        ptr = LibLKF.lkf_get_aufenthalt(context.ptr!, index)
    }
    
    init (context: LKFContext, ptr: OpaquePointer) {
        self.context = context
        self.ptr = ptr
    }
    
    public func createLine (with type: String, content: String? = nil) -> String {
        
        let kanr = getField(.kanr)
        let aufnr = getField(.aufnr).trimmingCharacters(in: .whitespaces)
        let aufid = getField(.aufid).trimmingCharacters(in: .whitespaces)
        let aufdat = getField(.aufdat)
        let prefix = "\(type)|\(kanr)|\(aufnr)|\(aufid)|\(aufdat)"
        
        guard let content else {
            return prefix
        }
        if content.starts(with: "|") {
            return prefix + content
        }
        return prefix + "|\(content)"
    }
    
    public var computed : Bool {
        return _computed && (context?.computed ?? false)
    }
    
    public var lineCount: UInt32 {
        return intLineCount + mbdsLineCount
    }
    
    public var intLineCount: UInt32 {
        return LibLKF.lkf_auf_getintlineanz(ptr)
    }
    
    public var mbdsLineCount: UInt32 {
        return LibLKF.lkf_auf_getlineanz(ptr)
    }
    
    public func addLine (line: String) -> Bool {
        
        let firstChar = line.first
        if firstChar == nil || firstChar == "\0" {
            return false
        }
        self._computed = false
        let lineBuffer = (line as NSString).utf8String
        LibLKF.lkf_add_line_auf(ptr, lineBuffer)
        return true
    }
    
    public func mbdsLine (at index: UInt32, lineBuffer: UnsafeMutablePointer<CChar>, identBuffer: UnsafeMutablePointer<CChar>) -> String {
        LibLKF.lkf_auf_getLineAndIdent(ptr, index, lineBuffer, identBuffer)
        let s = String(cString: lineBuffer)
        return s
    }
    
    public func lineWith (ident: String, lineBuffer: UnsafeMutablePointer<CChar>) -> String? {
        if LibLKF.lkf_auf_getLineWithIdent(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 intLine (at index: UInt32, lineBuffer: UnsafeMutablePointer<CChar>, identBuffer: UnsafeMutablePointer<CChar>) -> String {
        LibLKF.lkf_auf_getintLineAndIdent(ptr, index, lineBuffer, identBuffer)
        return String(cString: lineBuffer)
    }
    
    public func intLine (ident: String, lineBuffer: UnsafeMutablePointer<CChar>) -> String? {
        LibLKF.lkf_auf_getLineWithIdent(ptr, ident.cString(using: .utf8), lineBuffer)
        let s = String(cString: lineBuffer)
        return s
    }
    
    public func intLine (ident: String) -> String? {
        let lineBuffer = UnsafeMutablePointer<CChar>.allocate(capacity:1024)
        defer {
            lineBuffer.deallocate()
        }
        if let s = intLine(ident: ident, lineBuffer: lineBuffer) {
            return s
        }
        return nil
    }
    
    public func replaceMbdsLine (with line: String, ident: String) -> Bool {
        self._computed = false
        return LibLKF.lkf_auf_replaceLine(ptr, line.cString(using: .utf8), ident.cString(using: .utf8)) == 0
    }
    
    public func deleteMbdsLine (ident: String) -> Bool {
        self._computed = false
        return LibLKF.lkf_delete_xline(ptr, ident.cString(using: .utf8)) == 0
    }
    
    public func replaceIntLine (with line: String, ident: String) -> Bool {
        self._computed = false
        return LibLKF.lkf_auf_replaceIntLine(ptr, line.cString(using: .utf8), ident.cString(using: .utf8)) == 0
    }
    
    public func deleteIntLine (ident: String) -> Bool {
        self._computed = false
        return LibLKF.lkf_auf_deleteIntLine(ptr, ident.cString(using: .utf8)) == 0
    }
    
    
    public func getOutput(mbds: Bool = true, intLines: Bool = true) -> [String] {
        
        var outputStrings = [String]()
        let lineBuffer = UnsafeMutablePointer<CChar>.allocate(capacity:1024)
        
        if mbds {
            let count = LibLKF.lkf_auf_getlineanz(ptr)
            outputStrings.reserveCapacity(Int(count))
            for i in 0..<count {
                LibLKF.lkf_auf_getline(ptr, i, lineBuffer)
                outputStrings.append(String(cString:lineBuffer))
            }
        }
        
        if intLines {
            let count = LibLKF.lkf_auf_getintlineanz(ptr)
            for i in 0..<count {
                LibLKF.lkf_auf_getintline(ptr, i, lineBuffer)
                outputStrings.append(String(cString:lineBuffer))
            }
        }
        
        
        lineBuffer.deallocate()
        return outputStrings
    }
    
    public func getResultLine () -> String {
        let lineBuffer = UnsafeMutablePointer<CChar>.allocate(capacity:1024)
        LibLKF.lkf_auf_line_scoring(ptr, lineBuffer)
        let s = String(cString: lineBuffer)
        lineBuffer.deallocate()
        return s
    }
    
    public func getField (fieldNum: UInt32) -> String {
        let lineBuffer = UnsafeMutablePointer<CChar>.allocate(capacity:512)
        LibLKF.lkf_auf_get_fieldvalue(ptr, fieldNum, lineBuffer, 512)
        let s = String(cString: lineBuffer)
        lineBuffer.deallocate()
        return s
    }
    
    public func getResultField (fieldNum: UInt32) -> Int {
        return LibLKF.lkf_auf_get_scorvalue(ptr, fieldNum)
    }
    
    public func getField (_ field: LKFAufenthaltFields) -> String {
        let lineBuffer = UnsafeMutablePointer<CChar>.allocate(capacity:512)
        LibLKF.lkf_auf_get_fieldvalue(ptr, field.rawValue, lineBuffer, 512)
        let s = String(cString: lineBuffer)
        lineBuffer.deallocate()
        return s
    }
    
    public func getResultField (_ field:LKFAufenthaltFields) -> Int {
        return LibLKF.lkf_auf_get_scorvalue(ptr, field.rawValue)
    }
    
    public var points : Int {
        return getResultField(fieldNum: 44)
    }
    
    public var errorCount : Int {
        var nextError = LibLKF.lkf_auf_firsterror(ptr)
        var ec = 0
        while nextError != nil {
            ec += 1
            nextError = LibLKF.lkf_auf_nexterror (nextError)
        }
        return ec
    }
    
    public var warningCount : Int {
        var nextError = LibLKF.lkf_auf_firstwarning (ptr)
        var ec = 0
        while nextError != nil {
            ec += 1
            nextError = LibLKF.lkf_auf_nextwarning(nextError)
        }
        return ec
    }
    
    public var infoCount : Int {
        var nextError = LibLKF.lkf_auf_firstinfo (ptr)
        var ec = 0
        while nextError != nil {
            ec += 1
            nextError = LibLKF.lkf_auf_nextinfo (nextError)
        }
        return ec
    }
    
    public var hd : String {
        return getField(fieldNum: 17)
    }
    
    public var aufId: String {
        return getField(fieldNum: 103)
    }
    
    public var gebDat: Date? {
        let s = getField(fieldNum: 4)
        return LKFConvert.toDate (string8:s)
    }
    
    public var staatb: String {
        return getField(.staatb)
    }
    
    public var staatw: String {
        return getField(.staatw)
    }
    
    public var plzw: String {
        return getField(.plzw)
    }
    
    public var aufart : String {
        return getField(.aufart)
    }
    
    public var entart : String {
        return getField(.entart)
    }
    
    
    public var aufDat: Date? {
        let s = getField(fieldNum: 40)
        return LKFConvert.toDate (string8:s)
    }
    
    public var entDat: Date? {
        let s = getField(.entdat)
        return LKFConvert.toDate(string8: s)
    }
    
    public var geschl: String {
        return getField(fieldNum: 6)
    }
    
    public func getTestOutput() -> [String] {
        return []
    }
    
    public var abtCount : UInt32 {
        return LibLKF.lkf_auf_get_abtanz(ptr)
    }
    
    public var melCount : UInt32 {
        return LibLKF.lkf_auf_get_melanz(ptr)
    }
    
    public var line : LKFLine
    {
        if _line == nil {
            let cline = UnsafeMutablePointer<CChar>.allocate(capacity:1024)
            defer {
                cline.deallocate()
            }
            LibLKF.lkf_auf_line_root (ptr, cline)
            _line = LKFLine (String(cString:cline))
        }
        return _line!
    }
    
    public var commentLine: LKFLine {
        if _commentLine == nil {
            let cline = UnsafeMutablePointer<CChar>.allocate(capacity:1024)
            defer {
                cline.deallocate()
            }
            LibLKF.lkf_auf_line_kommentar (ptr, 0, cline)
            _commentLine = LKFLine (String(cString:cline))
        }
        return _commentLine!

    }
    
    public var diagCount : UInt32 {
        return LibLKF.lkf_auf_get_diaganz(ptr)
    }
    
    public var mainDiagCount : UInt32 {
        return LibLKF.lkf_auf_get_hdiaganz(ptr)
    }
    
    public var akwarnCount: UInt32 {
        return LibLKF.lkf_auf_get_akwarnanz(ptr)
    }
    
    public func melAt (index: UInt32) -> LKFAufenthaltMEL {
        return LKFAufenthaltMEL(aufenthalt: self, index: index)
    }
    
    public func akwarnAt (index: UInt32) -> LKFAufenthaltAkWarn {
        return LKFAufenthaltAkWarn(aufenthalt: self, index: index)
    }
    
    public func diagAt (index: UInt32) -> LKFAufenthaltDiag {
        return LKFAufenthaltDiag(aufenthalt: self, index: index)
    }
    
    public func abtAt (index: UInt32) -> LKFAufenthaltAbt {
        return LKFAufenthaltAbt(aufenthalt: self, index: index)
    }
    
    public func getResult() -> LKFAufenthaltResult {
        let result = LKFAufenthaltResult(aufenthalt: self)
        self._result = result
        return result
    }
    
    public var isAmbulant : Bool {
        return LibLKF.lkf_auf_isAmb(ptr) != 0;
    }

    public var isAmbulantTKL : Bool {
        return LibLKF.lkf_auf_isAmb(ptr) == 2;
    }
    
    public var key : String {
        let lineBuffer = UnsafeMutablePointer<CChar>.allocate(capacity:512)
        LibLKF.lkf_auf_get_ident(ptr, lineBuffer)
        let s = String(cString: lineBuffer)
        lineBuffer.deallocate()
        return s
    }

    public var id : String {
        return getField(.id)
    }

    
    public var gesvd : UInt32 {
        let s = getField(.gesvd).trimmingCharacters(in: .whitespaces)
        if let v = UInt32(s) {
            return v
        }
        return 9999
    }
    
    public var altersgruppe : UInt32 {
        let s = getField(.altersgruppe).trimmingCharacters(in: .whitespaces)
        if let v = UInt32(s) {
            return v
        }
        return 9999
    }
    
    public var gesvdMinus : UInt32 {
        let s = getField(.gesvdMinus).trimmingCharacters(in: .whitespaces)
        if let v = UInt32(s) {
            return v
        }
        return 9999
    }
    
    public var gesvdSpez : UInt32 {
        let s = getField(.gesvdSpez).trimmingCharacters(in: .whitespaces)
        if let v = UInt32(s) {
            return v
        }
        return 9999
    }
    
    public var gruppe : String {
        return getField(fieldNum: 21)
    }
    
    public var knoten : String {
        return getField(fieldNum: 22)
    }
    
    public var mainMelIndex : UInt32? {
        let v = LibLKF.lkf_auf_get_scorvalue(ptr, 96)
        if v >= 0 {
            return UInt32(v)
        }
        return nil
    }
    
    public var gruppe_knoten : String? {
        let gruppe = self.gruppe
        if gruppe.count > 0 {
            let knoten = self.knoten
            if knoten.count > 0  && knoten.first! != " " {
                return "\(gruppe)/\(knoten)"
            }
            return gruppe
        }
        return nil
    }
    
    public func getIntResults() -> [LKFIntAbtResult?] {
        
        var arr = [LKFIntAbtResult?]()
        
        for i in 0..<abtCount {
            arr.append(LKFIntAbtResult(aufenthalt: self, index: i))
        }
        return arr
    }
    
    public func getErrors(format:LKFErrorFormat) -> [String] {
        var errorStrings = [String]()
        if let context = context {
            let lineBuffer = UnsafeMutablePointer<CChar>.allocate(capacity:1024)

            var err = LibLKF.lkf_auf_getSatzartError(ptr)
            while (err != nil) {
                _ = LibLKF.lkf_getSatzartErrorFromFormat (context.ptr, err, format.ptr, 1024, lineBuffer)
                errorStrings.append(String(cString: lineBuffer))
                err = LibLKF.lkf_auf_getNextSatzartError(err)
            }
            lineBuffer.deallocate()
        }
        return errorStrings
    }
    
    private static var defaultErrorFormat = LKFErrorFormat(fields: [.code,.aufnr,.abtfcent,.feld_lang,.text])
    
    public func getErrors() -> [String] {
        return getErrors(format:LKFAufenthalt.defaultErrorFormat)
    }
    
    public func findFirstMel(with melKey: Int64) -> LKFAufenthaltMEL? {
        let index = LibLKF.lkf_auf_findFirstMelIndex(ptr, melKey)
        if index >= 0 {
            return LKFAufenthaltMEL(aufenthalt: self, index: UInt32(index))
        }
        return nil
    }
    
    public func findFirstDiag(with diagKey: Int64) -> LKFAufenthaltDiag? {
        let index = LibLKF.lkf_auf_findFirstDiagIndex(ptr, diagKey)
        if index >= 0 {
            return LKFAufenthaltDiag(aufenthalt: self, index: UInt32(index))
        }
        return nil
    }
    
    public func findFirstAbt (abtfc: String) -> LKFAufenthaltAbt? {
        let index = LibLKF.lkf_auf_findFirstAbtString(ptr, abtfc.cString(using: .utf8), UInt32(abtfc.count))
        if index >= 0 {
            return LKFAufenthaltAbt(aufenthalt: self, index: UInt32(index))
        }
        return nil
    }
    
    public func findFirstAbtIndex (abtfc: [CChar], len: UInt32) -> Int32? {
        return LibLKF.lkf_auf_findFirstAbtString(ptr, abtfc, len);
    }

    public func findFirstMelIndex (mel: [CChar], len: UInt32) -> Int32? {
        return LibLKF.lkf_auf_findFirstMelString(ptr, mel, len);
    }

    public func findFirstDiagIndex (diag: [CChar], len: UInt32) -> Int32? {
        return LibLKF.lkf_auf_findFirstDiagString(ptr, diag, len);
    }

    public func matches (group: [CChar], len: UInt32) -> Bool {
        return LibLKF.lkf_auf_matchGroupString(ptr, group, len) != 0
    }
    
    public func createTrace () -> [String] {
        if let context = context {
            
            let traceOptions = LKFContextTraceOptions(mbdsLevel: 2, kaskoreLevel: 2, intscorLevel: 2, aufnr: aufId)
            lastTrace = []
            context.tracingStart(options: traceOptions) { s in
                self.lastTrace!.append(s)
            }
            recompute()
            context.tracingEnd()
            return self.lastTrace!
        }
        return []
    }
     
    public func createScoringDetails () -> LKFScoringDetails? {

        guard let context = context else {
            return nil
        }

        guard self.errorCount == 0 else {
            return nil
        }
        
        let traceOptions = LKFContextTraceOptions(mbdsLevel: 2, kaskoreLevel: 2, intscorLevel: 2, aufnr: aufId)
        lastTrace = []
        
        let originalFlag = context.flags
        context.flags = (originalFlag | 128)
        
        context.tracingStart(options: traceOptions) { s in
            self.lastTrace!.append(s)
        }
        recompute()
        context.tracingEnd()
        context.flags = originalFlag

        return LKFScoringDetails(aufenthalt: self)
    }
        
    public func trace () -> String {
        let output = createTrace()
        var result : String = ""
        for s in output {
            result.append(s)
            result.append("\n")
        }
        
        if let scoringDetails = createScoringDetails() {
            result.append("\nSCORING-Details:\n")
            for s in scoringDetails.createTrace() {
                result.append(s)
                result.append("\n")
            }
        }
        return result
    }
    
    public func recompute() {
        LibLKF.lkf_recompute_aufenthalt(ptr)
        self._computed = true
    }
    
    public func getDiags() -> [LKFAufenthaltDiag]? {
        let count = diagCount
        guard count > 0 else {
            return nil
        }
        var arr = [LKFAufenthaltDiag]()
        for i in 0..<count {
            arr.append (diagAt(index: i))
        }
        return arr
    }
    
    public func getAkWarn() -> [LKFAufenthaltAkWarn]? {
        let count = akwarnCount
        guard count > 0 else {
            return nil
        }
        var arr = [LKFAufenthaltAkWarn]()
        for i in 0..<count {
            arr.append (akwarnAt(index: i))
        }
        return arr
    }
    
    public func getMels() -> [LKFAufenthaltMEL]? {
        let count = melCount
        guard count > 0 else {
            return nil
        }
        var arr = [LKFAufenthaltMEL]()
        for i in 0..<count {
            arr.append (melAt(index: i))
        }
        return arr
    }
    
    public func getAbts() -> [LKFAufenthaltAbt]? {
        let count = abtCount
        guard count > 0 else {
            return nil
        }
        var arr = [LKFAufenthaltAbt]()
        for i in 0..<count {
            arr.append (abtAt(index: i))
        }
        return arr
    }
    
    public var optimizable : Bool {
        return LibLKF.lkf_auf_is_optimizable(ptr) != 0
    }
    
    public var optPercentage : Double {
        return LibLKF.lkf_auf_get_opt_percentage(ptr)
    }
    
    public func replace (text: String) throws {
        let lines = text.components(separatedBy: .newlines)
        try replace(lines: lines)
    }
    
    public func replace (lines: [String]) throws {
        
        guard lines.count > 0 else {
            throw LKFError("cannot replace LKFAufenthalt - empty buffer")
        }
        
        guard lines[0].hasPrefix("X01") else {
            throw LKFError("cannot replace LKFAufenthalt - first line needs to be X01")
        }
            
        let change = LKFContextChangeAufenthalt(operation: .replace, aufenthalt: self)
        
        let replaceReturn = LibLKF.lkf_replace_aufenthalt(ptr, lines[0].cString(using: .utf8))
        guard replaceReturn == 0 else {
            throw LKFError("cannot replace LKFAufenthalt - LKFScoring returned \(replaceReturn)")
        }
        
        for line in lines.dropFirst() {
            guard line.count > 3 else {
                continue
            }
            _ = addLine(line: line)
        }
        
        if self.changedAufenthalt == nil {
            self.changedAufenthalt = change
        }
    }
    
    public func acceptanceState (for code: String, value: String) -> LKFAufenthaltAcceptanceState {
        // TODO
        return .notAcceptable
    }
}
