import Foundation

internal func identityAsString(_ value: Any?) -> String {
    let anyObject = value as AnyObject?
    if let value = anyObject {
        return NSString(format: "<%p>", unsafeBitCast(value, to: Int.self)).description
    } else {
        return "nil"
    }
}

internal func arrayAsString<T>(_ items: [T], joiner: String = ", ") -> String {
    return items.reduce("") { accum, item in
        let prefix = (accum.isEmpty ? "" : joiner)
        return accum + prefix + "\(stringify(item))"
    }
}

/// A type with a customized test output text representation.
///
/// This textual representation is produced when values will be
/// printed in test runs, and may be useful when producing
/// error messages in custom matchers.
///
/// - SeeAlso: `CustomDebugStringConvertible`
public protocol TestOutputStringConvertible {
    var testDescription: String { get }
}

extension Double: TestOutputStringConvertible {
    public var testDescription: String {
        return NSNumber(value: self).testDescription
    }
}

extension Float: TestOutputStringConvertible {
    public var testDescription: String {
        return NSNumber(value: self).testDescription
    }
}

extension NSNumber: TestOutputStringConvertible {
    public var testDescription: String {
        let description = self.description

        if description.contains(".") {
            let decimalPlaces = description.split(separator: ".")[1]
            switch decimalPlaces.count {
            case 1:
                return String(format: "%0.1f", doubleValue)
            case 2:
                return String(format: "%0.2f", doubleValue)
            case 3:
                return String(format: "%0.3f", doubleValue)
            default:
                return String(format: "%0.4f", doubleValue)
            }
        }

        return description
    }
}

extension Array: TestOutputStringConvertible {
    public var testDescription: String {
        let list = self.map(Nimble.stringify).joined(separator: ", ")
        return "[\(list)]"
    }
}

extension AnySequence: TestOutputStringConvertible {
    public var testDescription: String {
        let generator = self.makeIterator()
        var strings = [String]()
        var value: AnySequence.Element?

        repeat {
            value = generator.next()
            if let value = value {
                strings.append(stringify(value))
            }
        } while value != nil

        let list = strings.joined(separator: ", ")
        return "[\(list)]"
    }
}

extension NSArray: TestOutputStringConvertible {
    public var testDescription: String {
        let list = Array(self).map(Nimble.stringify).joined(separator: ", ")
        return "(\(list))"
    }
}

extension NSIndexSet: TestOutputStringConvertible {
    public var testDescription: String {
        let list = Array(self).map(Nimble.stringify).joined(separator: ", ")
        return "(\(list))"
    }
}

extension String: TestOutputStringConvertible {
    public var testDescription: String {
        return self
    }
}

extension Data: TestOutputStringConvertible {
    public var testDescription: String {
        return "Data<hash=\((self as NSData).hash),length=\(count)>"
    }
}

///
/// Returns a string appropriate for displaying in test output
/// from the provided value.
///
/// - parameter value: A value that will show up in a test's output.
///
/// - returns: The string that is returned can be
///     customized per type by conforming a type to the `TestOutputStringConvertible`
///     protocol. When stringifying a non-`TestOutputStringConvertible` type, this
///     function will return the value's debug description and then its
///     normal description if available and in that order. Otherwise it
///     will return the result of constructing a string from the value.
///
/// - SeeAlso: `TestOutputStringConvertible`
public func stringify<T>(_ value: T?) -> String {
    guard let value = value else { return "nil" }

    if let value = value as? TestOutputStringConvertible {
        return value.testDescription
    }

    if let value = value as? CustomDebugStringConvertible {
        return value.debugDescription
    }

    return String(describing: value)
}

#if canImport(Darwin)
@objc public class NMBStringer: NSObject {
    @objc public class func stringify(_ obj: Any?) -> String {
        return Nimble.stringify(obj)
    }
}
#endif

// MARK: Collection Type Stringers

/// Attempts to generate a pretty type string for a given value. If the value is of a Objective-C
/// collection type, or a subclass thereof, (e.g. `NSArray`, `NSDictionary`, etc.). 
/// This function will return the type name of the root class of the class cluster for better
/// readability (e.g. `NSArray` instead of `__NSArrayI`).
///
/// For values that don't have a type of an Objective-C collection, this function returns the
/// default type description.
///
/// - parameter value: A value that will be used to determine a type name.
///
/// - returns: The name of the class cluster root class for Objective-C collection types, or the
/// the `dynamicType` of the value for values of any other type.
public func prettyCollectionType<T>(_ value: T) -> String {
    switch value {
    case is NSArray:
        return String(describing: NSArray.self)
    case is NSDictionary:
        return String(describing: NSDictionary.self)
    case is NSSet:
        return String(describing: NSSet.self)
    case is NSIndexSet:
        return String(describing: NSIndexSet.self)
    default:
        return String(describing: value)
    }
}

/// Returns the type name for a given collection type. This overload is used by Swift
/// collection types.
///
/// - parameter collection: A Swift `CollectionType` value.
///
/// - returns: A string representing the `dynamicType` of the value.
public func prettyCollectionType<T: Collection>(_ collection: T) -> String {
    return String(describing: type(of: collection))
}
