//
// Copyright (c) 2019 Adyen N.V.
//
// This file is open source and available under the MIT license. See the LICENSE file for more info.
//

import Foundation

internal extension KeyedDecodingContainer {
    
    // MARK: - Dictionary
    
    /// Decodes a value of the given type for the given key.
    ///
    /// - parameter type: The type of value to decode.
    /// - parameter key: The key that the decoded value is associated with.
    /// - returns: A value of the requested type, if present for the given key and convertible to the requested type.
    /// - throws: `DecodingError.typeMismatch` if the encountered encoded value is not convertible to the requested type.
    /// - throws: `DecodingError.keyNotFound` if `self` does not have an entry for the given key.
    /// - throws: `DecodingError.valueNotFound` if `self` has a null entry for the given key.
    func decode(_ type: [String: Any].Type, forKey key: K) throws -> [String: Any] { // swiftlint:disable:this explicit_acl
        let container = try nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key)
        
        return try container.decode(type)
    }
    
    /// Decodes a value of the given type for the given key, if present.
    ///
    /// This method returns `nil` if the container does not have a value associated with `key`,
    /// or if the value is null. The difference between these states can be distinguished with a `contains(_:)` call.
    ///
    /// - parameter type: The type of value to decode.
    /// - parameter key: The key that the decoded value is associated with.
    /// - returns: A decoded value of the requested type,
    ///            or `nil` if the `Decoder` does not have an entry associated with the given key,
    ///            or if the value is a null value.
    /// - throws: `DecodingError.typeMismatch` if the encountered encoded value is not convertible to the requested type.
    func decodeIfPresent(_ type: [String: Any].Type, forKey key: K) throws -> [String: Any]? { // swiftlint:disable:this explicit_acl
        guard contains(key) else {
            return nil
        }
        
        return try decode(type, forKey: key)
    }
    
    // MARK: - Array
    
    /// Decodes a value of the given type for the given key.
    ///
    /// - parameter type: The type of value to decode.
    /// - parameter key: The key that the decoded value is associated with.
    /// - returns: A value of the requested type, if present for the given key and convertible to the requested type.
    /// - throws: `DecodingError.typeMismatch` if the encountered encoded value is not convertible to the requested type.
    /// - throws: `DecodingError.keyNotFound` if `self` does not have an entry for the given key.
    /// - throws: `DecodingError.valueNotFound` if `self` has a null entry for the given key.
    func decode(_ type: [Any].Type, forKey key: K) throws -> [Any] { // swiftlint:disable:this explicit_acl
        var container = try nestedUnkeyedContainer(forKey: key)
        
        return try container.decode(type)
    }
    
    /// Decodes a value of the given type for the given key, if present.
    ///
    /// This method returns `nil` if the container does not have a value associated with `key`,
    /// or if the value is null. The difference between these states can be distinguished with a `contains(_:)` call.
    ///
    /// - parameter type: The type of value to decode.
    /// - parameter key: The key that the decoded value is associated with.
    /// - returns: A decoded value of the requested type,
    ///            or `nil` if the `Decoder` does not have an entry associated with the given key,
    ///            or if the value is a null value.
    /// - throws: `DecodingError.typeMismatch` if the encountered encoded value is not convertible to the requested type.
    func decodeIfPresent(_ type: [Any].Type, forKey key: K) throws -> [Any]? { // swiftlint:disable:this explicit_acl
        guard contains(key) else {
            return nil
        }
        
        return try decode(type, forKey: key)
    }
    
    // MARK: - Private
    
    fileprivate func decode(_ type: [String: Any].Type) throws -> [String: Any] {
        var dictionary = [String: Any]()
        
        for key in allKeys {
            if let boolValue = try? decode(Bool.self, forKey: key) {
                dictionary[key.stringValue] = boolValue
            } else if let stringValue = try? decode(String.self, forKey: key) {
                dictionary[key.stringValue] = stringValue
            } else if let intValue = try? decode(Int.self, forKey: key) {
                dictionary[key.stringValue] = intValue
            } else if let doubleValue = try? decode(Double.self, forKey: key) {
                dictionary[key.stringValue] = doubleValue
            } else if let nestedDictionary = try? decode([String: Any].self, forKey: key) {
                dictionary[key.stringValue] = nestedDictionary
            } else if let nestedArray = try? decode([Any].self, forKey: key) {
                dictionary[key.stringValue] = nestedArray
            }
        }
        
        return dictionary
    }
    
}

internal extension KeyedEncodingContainer {
    /// Encodes the given value for the given key.
    ///
    /// - parameter value: The value to encode.
    /// - parameter key: The key to associate the value with.
    /// - throws: `EncodingError.invalidValue` if the given value is invalid in
    ///   the current context for this format.
    mutating func encode(_ value: [String: Any], forKey key: KeyedEncodingContainer.Key) throws { // swiftlint:disable:this explicit_acl
        var container = nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key)
        
        try value.forEach { (key: String, value: Any) in
            let codingKey = JSONCodingKeys(value: key)
            
            switch value {
            case let boolValue as Bool:
                try container.encode(boolValue, forKey: codingKey)
            case let stringValue as String:
                try container.encode(stringValue, forKey: codingKey)
            case let intValue as Int:
                try container.encode(intValue, forKey: codingKey)
            case let doubleValue as Double:
                try container.encode(doubleValue, forKey: codingKey)
            case let nestedDictionary as [String: Any]:
                try container.encode(nestedDictionary, forKey: codingKey)
            default:
                let context = EncodingError.Context(codingPath: codingPath, debugDescription: "Failed to encode dictionary value.")
                
                throw EncodingError.invalidValue(value, context)
            }
        }
    }
    
}

private extension UnkeyedDecodingContainer {
    mutating func decode(_ type: [Any].Type) throws -> [Any] {
        var array: [Any] = []
        
        while isAtEnd == false {
            if let value = try? decode(Bool.self) {
                array.append(value)
            } else if let value = try? decode(Double.self) {
                array.append(value)
            } else if let value = try? decode(String.self) {
                array.append(value)
            } else if let nestedDictionary = try? decode([String: Any].self) {
                array.append(nestedDictionary)
            } else if let nestedArray = try? decode([Any].self) {
                array.append(nestedArray)
            }
        }
        
        return array
    }
    
    mutating func decode(_ type: [String: Any].Type) throws -> [String: Any] {
        let nestedContainer = try self.nestedContainer(keyedBy: JSONCodingKeys.self)
        
        return try nestedContainer.decode(type)
    }
}

private struct JSONCodingKeys: CodingKey {
    internal var stringValue: String
    
    internal var intValue: Int?
    
    internal init(value: String) {
        self.stringValue = value
    }
    
    internal init?(stringValue: String) {
        self.stringValue = stringValue
    }
    
    internal init?(intValue: Int) {
        self.init(stringValue: "\(intValue)")
        self.intValue = intValue
    }
    
}
