//
//  XMLDecoder.swift
//  XMLCoder
//
//  Created by Shawn Moore on 11/20/17.
//  Copyright © 2017 Shawn Moore. All rights reserved.
//

import Foundation

class XMLDecoderImplementation: Decoder {
    // MARK: Properties

    /// The decoder's storage.
    var storage: XMLDecodingStorage = XMLDecodingStorage()

    /// Options set on the top-level decoder.
    let options: XMLDecoder.Options

    /// The path to the current point in encoding.
    public internal(set) var codingPath: [CodingKey]

    public var nodeDecodings: [(CodingKey) -> XMLDecoder.NodeDecoding]

    /// Contextual user-provided information for use during encoding.
    public var userInfo: [CodingUserInfoKey: Any] {
        return options.userInfo
    }

    // The error context lenght
    open var errorContextLenght: UInt = 0

    // MARK: - Initialization

    /// Initializes `self` with the given top-level container and options.
    init(
        referencing container: Box,
        options: XMLDecoder.Options,
        nodeDecodings: [(CodingKey) -> XMLDecoder.NodeDecoding],
        codingPath: [CodingKey] = []
    ) {
        storage.push(container: container)
        self.codingPath = codingPath
        self.nodeDecodings = nodeDecodings
        self.options = options
    }

    // MARK: - Decoder Methods

    internal func topContainer() throws -> Box {
        guard let topContainer = storage.topContainer() else {
            throw DecodingError.valueNotFound(Box.self, DecodingError.Context(
                codingPath: codingPath,
                debugDescription: "Cannot get decoding container -- empty container stack."
            ))
        }
        return topContainer
    }

    private func popContainer() throws -> Box {
        guard let topContainer = storage.popContainer() else {
            throw DecodingError.valueNotFound(Box.self, DecodingError.Context(
                codingPath: codingPath,
                debugDescription:
                """
                Cannot get decoding container -- empty container stack.
                """
            ))
        }
        return topContainer
    }

    public func container<Key>(
        keyedBy _: Key.Type
    ) throws -> KeyedDecodingContainer<Key> {
        let topContainer = try self.topContainer()

        switch topContainer {
        case _ where topContainer.isNull:
            throw DecodingError.valueNotFound(
                KeyedDecodingContainer<Key>.self,
                DecodingError.Context(
                    codingPath: codingPath,
                    debugDescription:
                    """
                    Cannot get keyed decoding container -- found null box instead.
                    """
                )
            )
        case let string as StringBox:
            return KeyedDecodingContainer(XMLKeyedDecodingContainer<Key>(
                referencing: self,
                wrapping: SharedBox(KeyedBox(
                    elements: KeyedStorage([("value", string)]),
                    attributes: KeyedStorage()
                ))
            ))
        case let keyed as SharedBox<KeyedBox>:
            return KeyedDecodingContainer(XMLKeyedDecodingContainer<Key>(
                referencing: self,
                wrapping: keyed
            ))
        default:
            throw DecodingError._typeMismatch(
                at: codingPath,
                expectation: [String: Any].self,
                reality: topContainer
            )
        }
    }

    public func unkeyedContainer() throws -> UnkeyedDecodingContainer {
        let topContainer = try self.topContainer()

        guard !topContainer.isNull else {
            throw DecodingError.valueNotFound(
                UnkeyedDecodingContainer.self,
                DecodingError.Context(
                    codingPath: codingPath,
                    debugDescription:
                    """
                    Cannot get unkeyed decoding container -- found null box instead.
                    """
                )
            )
        }

        let unkeyed = (topContainer as? SharedBox<UnkeyedBox>) ??
            SharedBox(UnkeyedBox([topContainer]))

        return XMLUnkeyedDecodingContainer(referencing: self, wrapping: unkeyed)
    }

    public func singleValueContainer() throws -> SingleValueDecodingContainer {
        return self
    }
}

// MARK: - Concrete Value Representations

extension XMLDecoderImplementation {
    /// Returns the given box unboxed from a container.

    private func typedBox<T, B: Box>(_ box: Box, for valueType: T.Type) throws -> B {
        switch box {
        case let typedBox as B:
            return typedBox
        case let keyedBox as SharedBox<KeyedBox>:
            guard
                let value = keyedBox.withShared({ $0.elements["value"] as? B })
            else {
                fallthrough
            }
            return value
        case is NullBox:
            throw DecodingError.valueNotFound(valueType, DecodingError.Context(
                codingPath: codingPath,
                debugDescription: "Expected \(valueType) but found null instead."
            ))
        case let keyedBox as KeyedBox:
            guard
                let value = keyedBox.elements["value"] as? B
            else {
                fallthrough
            }
            return value
        default:
            throw DecodingError._typeMismatch(
                at: codingPath,
                expectation: valueType,
                reality: box
            )
        }
    }

    func unbox(_ box: Box) throws -> Bool {
        let stringBox: StringBox = try typedBox(box, for: Bool.self)
        let string = stringBox.unboxed

        guard let boolBox = BoolBox(xmlString: string) else {
            throw DecodingError._typeMismatch(at: codingPath, expectation: Bool.self, reality: box)
        }

        return boolBox.unboxed
    }

    func unbox(_ box: Box) throws -> Decimal {
        let stringBox: StringBox = try typedBox(box, for: Decimal.self)
        let string = stringBox.unboxed

        guard let decimalBox = DecimalBox(xmlString: string) else {
            throw DecodingError._typeMismatch(at: codingPath, expectation: Decimal.self, reality: box)
        }

        return decimalBox.unboxed
    }

    func unbox<T: BinaryInteger & SignedInteger & Decodable>(_ box: Box) throws -> T {
        let stringBox: StringBox = try typedBox(box, for: T.self)
        let string = stringBox.unboxed

        guard let intBox = IntBox(xmlString: string) else {
            throw DecodingError._typeMismatch(at: codingPath, expectation: T.self, reality: box)
        }

        guard let int: T = intBox.unbox() else {
            throw DecodingError.dataCorrupted(DecodingError.Context(
                codingPath: codingPath,
                debugDescription: "Parsed XML number <\(string)> does not fit in \(T.self)."
            ))
        }

        return int
    }

    func unbox<T: BinaryInteger & UnsignedInteger & Decodable>(_ box: Box) throws -> T {
        let stringBox: StringBox = try typedBox(box, for: T.self)
        let string = stringBox.unboxed

        guard let uintBox = UIntBox(xmlString: string) else {
            throw DecodingError._typeMismatch(at: codingPath, expectation: T.self, reality: box)
        }

        guard let uint: T = uintBox.unbox() else {
            throw DecodingError.dataCorrupted(DecodingError.Context(
                codingPath: codingPath,
                debugDescription: "Parsed XML number <\(string)> does not fit in \(T.self)."
            ))
        }

        return uint
    }

    func unbox<T: BinaryFloatingPoint & Decodable>(_ box: Box) throws -> T {
        let stringBox: StringBox = try typedBox(box, for: T.self)
        let string = stringBox.unboxed

        guard let floatBox = FloatBox(xmlString: string) else {
            throw DecodingError._typeMismatch(at: codingPath, expectation: T.self, reality: box)
        }

        guard let float: T = floatBox.unbox() else {
            throw DecodingError.dataCorrupted(DecodingError.Context(
                codingPath: codingPath,
                debugDescription: "Parsed XML number <\(string)> does not fit in \(T.self)."
            ))
        }

        return float
    }

    func unbox(_ box: Box) throws -> String {
        let stringBox: StringBox = try typedBox(box, for: String.self)
        let string = stringBox.unboxed

        return string
    }

    func unbox(_ box: Box) throws -> Date {
        switch options.dateDecodingStrategy {
        case .deferredToDate:
            storage.push(container: box)
            defer { storage.popContainer() }
            return try Date(from: self)

        case .secondsSince1970:
            let stringBox: StringBox = try typedBox(box, for: Date.self)
            let string = stringBox.unboxed

            guard let dateBox = DateBox(secondsSince1970: string) else {
                throw DecodingError.dataCorrupted(DecodingError.Context(
                    codingPath: codingPath,
                    debugDescription: "Expected date string to be formatted in seconds since 1970."
                ))
            }
            return dateBox.unboxed
        case .millisecondsSince1970:
            let stringBox: StringBox = try typedBox(box, for: Date.self)
            let string = stringBox.unboxed

            guard let dateBox = DateBox(millisecondsSince1970: string) else {
                throw DecodingError.dataCorrupted(DecodingError.Context(
                    codingPath: codingPath,
                    debugDescription: "Expected date string to be formatted in milliseconds since 1970."
                ))
            }
            return dateBox.unboxed
        case .iso8601:
            let stringBox: StringBox = try typedBox(box, for: Date.self)
            let string = stringBox.unboxed

            guard let dateBox = DateBox(iso8601: string) else {
                throw DecodingError.dataCorrupted(DecodingError.Context(
                    codingPath: codingPath,
                    debugDescription: "Expected date string to be ISO8601-formatted."
                ))
            }
            return dateBox.unboxed
        case let .formatted(formatter):
            let stringBox: StringBox = try typedBox(box, for: Date.self)
            let string = stringBox.unboxed

            guard let dateBox = DateBox(xmlString: string, formatter: formatter) else {
                throw DecodingError.dataCorrupted(DecodingError.Context(
                    codingPath: codingPath,
                    debugDescription: "Date string does not match format expected by formatter."
                ))
            }
            return dateBox.unboxed
        case let .custom(closure):
            storage.push(container: box)
            defer { storage.popContainer() }
            return try closure(self)
        }
    }

    func unbox(_ box: Box) throws -> Data {
        switch options.dataDecodingStrategy {
        case .deferredToData:
            storage.push(container: box)
            defer { storage.popContainer() }
            return try Data(from: self)
        case .base64:
            let stringBox: StringBox = try typedBox(box, for: Data.self)
            let string = stringBox.unboxed

            guard let dataBox = DataBox(base64: string) else {
                throw DecodingError.dataCorrupted(DecodingError.Context(
                    codingPath: codingPath,
                    debugDescription: "Encountered Data is not valid Base64"
                ))
            }
            return dataBox.unboxed
        case let .custom(closure):
            storage.push(container: box)
            defer { storage.popContainer() }
            return try closure(self)
        }
    }

    func unbox(_ box: Box) throws -> URL {
        let stringBox: StringBox = try typedBox(box, for: URL.self)
        let string = stringBox.unboxed

        guard let urlBox = URLBox(xmlString: string) else {
            throw DecodingError.dataCorrupted(DecodingError.Context(
                codingPath: codingPath,
                debugDescription: "Encountered Data is not valid Base64"
            ))
        }

        return urlBox.unboxed
    }

    private struct TypeMismatch: Error {}

    func unbox<T: Decodable>(_ box: Box) throws -> T {
        let decoded: T?
        let type = T.self

        if type == Date.self || type == NSDate.self {
            let date: Date = try unbox(box)
            decoded = date as? T
        } else if type == Data.self || type == NSData.self {
            let data: Data = try unbox(box)
            decoded = data as? T
        } else if type == URL.self || type == NSURL.self {
            let data: URL = try unbox(box)
            decoded = data as? T
        } else if type == Decimal.self || type == NSDecimalNumber.self {
            let decimal: Decimal = try unbox(box)
            decoded = decimal as? T
        } else if
            type == String.self || type == NSString.self,
            let value = (try unbox(box) as String) as? T {
            decoded = value
        } else {
            storage.push(container: box)
            defer { storage.popContainer() }

            do {
                decoded = try type.init(from: self)
            } catch {
                guard case DecodingError.valueNotFound = error,
                    let type = type as? AnyOptional.Type,
                    let result = type.init() as? T else {
                    throw error
                }

                return result
            }
        }

        guard let result = decoded else {
            throw DecodingError._typeMismatch(
                at: codingPath, expectation: type, reality: box
            )
        }

        return result
    }
}
