//
//  LKFTestCache.swift
//  lkfc
//
//  Created by Alexander Holzer on 10.06.18.
//  Copyright © 2018 Alexander Holzer. All rights reserved.
//

import Foundation
@_implementationOnly import LibLKF

class LKFCompareErrorCacheLine {

    static func createValue (from line: String) -> String {
        let errorLine = line.dropFirst(4)
        let parts = errorLine.split(separator: ";")
        if parts.count < 4 {
            return ""
        }
        return parts.dropFirst(4).map { $0.trimmingCharacters(in: .whitespaces) }.joined(separator: "|")
    }
    
    let value: String
    var count : Int

    init (value: String) {
        self.value = value
        self.count = 1
    }
}

class LKFCompareErrorCacheKey {
    
    static func createKey(from line: String) -> String {
        let errorLine = line.dropFirst(4)
        let parts = errorLine.split(separator: ";")
        if parts.count < 4 {
            return String(errorLine)
        }
        return parts[1...3].map { $0.trimmingCharacters(in: .whitespaces) }.joined(separator: "|")
    }
    
    let key: String
    var values = [String:LKFCompareErrorCacheLine]()
    
    init (key : String, line: String) {
        self.key = key
        let value = LKFCompareErrorCacheLine.createValue(from: line)
        self.values = [value: LKFCompareErrorCacheLine (value: value)]
    }
    
    func add (line: String) {
        
        let value = LKFCompareErrorCacheLine.createValue(from: line)
        
        if let v = values[value] {
            v.count += 1
        }
        else {
            values[value] = LKFCompareErrorCacheLine (value: value)
        }
    }
    
    func getErrorCount() -> Int {
        var c = 0
        values.values.forEach { c += $0.count }
        return c
    }
}

class LKFCompareErrorCacheCode {
    static func createCode(from line: String) -> String {
        let errorLine = line.dropFirst(4)
        let parts = errorLine.split(separator: ";")
        return String(parts[0].trimmingCharacters(in: .whitespaces))
    }
    
    let code: String
    var errors = [String:LKFCompareErrorCacheKey]()
    
    init (code : String, line: String) {
        self.code = code
        let key = LKFCompareErrorCacheKey.createKey (from: line)
        self.errors = [key: LKFCompareErrorCacheKey(key: key, line: line)]
    }
    
    func add (line: String) {
        let key = LKFCompareErrorCacheKey.createKey (from: line)
        
        if let v = errors[key] {
            v.add(line: line)
        }
        else {
            errors[key] = LKFCompareErrorCacheKey (key: key, line: line)
        }
    }
    
    func getErrorCount() -> Int {
        var c = 0
        errors.values.forEach { c += $0.getErrorCount() }
        return c
    }
}


public class LKFCompareCache {

    weak var context: LKFContext?
    var x05Lines = [String]()
    var x05TargetLines = [String]()
    
    var sourceErrors = [String: LKFCompareErrorCacheCode] ()
    var targetErrors = [String: LKFCompareErrorCacheCode] ()
    
    init(context: LKFContext) {
        self.context = context
    }
    
    func addSourceError (line: String) {
        let code = LKFCompareErrorCacheCode.createCode (from: line)
        
        if let ek = sourceErrors[code] {
            ek.add(line: line)
        }
        else {
            sourceErrors[code] = LKFCompareErrorCacheCode (code: code, line: line)
        }
    }
 
    
    func addTargetError (line: String) {
        let code = LKFCompareErrorCacheCode.createCode (from: line)
        
        if let ek = targetErrors[code] {
            ek.add(line: line)
        }
        else {
            targetErrors[code] = LKFCompareErrorCacheCode (code: code, line: line)
        }
    }
    
    func sourceLineAdded (line: String) {
        
        if line.first == "C" {
            if line.hasPrefix("CW1|") {
                addSourceError(line: line)
            }
            else if line.hasPrefix("CE1|") {
                addSourceError(line: line)
            }
            else if line.hasPrefix("CI1|") {
                addSourceError(line: line)
            }
            return
        }
                
        if line.hasPrefix("X05") || line.hasPrefix("M06") {
            x05Lines.append(line)
        }
    }
    
    func targetLineAdded (line: String) {
        if line.hasPrefix("X05") || line.hasPrefix("M06") {
            x05TargetLines.append(line)
        }
    }
    
    func applyResult () {
        if let context = context {
            let count = LibLKF.lkf_get_aufenthalt_anz(context.ptr!)
            let lineBuffer = UnsafeMutablePointer<CChar>.allocate(capacity:1024)
            
            for i in 0..<count {
                let aufenthalt = LKFAufenthalt(context:context, index:i)
                _ = LibLKF.lkf_auf_line_scoring(aufenthalt.ptr, lineBuffer)
                targetLineAdded(line: String(cString:lineBuffer))
            }
            lineBuffer.deallocate()
        }
    }
    
    public func remove (part: Int, nullToBlank: Bool, from line: String) -> String {
        var parts = line.split(separator: "|", maxSplits: Int.max, omittingEmptySubsequences: false)
        
        if parts.count > part {
            parts[part] = ""
            
            for i in part..<parts.count {
                if parts[i] == "0" {
                    parts[i] = ""
                }
            }
            
            return parts.joined(separator: "|")
        }
        return line
    }
    
    public func compareErrors () -> [String]? {
        
        var differences = [String]()
        
        targetErrors.values.forEach { code in
            guard let sourceError = sourceErrors[code.code] else {
                differences.append("\(code.code) only in target (\(code.getErrorCount())x)")
                return
            }
            code.errors.forEach { errorKey in
                guard let sourceErrorkey = sourceError.errors[errorKey.key] else {
                    differences.append("\(code.code): key '\(errorKey.key)' only in target (\(errorKey.value.getErrorCount())x)")
                    return
                }
                
                errorKey.value.values.forEach { value in
                    guard let sourceValue = sourceErrorkey.values[value.key] else {
                        differences.append("\(code.code).\(errorKey.key): value '\(value.key)' only in target (\(value.value.count)x)")
                        return
                    }
                    if value.value.count != sourceValue.count {
                        differences.append("\(code.code).\(errorKey.key).\(value.key): source=\(sourceValue.count)x target=\(value.value.count)x")
                    }
                }
            }
        }

        sourceErrors.values.forEach { code in
            guard let targetError = targetErrors[code.code] else {
                differences.append("\(code.code) only in source (\(code.getErrorCount())x)")
                return
            }
            code.errors.forEach { errorKey in
                guard let targetErrorkey = targetError.errors[errorKey.key] else {
                    differences.append("\(code.code): key '\(errorKey.key)' only in source (\(errorKey.value.getErrorCount())x)")
                    return
                }
                
                errorKey.value.values.forEach { value in
                    guard targetErrorkey.values[value.key] != nil else {
                        differences.append("\(code.code).\(errorKey.key): value '\(value.key)' only in source (\(value.value.count)x)")
                        return
                    }
                }
            }
        }
        
        return differences.count > 0 ? differences : nil
    }
    
    public func hasPartDifferences(source: String, target:String) -> Bool {
        let sourceParts = source.components(separatedBy: "|")
        let targetParts = target.components(separatedBy: "|")
        
        if sourceParts.count != targetParts.count {
            return true
        }
        
        for i in 0..<sourceParts.count {
            if sourceParts[i] == targetParts[i] {
                continue
            }
            if sourceParts[i] == "0" && targetParts[i] == "" {
                continue
            }
            if sourceParts[i] == "" && targetParts[i] == "0" {
                continue
            }
            return true
        }
        return false
    }
    
    public func compare (removeBM: Bool = false, onlyPointDiff: Bool = false) -> [String]? {
        let sortedSource = x05Lines.sorted {
            $0.compare($1) == ComparisonResult.orderedAscending
        }
        
        let sortedTarget = x05TargetLines.sorted {
            $0.compare($1) == ComparisonResult.orderedAscending
        }

        var diffs = [String]()
        
        
        var sourceSlice = ArraySlice<String>(sortedSource)
        var targetSlice = ArraySlice<String>(sortedTarget)
        
        var source : String?
        var target : String?
        
        repeat {
            source = sourceSlice.first
            target = targetSlice.first
            
            
            if let sourceLine = source, let target {

                let source : String
                if removeBM {
                    source = remove (part: 3, nullToBlank: true, from: sourceLine)
                }
                else {
                    source = sourceLine
                }

                if onlyPointDiff, let targetSub = target.substr(to: 14), source.starts(with: targetSub) {
                    if source.contains("FEHLER") || target.contains("FEHLER") {
                        sourceSlice = sourceSlice.dropFirst()
                        targetSlice = targetSlice.dropFirst()
                        continue;
                    }
                    
                }
                
                switch (source.compare(target)) {
                case .orderedAscending:
                    if hasPartDifferences(source: source, target: target) {
                        diffs.append("source: \(source)")
                    }
                    else {
                        targetSlice = targetSlice.dropFirst()
                    }
                    sourceSlice = sourceSlice.dropFirst()
                    break
                case .orderedDescending:
                    if hasPartDifferences(source: source, target: target) {
                        diffs.append("target: \(target)")
                    }
                    else {
                        sourceSlice = sourceSlice.dropFirst()
                    }
                    targetSlice = targetSlice.dropFirst()
                    break
                case .orderedSame:
                    sourceSlice = sourceSlice.dropFirst()
                    targetSlice = targetSlice.dropFirst()
                    break
                }
            }
            else if source != nil {
                diffs.append("source: \(source!)")
                sourceSlice = sourceSlice.dropFirst()
            }
            else if target != nil {
                diffs.append("target: \(target!)")
                targetSlice = targetSlice.dropFirst()
            }
            
        } while source != nil || target != nil
        
        return diffs.count > 0 ? diffs : nil
    }
}
