import Foundation

extension String: PostgreSQLDataCustomConvertible {
    /// See `PostgreSQLDataCustomConvertible.postgreSQLDataType`
    public static var postgreSQLDataType: PostgreSQLDataType { return .text }

    /// See `PostgreSQLDataCustomConvertible.postgreSQLDataArrayType`
    public static var postgreSQLDataArrayType: PostgreSQLDataType { return ._text }

    /// See `PostgreSQLDataCustomConvertible.convertFromPostgreSQLData(_:)`
    public static func convertFromPostgreSQLData(_ data: PostgreSQLData) throws -> String {
        guard let value = data.data else {
            throw PostgreSQLError(identifier: "string", reason: "Could not decode String from `null` data.", source: .capture())
        }
        switch data.format {
        case .text:
            guard let string = String(data: value, encoding: .utf8) else {
                throw PostgreSQLError(identifier: "string", reason: "Non-UTF8 string: \(value.hexDebug).", source: .capture())
            }
            return string
        case .binary:
            switch data.type {
            case .text, .name, .varchar, .bpchar:
                guard let string = String(data: value, encoding: .utf8) else {
                    throw PostgreSQLError(identifier: "string", reason: "Non-UTF8 string: \(value.hexDebug).", source: .capture())
                }
                return string
            case .point:
                let point = try PostgreSQLPoint.convertFromPostgreSQLData(data)
                return point.description
            case .uuid:
                return try UUID.convertFromPostgreSQLData(data).uuidString
            case .numeric:
                /// create mutable value since we will be using `.extract` which advances the buffer's view
                var value = value

                /// grab the numeric metadata from the beginning of the array
                let metadata = value.extract(PostgreSQLNumericMetadata.self)

                var integer = ""
                var fractional = ""
                for offset in 0..<metadata.ndigits.bigEndian {
                    /// extract current char and advance memory
                    let char = value.extract(Int16.self).bigEndian

                    /// conver the current char to its string form
                    let string: String
                    if char == 0 {
                        /// 0 means 4 zeros
                        string = "0000"
                    } else {
                        string = char.description
                    }

                    /// depending on our offset, append the string to before or after the decimal point
                    if offset < metadata.weight.bigEndian + 1 {
                        integer += string
                    } else {
                        fractional += string
                    }
                }

                /// use the dscale to remove extraneous zeroes at the end of the fractional part
                let lastSignificantIndex = fractional.index(fractional.startIndex, offsetBy: Int(metadata.dscale.bigEndian))
                fractional = String(fractional[..<lastSignificantIndex])

                /// determine whether fraction is empty and dynamically add `.`
                let numeric: String
                if fractional != "" {
                    numeric = integer + "." + fractional
                } else {
                    numeric = integer
                }

                /// use sign to determine adding a leading `-`
                if metadata.sign.bigEndian == 1 {
                    return "-" + numeric
                } else {
                    return numeric
                }
            default: throw PostgreSQLError(identifier: "string", reason: "Could not decode String from binary data type: \(data.type)", source: .capture())
            }
        }
    }

    /// See `PostgreSQLDataCustomConvertible.convertToPostgreSQLData()`
    public func convertToPostgreSQLData() throws -> PostgreSQLData {
        return PostgreSQLData(type: .text, format: .binary, data: Data(utf8))
    }
}

/// Represents the meta information preceeding a numeric value.
/// Note: all values must be accessed adding `.bigEndian`
struct PostgreSQLNumericMetadata {
    /// The number of digits after this metadata
    var ndigits: Int16
    /// How many of the digits are before the decimal point (always add 1)
    var weight: Int16
    /// If 1, this number is negative. Otherwise, positive.
    var sign: Int16
    /// The number of sig digits after the decimal place (get rid of trailing 0s)
    var dscale: Int16
}

extension Data {
    /// Convert the row's data into a string, throwing if invalid encoding.
    internal func makeString(encoding: String.Encoding = .utf8) throws -> String {
        guard let string = String(data: self, encoding: encoding) else {
            throw PostgreSQLError(identifier: "utf8String", reason: "Unexpected non-UTF8 string: \(hexDebug).", source: .capture())
        }

        return string
    }
}