// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//


extension MassFormatter {
    public enum Unit : Int {
        case gram
        case kilogram
        case ounce
        case pound
        case stone
    }
}
    
open class MassFormatter : Formatter {
    
    public override init() {
        numberFormatter = NumberFormatter()
        numberFormatter.numberStyle = .decimal
        unitStyle = .medium
        isForPersonMassUse = false
        super.init()
    }
    
    public required init?(coder: NSCoder) {
        numberFormatter = NumberFormatter()
        numberFormatter.numberStyle = .decimal
        unitStyle = .medium
        isForPersonMassUse = false
        super.init(coder:coder)
    }
    
    /*@NSCopying*/ open var numberFormatter: NumberFormatter! // default is NumberFormatter with NumberFormatter.Style.decimal
    open var unitStyle: UnitStyle // default is Formatting.UnitStyle.medium
    
    open var isForPersonMassUse: Bool // default is NO; if it is set to YES, the number argument for -stringFromKilograms: and -unitStringFromKilograms: is considered as a person’s mass
    
    // Format a combination of a number and an unit to a localized string.
    open func string(fromValue value: Double, unit: Unit) -> String {
        // special case: stone shows fractional values in pounds
        if unit == .stone {
            let stone = value.rounded(.towardZero)
            let stoneString = singlePartString(fromValue: stone, unit: unit) // calling `string(fromValue: stone, unit: .stone)` would infinitely recur
            let pounds = abs(value.truncatingRemainder(dividingBy: 1.0)) * MassFormatter.poundsPerStone
            
            // if we don't have any fractional component, don't append anything
            if pounds == 0 {
                return stoneString
            } else {
                let poundsString = string(fromValue: pounds, unit: .pound)
                let separator = unitStyle == .short ? " " : ", "
                
                return ("\(stoneString)\(separator)\(poundsString)")
            }
        }
        
        // normal case: kilograms and pounds
        return singlePartString(fromValue: value, unit: unit)
    }
    
    // Format a number in kilograms to a localized string with the locale-appropriate unit and an appropriate scale (e.g. 1.2kg = 2.64lb in the US locale).
    open func string(fromKilograms numberInKilograms: Double) -> String {
        //Convert to the locale-appropriate unit
        let unitFromKilograms = convertedUnit(fromKilograms: numberInKilograms)
        
        //Map the unit to UnitMass type for conversion later
        let unitMassFromKilograms = MassFormatter.unitMass[unitFromKilograms]!
        
        //Create a measurement object based on the value in kilograms
        let kilogramMeasurement = Measurement<UnitMass>(value:numberInKilograms, unit: .kilograms)
        
        //Convert the object to the locale-appropriate unit determined above
        let unitMeasurement = kilogramMeasurement.converted(to: unitMassFromKilograms)
        
        //Extract the number from the measurement
        let numberInUnit = unitMeasurement.value
        
        return string(fromValue: numberInUnit, unit: unitFromKilograms)
    }
    
    // Return a localized string of the given unit, and if the unit is singular or plural is based on the given number.
    open func unitString(fromValue value: Double, unit: Unit) -> String {
        if unitStyle == .short {
            return MassFormatter.shortSymbol[unit]!
        } else if unitStyle == .medium {
            return MassFormatter.mediumSymbol[unit]!
        } else if unit == .stone { // special case, see `unitStringDisplayedAdjacent(toValue:, unit:)`
            return MassFormatter.largeSingularSymbol[unit]!
        } else if value == 1.0 {
            return MassFormatter.largeSingularSymbol[unit]!
        } else {
            return MassFormatter.largePluralSymbol[unit]!
        }
    }
    
    // Return the locale-appropriate unit, the same unit used by -stringFromKilograms:.
    open func unitString(fromKilograms numberInKilograms: Double, usedUnit unitp: UnsafeMutablePointer<Unit>?) -> String {
        //Convert to the locale-appropriate unit
        let unitFromKilograms = convertedUnit(fromKilograms: numberInKilograms)
        unitp?.pointee = unitFromKilograms
        
        //Map the unit to UnitMass type for conversion later
        let unitMassFromKilograms = MassFormatter.unitMass[unitFromKilograms]!
        
        //Create a measurement object based on the value in kilograms
        let kilogramMeasurement = Measurement<UnitMass>(value:numberInKilograms, unit: .kilograms)
        
        //Convert the object to the locale-appropriate unit determined above
        let unitMeasurement = kilogramMeasurement.converted(to: unitMassFromKilograms)
        
        //Extract the number from the measurement
        let numberInUnit = unitMeasurement.value
        
        //Return the appropriate representation of the unit based on the selected unit style
        return unitString(fromValue: numberInUnit, unit: unitFromKilograms)
    }
    
    // MARK: - Private
    
    /// This method selects the appropriate unit based on the formatter’s locale,
    /// the magnitude of the value, and isForPersonMassUse property.
    ///
    /// - Parameter numberInKilograms: the magnitude in terms of kilograms
    /// - Returns: Returns the appropriate unit
    private func convertedUnit(fromKilograms numberInKilograms: Double) -> Unit {
        if numberFormatter.locale.usesMetricSystem {
            if numberInKilograms > 1.0 || numberInKilograms <= 0.0 {
                return .kilogram
            } else {
                return .gram
            }
        } else {
            let metricMeasurement = Measurement<UnitMass>(value:numberInKilograms, unit: .kilograms)
            let imperialMeasurement = metricMeasurement.converted(to: .pounds)
            let numberInPounds = imperialMeasurement.value
            
            if numberInPounds >= 1.0 || numberInPounds <= 0.0  {
                return .pound
            } else {
                return .ounce
            }
        }
    }
    
    /// Formats the given value and unit into a string containing one logical 
    /// value. This is intended for units like kilogram and pound where 
    /// fractional values are represented as a decimal instead of converted 
    /// values in another unit.
    ///
    /// - Parameter value: The mass's value in the given unit.
    /// - Parameter unit: The unit used in the resulting mass string.
    /// - Returns: A properly formatted mass string for the given value and unit.
    private func singlePartString(fromValue value: Double, unit: Unit) -> String {
        guard let formattedValue = numberFormatter.string(from:NSNumber(value: value)) else {
            fatalError("Cannot format \(value) as string")
        }
        
        let separator = unitStyle == .short ? "" : " "
        
        return "\(formattedValue)\(separator)\(unitStringDisplayedAdjacent(toValue: value, unit: unit))"
    }
    
    /// Return the locale-appropriate unit to be shown adjacent to the given
    /// value.  In most cases this will match `unitStringDisplayedAdjacent(toValue:, unit:)` 
    /// however there are a few special cases:
    ///     - Imperial pounds with a short representation use "lb" in the
    ///       abstract and "#" only when shown with a numeral.
    ///     - Stones are are singular in the abstract and only plural when 
    ///       shown with a numeral.
    ///
    /// - Parameter value: The mass's value in the given unit.
    /// - Parameter unit: The unit used in the resulting mass string.
    /// - Returns: The locale-appropriate unit
    open func unitStringDisplayedAdjacent(toValue value: Double, unit: Unit) -> String {
        if unit == .pound && unitStyle == .short {
            return "#"
        } else if unit == .stone && unitStyle == .long {
            if value == 1.0 {
                return MassFormatter.largeSingularSymbol[unit]!
            } else {
                return MassFormatter.largePluralSymbol[unit]!
            }
        } else {
            return unitString(fromValue: value, unit: unit)
        }
    }
    

    
    /// The number of pounds in 1 stone
    private static let poundsPerStone = 14.0
    
    /// Maps MassFormatter.Unit enum to UnitMass class. Used for measurement conversion.
    private static let unitMass: [Unit: UnitMass] = [.gram: .grams,
                                                     .kilogram: .kilograms,
                                                     .ounce: .ounces,
                                                     .pound: .pounds,
                                                     .stone: .stones]
    
    /// Maps a unit to its short symbol. Reuses strings from UnitMass.
    private static let shortSymbol: [Unit: String] = [.gram: UnitMass.grams.symbol,
                                                      .kilogram: UnitMass.kilograms.symbol,
                                                      .ounce: UnitMass.ounces.symbol,
                                                      .pound: UnitMass.pounds.symbol, // see `unitStringDisplayedAdjacent(toValue:, unit:)`
                                                      .stone: UnitMass.stones.symbol]
    
    /// Maps a unit to its medium symbol. Reuses strings from UnitMass.
    private static let mediumSymbol: [Unit: String] = [.gram: UnitMass.grams.symbol,
                                                       .kilogram: UnitMass.kilograms.symbol,
                                                       .ounce: UnitMass.ounces.symbol,
                                                       .pound: UnitMass.pounds.symbol,
                                                       .stone: UnitMass.stones.symbol]
    
    /// Maps a unit to its large, singular symbol.
    private static let largeSingularSymbol: [Unit: String] = [.gram: "gram",
                                                              .kilogram: "kilogram",
                                                              .ounce: "ounce",
                                                              .pound: "pound",
                                                              .stone: "stone"]
    
    /// Maps a unit to its large, plural symbol.
    private static let largePluralSymbol: [Unit: String] = [.gram: "grams",
                                                            .kilogram: "kilograms",
                                                            .ounce: "ounces",
                                                            .pound: "pounds",
                                                            .stone: "stones"]
}
