/*
 Copyright (c) 2015, Apple Inc. All rights reserved.
 Copyright (c) 2015, Scott Guelich.
 Copyright (c) 2016, Ricardo Sánchez-Sáez.
 Copyright (c) 2017, Medable Inc. All rights reserved.
 Copyright (c) 2017, Macro Yau.
 Copyright (c) 2017, Sage Bionetworks.
 
 Redistribution and use in source and binary forms, with or without modification,
 are permitted provided that the following conditions are met:
 
 1.  Redistributions of source code must retain the above copyright notice, this
 list of conditions and the following disclaimer.
 
 2.  Redistributions in binary form must reproduce the above copyright notice,
 this list of conditions and the following disclaimer in the documentation and/or
 other materials provided with the distribution.
 
 3.  Neither the name of the copyright holder(s) nor the names of any contributors
 may be used to endorse or promote products derived from this software without
 specific prior written permission. No license is granted to the trademarks of
 the copyright holders even if such marks are included in this software.
 
 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
 FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


#import "ORKAnswerFormat.h"
#import "ORKAnswerFormat_Internal.h"

#import "ORKChoiceAnswerFormatHelper.h"
#import "ORKHealthAnswerFormat.h"
#import "ORKQuestionResult_Private.h"
#import "ORKResult_Private.h"

#import "ORKHelpers_Internal.h"

@import HealthKit;
@import MapKit;


NSString *const EmailValidationRegularExpressionPattern = @"^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}$";

id ORKNullAnswerValue() {
    return [NSNull null];
}

BOOL ORKIsAnswerEmpty(id answer) {
    return  (answer == nil) ||
    (answer == ORKNullAnswerValue()) ||
    ([answer isKindOfClass:[NSArray class]] && ((NSArray *)answer).count == 0);     // Empty answer of choice or value picker
}

NSString *ORKQuestionTypeString(ORKQuestionType questionType) {
#define SQT_CASE(x) case ORKQuestionType ## x : return @ORK_STRINGIFY(ORKQuestionType ## x);
    switch (questionType) {
            SQT_CASE(None);
            SQT_CASE(Scale);
            SQT_CASE(SingleChoice);
            SQT_CASE(MultipleChoice);
            SQT_CASE(MultiplePicker);
            SQT_CASE(Decimal);
            SQT_CASE(Integer);
            SQT_CASE(Boolean);
            SQT_CASE(Text);
            SQT_CASE(DateAndTime);
            SQT_CASE(TimeOfDay);
            SQT_CASE(Date);
            SQT_CASE(TimeInterval);
            SQT_CASE(Height);
            SQT_CASE(Weight);
            SQT_CASE(Location);
    }
#undef SQT_CASE
}

static NSNumberFormatterStyle ORKNumberFormattingStyleConvert(ORKNumberFormattingStyle style) {
    return style == ORKNumberFormattingStylePercent ? NSNumberFormatterPercentStyle : NSNumberFormatterDecimalStyle;
}


@implementation ORKAnswerDefaultSource {
    NSMutableDictionary *_unitsTable;
}

@synthesize healthStore=_healthStore;

+ (instancetype)sourceWithHealthStore:(HKHealthStore *)healthStore {
    ORKAnswerDefaultSource *source = [[ORKAnswerDefaultSource alloc] initWithHealthStore:healthStore];
    return source;
}

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-designated-initializers"
- (instancetype)init {
    ORKThrowMethodUnavailableException();
}
#pragma clang diagnostic pop

- (instancetype)initWithHealthStore:(HKHealthStore *)healthStore {
    self = [super init];
    if (self) {
        _healthStore = healthStore;
        
        if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){.majorVersion = 8, .minorVersion = 2, .patchVersion = 0}]) {
            [[NSNotificationCenter defaultCenter] addObserver:self
                                                     selector:@selector(healthKitUserPreferencesDidChange:)
                                                         name:HKUserPreferencesDidChangeNotification
                                                       object:healthStore];
        }
    }
    return self;
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (void)healthKitUserPreferencesDidChange:(NSNotification *)notification {
    _unitsTable = nil;
}

- (id)defaultValueForCharacteristicType:(HKCharacteristicType *)characteristicType error:(NSError **)error {
    id result = nil;
    if ([[characteristicType identifier] isEqualToString:HKCharacteristicTypeIdentifierDateOfBirth]) {
        NSDate *dob = [_healthStore dateOfBirthWithError:error];
        if (dob) {
            result = dob;
        }
    }
    if ([[characteristicType identifier] isEqualToString:HKCharacteristicTypeIdentifierBloodType]) {
        HKBloodTypeObject *bloodType = [_healthStore bloodTypeWithError:error];
        if (bloodType && bloodType.bloodType != HKBloodTypeNotSet) {
            result = ORKHKBloodTypeString(bloodType.bloodType);
        }
        if (result) {
            result = @[result];
        }
    }
    if ([[characteristicType identifier] isEqualToString:HKCharacteristicTypeIdentifierBiologicalSex]) {
        HKBiologicalSexObject *biologicalSex = [_healthStore biologicalSexWithError:error];
        if (biologicalSex && biologicalSex.biologicalSex != HKBiologicalSexNotSet) {
            result = ORKHKBiologicalSexString(biologicalSex.biologicalSex);
        }
        if (result) {
            result = @[result];
        }
    }
    if ([[characteristicType identifier] isEqualToString:HKCharacteristicTypeIdentifierFitzpatrickSkinType]) {
        HKFitzpatrickSkinTypeObject *skinType = [_healthStore fitzpatrickSkinTypeWithError:error];
        if (skinType && skinType.skinType != HKFitzpatrickSkinTypeNotSet) {
            result = @(skinType.skinType);
        }
        if (result) {
            result = @[result];
        }
    }
    if (ORK_IOS_10_WATCHOS_3_AVAILABLE && [[characteristicType identifier] isEqualToString:HKCharacteristicTypeIdentifierWheelchairUse]) {
        HKWheelchairUseObject *wheelchairUse = [_healthStore wheelchairUseWithError:error];
        if (wheelchairUse && wheelchairUse.wheelchairUse != HKWheelchairUseNotSet) {
            result = (wheelchairUse.wheelchairUse == HKWheelchairUseYes) ? @YES : @NO;
        }
        if (result) {
            result = @[result];
        }
    }
    return result;
}

- (void)fetchDefaultValueForQuantityType:(HKQuantityType *)quantityType unit:(HKUnit *)unit handler:(void(^)(id defaultValue, NSError *error))handler {
    if (!unit) {
        handler(nil, nil);
        return;
    }
    
    HKHealthStore *healthStore = _healthStore;
    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:HKSampleSortIdentifierEndDate ascending:NO];
    dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
        HKSampleQuery *sampleQuery = [[HKSampleQuery alloc] initWithSampleType:quantityType predicate:nil limit:1 sortDescriptors:@[sortDescriptor] resultsHandler:^(HKSampleQuery *query, NSArray *results, NSError *error) {
            HKQuantitySample *sample = results.firstObject;
            id value = nil;
            if (sample) {
                if (unit == [HKUnit percentUnit]) {
                    value = @(100 * [sample.quantity doubleValueForUnit:unit]);
                } else {
                    value = @([sample.quantity doubleValueForUnit:unit]);
                }
            }
            handler(value, error);
        }];
        [healthStore executeQuery:sampleQuery];
    });
}

- (void)fetchDefaultValueForAnswerFormat:(ORKAnswerFormat *)answerFormat handler:(void(^)(id defaultValue, NSError *error))handler {
    HKObjectType *objectType = [answerFormat healthKitObjectType];
    BOOL handled = NO;
    if (objectType) {
        if ([HKHealthStore isHealthDataAvailable]) {
            if ([answerFormat isKindOfClass:[ORKHealthKitCharacteristicTypeAnswerFormat class]]) {
                NSError *error = nil;
                id defaultValue = [self defaultValueForCharacteristicType:(HKCharacteristicType *)objectType error:&error];
                handler(defaultValue, error);
                handled = YES;
            } else if ([answerFormat isKindOfClass:[ORKHealthKitQuantityTypeAnswerFormat class]]) {
                [self updateHealthKitUnitForAnswerFormat:answerFormat force:NO];
                HKUnit *unit = [answerFormat healthKitUserUnit];
                [self fetchDefaultValueForQuantityType:(HKQuantityType *)objectType unit:unit handler:handler];
                handled = YES;
            }
        }
    }
    if (!handled) {
        handler(nil, nil);
    }
}

- (HKUnit *)defaultHealthKitUnitForAnswerFormat:(ORKAnswerFormat *)answerFormat {
    __block HKUnit *unit = [answerFormat healthKitUnit];
    HKObjectType *objectType = [answerFormat healthKitObjectType];
    if (![[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){.majorVersion = 8, .minorVersion = 2, .patchVersion = 0}]) {
        return unit;
    }
    
    if (unit == nil && [objectType isKindOfClass:[HKQuantityType class]] && [HKHealthStore isHealthDataAvailable]) {
        unit = _unitsTable[objectType];
        if (unit) {
            return unit;
        }
        if (!_unitsTable) {
            _unitsTable = [NSMutableDictionary dictionary];
        }
        
        HKQuantityType *quantityType = (HKQuantityType *)objectType;
        
        dispatch_semaphore_t sem = dispatch_semaphore_create(0);
        [_healthStore preferredUnitsForQuantityTypes:[NSSet setWithObject:quantityType] completion:^(NSDictionary *preferredUnits, NSError *error) {
            
            unit = preferredUnits[quantityType];
            
            dispatch_semaphore_signal(sem);
        }];
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
        
        if (unit) {
            _unitsTable[objectType] = unit;
        }
    }
    return unit;
}

- (void)updateHealthKitUnitForAnswerFormat:(ORKAnswerFormat *)answerFormat force:(BOOL)force {
    HKUnit *unit = [answerFormat healthKitUserUnit];
    HKUnit *healthKitDefault = [self defaultHealthKitUnitForAnswerFormat:answerFormat];
    if (!ORKEqualObjects(unit,healthKitDefault) && (force || (unit == nil))) {
        [answerFormat setHealthKitUserUnit:healthKitDefault];
    }
}

@end


#pragma mark - ORKAnswerFormat

@implementation ORKAnswerFormat

+ (ORKScaleAnswerFormat *)scaleAnswerFormatWithMaximumValue:(NSInteger)scaleMaximum
                                               minimumValue:(NSInteger)scaleMinimum
                                               defaultValue:(NSInteger)defaultValue
                                                       step:(NSInteger)step
                                                   vertical:(BOOL)vertical
                                    maximumValueDescription:(nullable NSString *)maximumValueDescription
                                    minimumValueDescription:(nullable NSString *)minimumValueDescription {
    return [[ORKScaleAnswerFormat alloc] initWithMaximumValue:scaleMaximum
                                                 minimumValue:scaleMinimum
                                                 defaultValue:defaultValue
                                                         step:step
                                                     vertical:vertical
                                      maximumValueDescription:maximumValueDescription
                                      minimumValueDescription:minimumValueDescription];
}

+ (ORKContinuousScaleAnswerFormat *)continuousScaleAnswerFormatWithMaximumValue:(double)scaleMaximum
                                                                   minimumValue:(double)scaleMinimum
                                                                   defaultValue:(double)defaultValue
                                                          maximumFractionDigits:(NSInteger)maximumFractionDigits
                                                                       vertical:(BOOL)vertical
                                                        maximumValueDescription:(nullable NSString *)maximumValueDescription
                                                        minimumValueDescription:(nullable NSString *)minimumValueDescription {
    return [[ORKContinuousScaleAnswerFormat alloc] initWithMaximumValue:scaleMaximum
                                                           minimumValue:scaleMinimum
                                                           defaultValue:defaultValue
                                                  maximumFractionDigits:maximumFractionDigits
                                                               vertical:vertical
                                                maximumValueDescription:maximumValueDescription
                                                minimumValueDescription:minimumValueDescription];
}

+ (ORKTextScaleAnswerFormat *)textScaleAnswerFormatWithTextChoices:(NSArray<ORKTextChoice *> *)textChoices
                                                      defaultIndex:(NSInteger)defaultIndex
                                                          vertical:(BOOL)vertical {
    return [[ORKTextScaleAnswerFormat alloc] initWithTextChoices:textChoices
                                                    defaultIndex:defaultIndex
                                                        vertical:vertical];
}

+ (ORKBooleanAnswerFormat *)booleanAnswerFormat {
    return [ORKBooleanAnswerFormat new];
}

+ (ORKBooleanAnswerFormat *)booleanAnswerFormatWithYesString:(NSString *)yes noString:(NSString *)no {
    return [[ORKBooleanAnswerFormat alloc] initWithYesString:yes noString:no];
}

+ (ORKValuePickerAnswerFormat *)valuePickerAnswerFormatWithTextChoices:(NSArray<ORKTextChoice *> *)textChoices {
    return [[ORKValuePickerAnswerFormat alloc] initWithTextChoices:textChoices];
}

+ (ORKMultipleValuePickerAnswerFormat *)multipleValuePickerAnswerFormatWithValuePickers:(NSArray<ORKValuePickerAnswerFormat *> *)valuePickers {
    return [[ORKMultipleValuePickerAnswerFormat alloc] initWithValuePickers:valuePickers];
}

+ (ORKImageChoiceAnswerFormat *)choiceAnswerFormatWithImageChoices:(NSArray<ORKImageChoice *> *)imageChoices {
    return [[ORKImageChoiceAnswerFormat alloc] initWithImageChoices:imageChoices];
}

+ (ORKImageChoiceAnswerFormat *)choiceAnswerFormatWithImageChoices:(NSArray<ORKImageChoice *> *)imageChoices
                                                             style:(ORKChoiceAnswerStyle)style
                                                          vertical:(BOOL)vertical {
    return [[ORKImageChoiceAnswerFormat alloc] initWithImageChoices:imageChoices
                                                              style:style
                                                           vertical:vertical];
}

+ (ORKTextChoiceAnswerFormat *)choiceAnswerFormatWithStyle:(ORKChoiceAnswerStyle)style
                                               textChoices:(NSArray<ORKTextChoice *> *)textChoices {
    return [[ORKTextChoiceAnswerFormat alloc] initWithStyle:style textChoices:textChoices];
}

+ (ORKNumericAnswerFormat *)decimalAnswerFormatWithUnit:(NSString *)unit {
    return [[ORKNumericAnswerFormat alloc] initWithStyle:ORKNumericAnswerStyleDecimal unit:unit minimum:nil maximum:nil];
}
+ (ORKNumericAnswerFormat *)integerAnswerFormatWithUnit:(NSString *)unit {
    return [[ORKNumericAnswerFormat alloc] initWithStyle:ORKNumericAnswerStyleInteger unit:unit minimum:nil maximum:nil];
}

+ (ORKTimeOfDayAnswerFormat *)timeOfDayAnswerFormat {
    return [ORKTimeOfDayAnswerFormat new];
}
+ (ORKTimeOfDayAnswerFormat *)timeOfDayAnswerFormatWithDefaultComponents:(NSDateComponents *)defaultComponents {
    return [[ORKTimeOfDayAnswerFormat alloc] initWithDefaultComponents:defaultComponents];
}

+ (ORKDateAnswerFormat *)dateTimeAnswerFormat {
    return [[ORKDateAnswerFormat alloc] initWithStyle:ORKDateAnswerStyleDateAndTime];
}
+ (ORKDateAnswerFormat *)dateTimeAnswerFormatWithDefaultDate:(NSDate *)defaultDate
                                                 minimumDate:(NSDate *)minimumDate
                                                 maximumDate:(NSDate *)maximumDate
                                                    calendar:(NSCalendar *)calendar {
    return [[ORKDateAnswerFormat alloc] initWithStyle:ORKDateAnswerStyleDateAndTime
                                          defaultDate:defaultDate
                                          minimumDate:minimumDate
                                          maximumDate:maximumDate
                                             calendar:calendar];
}

+ (ORKDateAnswerFormat *)dateAnswerFormat {
    return [[ORKDateAnswerFormat alloc] initWithStyle:ORKDateAnswerStyleDate];
}
+ (ORKDateAnswerFormat *)dateAnswerFormatWithDefaultDate:(NSDate *)defaultDate
                                             minimumDate:(NSDate *)minimumDate
                                             maximumDate:(NSDate *)maximumDate
                                                calendar:(NSCalendar *)calendar  {
    return [[ORKDateAnswerFormat alloc] initWithStyle:ORKDateAnswerStyleDate
                                          defaultDate:defaultDate
                                          minimumDate:minimumDate
                                          maximumDate:maximumDate
                                             calendar:calendar];
}

+ (ORKTextAnswerFormat *)textAnswerFormat {
    return [ORKTextAnswerFormat new];
}

+ (ORKTextAnswerFormat *)textAnswerFormatWithMaximumLength:(NSInteger)maximumLength {
    return [[ORKTextAnswerFormat alloc] initWithMaximumLength:maximumLength];
}

+ (ORKTextAnswerFormat *)textAnswerFormatWithValidationRegularExpression:(NSRegularExpression *)validationRegularExpression
                                                          invalidMessage:(NSString *)invalidMessage {
    return [[ORKTextAnswerFormat alloc] initWithValidationRegularExpression:validationRegularExpression
                                                             invalidMessage:invalidMessage];
}

+ (ORKEmailAnswerFormat *)emailAnswerFormat {
    return [ORKEmailAnswerFormat new];
}

+ (ORKTimeIntervalAnswerFormat *)timeIntervalAnswerFormat {
    return [ORKTimeIntervalAnswerFormat new];
}

+ (ORKTimeIntervalAnswerFormat *)timeIntervalAnswerFormatWithDefaultInterval:(NSTimeInterval)defaultInterval
                                                                        step:(NSInteger)step {
    return [[ORKTimeIntervalAnswerFormat alloc] initWithDefaultInterval:defaultInterval step:step];
}

+ (ORKHeightAnswerFormat *)heightAnswerFormat {
    return [ORKHeightAnswerFormat new];
}

+ (ORKHeightAnswerFormat *)heightAnswerFormatWithMeasurementSystem:(ORKMeasurementSystem)measurementSystem {
    return [[ORKHeightAnswerFormat alloc] initWithMeasurementSystem:measurementSystem];
}

+ (ORKWeightAnswerFormat *)weightAnswerFormat {
    return [ORKWeightAnswerFormat new];
}

+ (ORKWeightAnswerFormat *)weightAnswerFormatWithMeasurementSystem:(ORKMeasurementSystem)measurementSystem {
    return [[ORKWeightAnswerFormat alloc] initWithMeasurementSystem:measurementSystem];
}

+ (ORKWeightAnswerFormat *)weightAnswerFormatWithMeasurementSystem:(ORKMeasurementSystem)measurementSystem
                                                  numericPrecision:(ORKNumericPrecision)numericPrecision {
    return [[ORKWeightAnswerFormat alloc] initWithMeasurementSystem:measurementSystem
                                                   numericPrecision:numericPrecision];
}

+ (ORKWeightAnswerFormat *)weightAnswerFormatWithMeasurementSystem:(ORKMeasurementSystem)measurementSystem
                                                  numericPrecision:(ORKNumericPrecision)numericPrecision
                                                      minimumValue:(double)minimumValue
                                                      maximumValue:(double)maximumValue
                                                      defaultValue:(double)defaultValue {
    return [[ORKWeightAnswerFormat alloc] initWithMeasurementSystem:measurementSystem
                                                   numericPrecision:numericPrecision
                                                       minimumValue:minimumValue
                                                       maximumValue:maximumValue
                                                       defaultValue:defaultValue];
}

+ (ORKLocationAnswerFormat *)locationAnswerFormat {
    return [ORKLocationAnswerFormat new];
}

- (void)validateParameters {
}

+ (BOOL)supportsSecureCoding {
    return YES;
}

- (instancetype)init {
    return [super init];
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super init];
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
}

- (instancetype)copyWithZone:(NSZone *)zone {
    return self;
}

- (BOOL)isEqual:(id)object {
    if ([self class] != [object class]) {
        return NO;
    }
    return YES;
}

- (NSUInteger)hash {
    // Ignore the task reference - it's not part of the content of the step.
    return 0;
}

- (BOOL)isHealthKitAnswerFormat {
    return NO;
}

- (HKObjectType *)healthKitObjectType {
    return nil;
}

- (HKObjectType *)healthKitObjectTypeForAuthorization {
    return nil;
}

- (HKUnit *)healthKitUnit {
    return nil;
}

- (HKUnit *)healthKitUserUnit {
    return nil;
}

- (void)setHealthKitUserUnit:(HKUnit *)unit {
    
}

- (ORKQuestionType)questionType {
    return ORKQuestionTypeNone;
}

- (ORKAnswerFormat *)impliedAnswerFormat {
    return self;
}

- (Class)questionResultClass {
    return [ORKQuestionResult class];
}

- (ORKQuestionResult *)resultWithIdentifier:(NSString *)identifier answer:(id)answer {
    ORKQuestionResult *questionResult = [[[self questionResultClass] alloc] initWithIdentifier:identifier];
    
    /*
     ContinuousScale navigation rules always evaluate to false because the result is different from what is displayed in the UI.
     The fraction digits have to be taken into account in self.answer as well.
     */
    if ([self isKindOfClass:[ORKContinuousScaleAnswerFormat class]]) {
        NSNumberFormatter* formatter = [(ORKContinuousScaleAnswerFormat*)self numberFormatter];
        answer = [formatter numberFromString:[formatter stringFromNumber:answer]];
    }
    
    questionResult.answer = answer;
    questionResult.questionType = self.questionType;
    return questionResult;
}

- (BOOL)isAnswerValid:(id)answer {
    ORKAnswerFormat *impliedFormat = [self impliedAnswerFormat];
    return impliedFormat == self ? YES : [impliedFormat isAnswerValid:answer];
}

- (BOOL)isAnswerValidWithString:(NSString *)text {
    ORKAnswerFormat *impliedFormat = [self impliedAnswerFormat];
    return impliedFormat == self ? YES : [impliedFormat isAnswerValidWithString:text];
}

- (NSString *)localizedInvalidValueStringWithAnswerString:(NSString *)text {
    return nil;
}

- (NSString *)stringForAnswer:(id)answer {
    ORKAnswerFormat *impliedFormat = [self impliedAnswerFormat];
    return impliedFormat == self ? nil : [impliedFormat stringForAnswer:answer];
}

@end


#pragma mark - ORKValuePickerAnswerFormat

static void ork_validateChoices(NSArray *choices) {
    const NSInteger ORKAnswerFormatMinimumNumberOfChoices = 1;
    if (choices.count < ORKAnswerFormatMinimumNumberOfChoices) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:[NSString stringWithFormat:@"The number of choices cannot be less than %@.", @(ORKAnswerFormatMinimumNumberOfChoices)]
                                     userInfo:nil];
    }
}

static NSArray *ork_processTextChoices(NSArray<ORKTextChoice *> *textChoices) {
    NSMutableArray<ORKTextChoice *> *choices = [[NSMutableArray alloc] init];
    for (ORKTextChoice *textObject in textChoices) {
        if (![textObject isKindOfClass:[ORKTextChoice class]]) {
            @throw [NSException exceptionWithName:NSGenericException reason:@"The textChoices array should only containt objects of the ORKTextChoice kind." userInfo:@{@"nonConformingObject": textObject}];
        }
        [choices addObject:textObject];
    }
    return choices;
}


@implementation ORKValuePickerAnswerFormat {
    ORKChoiceAnswerFormatHelper *_helper;
    ORKTextChoice *_nullTextChoice;
}

+ (instancetype)new {
    ORKThrowMethodUnavailableException();
}

- (instancetype)init {
    ORKThrowMethodUnavailableException();
}

- (instancetype)initWithTextChoices:(NSArray<ORKTextChoice *> *)textChoices {
    self = [super init];
    if (self) {
        [self commonInitWithTextChoices:textChoices nullChoice:nil];
    }
    return self;
}

- (instancetype)initWithTextChoices:(NSArray<ORKTextChoice *> *)textChoices nullChoice:(ORKTextChoice *)nullChoice {
    self = [super init];
    if (self) {
        [self commonInitWithTextChoices:textChoices nullChoice:nullChoice];
    }
    return self;
}

- (void)commonInitWithTextChoices:(NSArray<ORKTextChoice *> *)textChoices nullChoice:(ORKTextChoice *)nullChoice {
    _textChoices = ork_processTextChoices(textChoices);
    _nullTextChoice = nullChoice;
    _helper = [[ORKChoiceAnswerFormatHelper alloc] initWithAnswerFormat:self];
}


- (void)validateParameters {
    [super validateParameters];
    
    ork_validateChoices(_textChoices);
}

- (id)copyWithZone:(NSZone *)zone {
    __typeof(self) copy = [[[self class] alloc] initWithTextChoices:_textChoices nullChoice:_nullTextChoice];
    return copy;
}

- (BOOL)isEqual:(id)object {
    BOOL isParentSame = [super isEqual:object];
    
    __typeof(self) castObject = object;
    return (isParentSame &&
            ORKEqualObjects(self.textChoices, castObject.textChoices));
}

- (NSUInteger)hash {
    return super.hash ^ _textChoices.hash;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        ORK_DECODE_OBJ_ARRAY(aDecoder, textChoices, ORKTextChoice);
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    [super encodeWithCoder:aCoder];
    ORK_ENCODE_OBJ(aCoder, textChoices);
}

+ (BOOL)supportsSecureCoding {
    return YES;
}

- (ORKTextChoice *)nullTextChoice {
    return _nullTextChoice ?: [ORKTextChoice choiceWithText:ORKLocalizedString(@"NULL_ANSWER", nil) value:ORKNullAnswerValue()];
}

- (void)setNullTextChoice:(ORKTextChoice *)nullChoice {
    _nullTextChoice = nullChoice;
}

- (Class)questionResultClass {
    return [ORKChoiceQuestionResult class];
}

- (ORKQuestionType)questionType {
    return ORKQuestionTypeSingleChoice;
}

- (NSString *)stringForAnswer:(id)answer {
    return [_helper stringForChoiceAnswer:answer];
}

@end


#pragma mark - ORKMultipleValuePickerAnswerFormat

@implementation ORKMultipleValuePickerAnswerFormat

+ (instancetype)new {
    ORKThrowMethodUnavailableException();
}

- (instancetype)init {
    ORKThrowMethodUnavailableException();
}

- (instancetype)initWithValuePickers:(NSArray<ORKValuePickerAnswerFormat *> *)valuePickers {
    return [self initWithValuePickers:valuePickers separator:@" "];
}

- (instancetype)initWithValuePickers:(NSArray<ORKValuePickerAnswerFormat *> *)valuePickers separator:(NSString *)separator {
    self = [super init];
    if (self) {
        for (ORKValuePickerAnswerFormat *valuePicker in valuePickers) {
            // Do not show placeholder text for multiple component picker
            [valuePicker setNullTextChoice: [ORKTextChoice choiceWithText:@"" value:ORKNullAnswerValue()]];
        }
        _valuePickers = ORKArrayCopyObjects(valuePickers);
        _separator = [separator copy];
    }
    return self;
}

- (void)validateParameters {
    [super validateParameters];
    for (ORKValuePickerAnswerFormat *valuePicker in self.valuePickers) {
        [valuePicker validateParameters];
    }
}

- (id)copyWithZone:(NSZone *)zone {
    __typeof(self) copy = [[[self class] alloc] initWithValuePickers:self.valuePickers separator:self.separator];
    return copy;
}

- (BOOL)isEqual:(id)object {
    BOOL isParentSame = [super isEqual:object];
    
    __typeof(self) castObject = object;
    return (isParentSame &&
            ORKEqualObjects(self.valuePickers, castObject.valuePickers));
}

- (NSUInteger)hash {
    return super.hash ^ self.valuePickers.hash ^ self.separator.hash;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        ORK_DECODE_OBJ_ARRAY(aDecoder, valuePickers, ORKValuePickerAnswerFormat);
        ORK_DECODE_OBJ_CLASS(aDecoder, separator, NSString);
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    [super encodeWithCoder:aCoder];
    ORK_ENCODE_OBJ(aCoder, valuePickers);
    ORK_ENCODE_OBJ(aCoder, separator);
}

+ (BOOL)supportsSecureCoding {
    return YES;
}

- (Class)questionResultClass {
    return [ORKMultipleComponentQuestionResult class];
}

- (ORKQuestionResult *)resultWithIdentifier:(NSString *)identifier answer:(id)answer {
    ORKQuestionResult *questionResult = [super resultWithIdentifier:identifier answer:answer];
    if ([questionResult isKindOfClass:[ORKMultipleComponentQuestionResult class]]) {
        ((ORKMultipleComponentQuestionResult*)questionResult).separator = self.separator;
    }
    return questionResult;
}

- (ORKQuestionType)questionType {
    return ORKQuestionTypeMultiplePicker;
}

- (NSString *)stringForAnswer:(id)answer {
    if (![answer isKindOfClass:[NSArray class]] || ([(NSArray*)answer count] != self.valuePickers.count)) {
        return nil;
    }
    
    NSArray *answers = (NSArray*)answer;
    __block NSMutableArray <NSString *> *answerTexts = [NSMutableArray new];
    [answers enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSString *text = [self.valuePickers[idx] stringForAnswer:obj];
        if (text != nil) {
            [answerTexts addObject:text];
        } else {
            *stop = YES;
        }
    }];
    
    if (answerTexts.count != self.valuePickers.count) {
        return nil;
    }
    
    return [answerTexts componentsJoinedByString:self.separator];
}

@end


#pragma mark - ORKImageChoiceAnswerFormat

@interface ORKImageChoiceAnswerFormat () {
    ORKChoiceAnswerFormatHelper *_helper;
    
}

@end


@implementation ORKImageChoiceAnswerFormat

+ (instancetype)new {
    ORKThrowMethodUnavailableException();
}

- (instancetype)init {
    ORKThrowMethodUnavailableException();
}

- (instancetype)initWithImageChoices:(NSArray<ORKImageChoice *> *)imageChoices {
    self = [self initWithImageChoices:imageChoices style:ORKChoiceAnswerStyleSingleChoice vertical:NO];
    return self;
}

- (instancetype)initWithImageChoices:(NSArray<ORKImageChoice *> *)imageChoices
                               style:(ORKChoiceAnswerStyle)style
                            vertical:(BOOL)vertical {
    self = [super init];
    if (self) {
        NSMutableArray *choices = [[NSMutableArray alloc] init];
        
        for (NSObject *obj in imageChoices) {
            if ([obj isKindOfClass:[ORKImageChoice class]]) {
                
                [choices addObject:obj];
                
            } else {
                @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Options should be instances of ORKImageChoice" userInfo:@{ @"option": obj }];
            }
        }
        _imageChoices = choices;
        _style = style;
        _vertical = vertical;
        _helper = [[ORKChoiceAnswerFormatHelper alloc] initWithAnswerFormat:self];
    }
    return self;
}

- (void)validateParameters {
    [super validateParameters];
    
    ork_validateChoices(_imageChoices);
}

- (BOOL)isEqual:(id)object {
    BOOL isParentSame = [super isEqual:object];
    
    __typeof(self) castObject = object;
    return (isParentSame &&
            ORKEqualObjects(self.imageChoices, castObject.imageChoices) &&
            (_style == castObject.style) &&
            (_vertical == castObject.vertical));
}

- (NSUInteger)hash {
    return super.hash ^ self.imageChoices.hash ^ _style ^ _vertical;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        ORK_DECODE_OBJ_ARRAY(aDecoder, imageChoices, ORKImageChoice);
        ORK_DECODE_ENUM(aDecoder, style);
        ORK_DECODE_BOOL(aDecoder, vertical);
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    [super encodeWithCoder:aCoder];
    ORK_ENCODE_OBJ(aCoder, imageChoices);
    ORK_ENCODE_ENUM(aCoder, style);
    ORK_ENCODE_BOOL(aCoder, vertical);
}

+ (BOOL)supportsSecureCoding {
    return YES;
}

- (ORKQuestionType)questionType {
    return (_style == ORKChoiceAnswerStyleSingleChoice) ? ORKQuestionTypeSingleChoice : ORKQuestionTypeMultipleChoice;
}

- (Class)questionResultClass {
    return [ORKChoiceQuestionResult class];
}

- (NSString *)stringForAnswer:(id)answer {
    return [_helper stringForChoiceAnswer:answer];
}

@end


#pragma mark - ORKTextChoiceAnswerFormat

@interface ORKTextChoiceAnswerFormat () {
    
    ORKChoiceAnswerFormatHelper *_helper;
}

@end


@implementation ORKTextChoiceAnswerFormat

+ (instancetype)new {
    ORKThrowMethodUnavailableException();
}

- (instancetype)init {
    ORKThrowMethodUnavailableException();
}

- (instancetype)initWithStyle:(ORKChoiceAnswerStyle)style
                  textChoices:(NSArray<ORKTextChoice *> *)textChoices {
    self = [super init];
    if (self) {
        _style = style;
        _textChoices = ork_processTextChoices(textChoices);
        _helper = [[ORKChoiceAnswerFormatHelper alloc] initWithAnswerFormat:self];
    }
    return self;
}

- (void)validateParameters {
    [super validateParameters];
    
    ork_validateChoices(_textChoices);
}

- (BOOL)isEqual:(id)object {
    BOOL isParentSame = [super isEqual:object];
    
    __typeof(self) castObject = object;
    return (isParentSame &&
            ORKEqualObjects(self.textChoices, castObject.textChoices) &&
            (_style == castObject.style));
}

- (NSUInteger)hash {
    return super.hash ^ _textChoices.hash ^ _style;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        ORK_DECODE_OBJ_ARRAY(aDecoder, textChoices, ORKTextChoice);
        ORK_DECODE_ENUM(aDecoder, style);
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    [super encodeWithCoder:aCoder];
    ORK_ENCODE_OBJ(aCoder, textChoices);
    ORK_ENCODE_ENUM(aCoder, style);
    
}

+ (BOOL)supportsSecureCoding {
    return YES;
}
//new
- (ORKQuestionType)questionType {
    return (_style == ORKChoiceAnswerStyleSingleChoice) ? ORKQuestionTypeSingleChoice : ORKQuestionTypeMultipleChoice;
}

- (Class)questionResultClass {
    return [ORKChoiceQuestionResult class];
}

- (NSString *)stringForAnswer:(id)answer {
    return [_helper stringForChoiceAnswer:answer];
}

@end


#pragma mark - ORKTextChoice

@implementation ORKTextChoice {
    NSString *_text;
    id<NSCopying, NSCoding, NSObject> _value;
}

+ (instancetype)new {
    ORKThrowMethodUnavailableException();
}

- (instancetype)init {
    ORKThrowMethodUnavailableException();
}

+ (instancetype)choiceWithText:(NSString *)text primaryTextAttributedString:(NSAttributedString *)primaryTextAttributedString detailText:(NSString *)detailText detailTextAttributedString:(NSAttributedString *)detailTextAttributedString value:(id<NSCopying,NSCoding,NSObject>)value exclusive:(BOOL)exclusive {
    return [[ORKTextChoice alloc] initWithText:text primaryTextAttributedString:primaryTextAttributedString detailText:detailText detailTextAttributedString:detailTextAttributedString value:value exclusive:exclusive];
}

+ (instancetype)choiceWithText:(NSString *)text detailText:(NSString *)detailText value:(id<NSCopying, NSCoding, NSObject>)value exclusive:(BOOL)exclusive {
    ORKTextChoice *option = [[ORKTextChoice alloc] initWithText:text detailText:detailText value:value exclusive:exclusive];
    return option;
}

+ (instancetype)choiceWithText:(NSString *)text value:(id<NSCopying, NSCoding, NSObject>)value {
    return [ORKTextChoice choiceWithText:text detailText:nil value:value exclusive:NO];
}

+ (instancetype)choiceWithTextExclusive:(NSString *)text value:(id<NSCopying, NSCoding, NSObject>)value {
    return [ORKTextChoice choiceWithText:text detailText:nil value:value exclusive:YES];
}

- (instancetype)initWithText:(NSString *)text detailText:(NSString *)detailText value:(id<NSCopying,NSCoding,NSObject>)value exclusive:(BOOL)exclusive {
    return [self initWithText:text primaryTextAttributedString:nil detailText:detailText detailTextAttributedString:nil value:value exclusive:exclusive];
}

- (instancetype)initWithText:(NSString *)text primaryTextAttributedString:(NSAttributedString *)primaryTextAttributedString detailText:(NSString *)detailText detailTextAttributedString:(NSAttributedString *)detailTextAttributedString value:(id<NSCopying,NSCoding,NSObject>)value exclusive:(BOOL)exclusive {
    if (!text && !primaryTextAttributedString && !detailText && !detailTextAttributedString) {
        NSAssert(NO, @"All the input parameters cannot be nil, please provide at least one non-nil value.");
    }
    self = [super init];
    if (self) {
        _text = [text copy];
        _primaryTextAttributedString = [primaryTextAttributedString copy];
        _detailText = [detailText copy];
        _detailTextAttributedString = [detailTextAttributedString copy];
        _value = value;
        _exclusive = exclusive;
    }
    return self;
}

+ (BOOL)supportsSecureCoding {
    return YES;
}

- (instancetype)copyWithZone:(NSZone *)zone {
    return self;
}

- (BOOL)isEqual:(id)object {
    if ([self class] != [object class]) {
        return NO;
    }
    
    // Ignore the task reference - it's not part of the content of the step
    __typeof(self) castObject = object;
    return (ORKEqualObjects(self.text, castObject.text)
            && ORKEqualObjects(self.primaryTextAttributedString, castObject.primaryTextAttributedString)
            && ORKEqualObjects(self.detailText, castObject.detailText)
            && ORKEqualObjects(self.detailTextAttributedString, castObject.detailTextAttributedString)
            && ORKEqualObjects(self.value, castObject.value)
            && self.exclusive == castObject.exclusive);
}

- (NSUInteger)hash {
    // Ignore the task reference - it's not part of the content of the step
    return _text.hash ^ _primaryTextAttributedString.hash ^ _detailText.hash ^ _detailTextAttributedString.hash ^ _value.hash;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super init];
    if (self) {
        ORK_DECODE_OBJ_CLASS(aDecoder, text, NSString);
        ORK_DECODE_OBJ_CLASS(aDecoder, primaryTextAttributedString, NSAttributedString);
        ORK_DECODE_OBJ_CLASS(aDecoder, detailText, NSString);
        ORK_DECODE_OBJ_CLASS(aDecoder, detailTextAttributedString, NSAttributedString);
        ORK_DECODE_OBJ(aDecoder, value);
        ORK_DECODE_BOOL(aDecoder, exclusive);
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    ORK_ENCODE_OBJ(aCoder, text);
    ORK_ENCODE_OBJ(aCoder, primaryTextAttributedString);
    ORK_ENCODE_OBJ(aCoder, detailText);
    ORK_ENCODE_OBJ(aCoder, detailTextAttributedString);
    ORK_ENCODE_OBJ(aCoder, value);
    ORK_ENCODE_BOOL(aCoder, exclusive);
}

@end


#pragma mark - ORKImageChoice

@implementation ORKImageChoice {
    NSString *_text;
    id<NSCopying, NSCoding, NSObject> _value;
}

+ (instancetype)choiceWithNormalImage:(UIImage *)normal selectedImage:(UIImage *)selected text:(NSString *)text value:(id<NSCopying, NSCoding, NSObject>)value {
    return [[ORKImageChoice alloc] initWithNormalImage:normal selectedImage:selected text:text value:value];
}

+ (instancetype)new {
    ORKThrowMethodUnavailableException();
}

- (instancetype)init {
    ORKThrowMethodUnavailableException();
}

- (instancetype)initWithNormalImage:(UIImage *)normal selectedImage:(UIImage *)selected text:(NSString *)text value:(id<NSCopying,NSCoding,NSObject>)value {
    self = [super init];
    if (self) {
        _text = [text copy];
        _value = value;
        _normalStateImage = normal;
        _selectedStateImage = selected;
    }
    return self;
}

+ (BOOL)supportsSecureCoding {
    return YES;
}

- (instancetype)copyWithZone:(NSZone *)zone {
    return self;
}

- (NSString *)text {
    return _text;
}

- (id<NSCopying, NSCoding>)value {
    return _value;
}

- (BOOL)isEqual:(id)object {
    if ([self class] != [object class]) {
        return NO;
    }
    
    // Ignore the task reference - it's not part of the content of the step.
    
    __typeof(self) castObject = object;
    return (ORKEqualObjects(self.text, castObject.text)
            && ORKEqualObjects(self.value, castObject.value)
            && ORKEqualObjects(self.normalStateImage, castObject.normalStateImage)
            && ORKEqualObjects(self.selectedStateImage, castObject.selectedStateImage));
}

- (NSUInteger)hash {
    // Ignore the task reference - it's not part of the content of the step.
    return _text.hash ^ _value.hash;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super init];
    if (self) {
        ORK_DECODE_OBJ_CLASS(aDecoder, text, NSString);
        ORK_DECODE_OBJ(aDecoder, value);
        ORK_DECODE_IMAGE(aDecoder, normalStateImage);
        ORK_DECODE_IMAGE(aDecoder, selectedStateImage);
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    ORK_ENCODE_OBJ(aCoder, text);
    ORK_ENCODE_OBJ(aCoder, value);
    ORK_ENCODE_IMAGE(aCoder, normalStateImage);
    ORK_ENCODE_IMAGE(aCoder, selectedStateImage);
}

@end


#pragma mark - ORKBooleanAnswerFormat

@implementation ORKBooleanAnswerFormat

- (instancetype)initWithYesString:(NSString *)yes noString:(NSString *)no {
    self = [super init];
    if (self) {
        _yes = [yes copy];
        _no = [no copy];
    }
    return self;
}

- (ORKQuestionType)questionType {
    return ORKQuestionTypeBoolean;
}

- (ORKAnswerFormat *)impliedAnswerFormat {
  if (!_yes.length) {
      _yes = ORKLocalizedString(@"BOOL_YES", nil);
  }
  if (!_no.length) {
      _no = ORKLocalizedString(@"BOOL_NO", nil);
  }
  
  return [ORKAnswerFormat choiceAnswerFormatWithStyle:ORKChoiceAnswerStyleMultipleChoice
                                          textChoices:@[
    [ORKTextChoice choiceWithText:_yes detailText:@("") value:@(YES) exclusive:(YES)],
    [ORKTextChoice choiceWithText:_no detailText:@("") value:@(NO) exclusive:(YES)]
                                                        ]];
}

- (Class)questionResultClass {
    return [ORKBooleanQuestionResult class];
}

- (NSString *)stringForAnswer:(id)answer {
    return [self.impliedAnswerFormat stringForAnswer: @[answer]];
}

+ (BOOL)supportsSecureCoding {
    return YES;
}

- (instancetype)copyWithZone:(NSZone *)zone {
    ORKBooleanAnswerFormat *answerFormat = [super copyWithZone:zone];
    answerFormat->_yes = [_yes copy];
    answerFormat->_no = [_no copy];
    return answerFormat;
}

- (NSUInteger)hash {
    return super.hash ^ _yes.hash ^ _no.hash;
}

- (BOOL)isEqual:(id)object {
    BOOL isParentSame = [super isEqual:object];
    
    __typeof(self) castObject = object;
    return (isParentSame &&
            ORKEqualObjects(self.yes, castObject.yes) &&
            ORKEqualObjects(self.no, castObject.no));
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super init];
    if (self) {
        ORK_DECODE_OBJ_CLASS(aDecoder, yes, NSString);
        ORK_DECODE_OBJ_CLASS(aDecoder, no, NSString);
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    [super encodeWithCoder:aCoder];
    ORK_ENCODE_OBJ(aCoder, yes);
    ORK_ENCODE_OBJ(aCoder, no);
}

@end


#pragma mark - ORKTimeOfDayAnswerFormat

@implementation ORKTimeOfDayAnswerFormat

- (instancetype)init {
    self = [self initWithDefaultComponents:nil];
    return self;
}

- (instancetype)initWithDefaultComponents:(NSDateComponents *)defaultComponents {
    self = [super init];
    if (self) {
        _defaultComponents = [defaultComponents copy];
        _minuteInterval = 1;
    }
    return self;
}

- (ORKQuestionType)questionType {
    return ORKQuestionTypeTimeOfDay;
}

- (Class)questionResultClass {
    return [ORKTimeOfDayQuestionResult class];
}

- (NSDate *)pickerDefaultDate {
    
    if (self.defaultComponents) {
        return ORKTimeOfDayDateFromComponents(self.defaultComponents);
    }
    
    NSDateComponents *dateComponents = [[NSCalendar currentCalendar] componentsInTimeZone:[NSTimeZone systemTimeZone] fromDate:[NSDate date]];
    NSDateComponents *newDateComponents = [[NSDateComponents alloc] init];
    newDateComponents.calendar = ORKTimeOfDayReferenceCalendar();
    newDateComponents.hour = dateComponents.hour;
    newDateComponents.minute = dateComponents.minute;
    
    return ORKTimeOfDayDateFromComponents(newDateComponents);
}

- (BOOL)isEqual:(id)object {
    BOOL isParentSame = [super isEqual:object];
    
    __typeof(self) castObject = object;
    return (isParentSame &&
            ORKEqualObjects(self.defaultComponents, castObject.defaultComponents) &&
            (self.minuteInterval == castObject.minuteInterval));
}

- (NSUInteger)hash {
    // Don't bother including everything
    return super.hash & self.defaultComponents.hash;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        ORK_DECODE_OBJ_CLASS(aDecoder, defaultComponents, NSDateComponents);
        ORK_DECODE_INTEGER(aDecoder, minuteInterval);
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    [super encodeWithCoder:aCoder];
    ORK_ENCODE_OBJ(aCoder, defaultComponents);
    ORK_ENCODE_INTEGER(aCoder, minuteInterval);
}

+ (BOOL)supportsSecureCoding {
    return YES;
}

- (NSString *)stringForAnswer:(id)answer {
    return ORKTimeOfDayStringFromComponents(answer);
}

@end


#pragma mark - ORKDateAnswerFormat

@implementation ORKDateAnswerFormat

- (Class)questionResultClass {
    return [ORKDateQuestionResult class];
}

+ (instancetype)new {
    ORKThrowMethodUnavailableException();
}

- (instancetype)init {
    ORKThrowMethodUnavailableException();
}

- (instancetype)initWithStyle:(ORKDateAnswerStyle)style {
    self = [self initWithStyle:style defaultDate:nil minimumDate:nil maximumDate:nil calendar:nil];
    return self;
}

- (instancetype)initWithStyle:(ORKDateAnswerStyle)style
                  defaultDate:(NSDate *)defaultDate
                  minimumDate:(NSDate *)minimum
                  maximumDate:(NSDate *)maximum
                     calendar:(NSCalendar *)calendar {
    self = [super init];
    if (self) {
        _style = style;
        _defaultDate = [defaultDate copy];
        _minimumDate = [minimum copy];
        _maximumDate = [maximum copy];
        _calendar = [calendar copy];
        _minuteInterval = 1;
    }
    return self;
}

- (BOOL)isEqual:(id)object {
    BOOL isParentSame = [super isEqual:object];
    
    __typeof(self) castObject = object;
    return (isParentSame &&
            ORKEqualObjects(self.defaultDate, castObject.defaultDate) &&
            ORKEqualObjects(self.minimumDate, castObject.minimumDate) &&
            ORKEqualObjects(self.maximumDate, castObject.maximumDate) &&
            ORKEqualObjects(self.calendar, castObject.calendar) &&
            (self.minuteInterval == castObject.minuteInterval) &&
            (_style == castObject.style));
}

- (NSUInteger)hash {
    // Don't bother including everything - style is the main item.
    return ([super hash] & [self.defaultDate hash]) ^ _style;
}

- (NSCalendar *)currentCalendar {
    return (_calendar ? : [NSCalendar currentCalendar]);
}

- (NSDateFormatter *)resultDateFormatter {
    NSDateFormatter *dfm = nil;
    switch (self.questionType) {
        case ORKQuestionTypeDate: {
            dfm = ORKResultDateFormatter();
            break;
        }
        case ORKQuestionTypeTimeOfDay: {
            dfm = ORKResultTimeFormatter();
            break;
        }
        case ORKQuestionTypeDateAndTime: {
            dfm = ORKResultDateTimeFormatter();
            break;
        }
        default:
            break;
    }
    dfm = [dfm copy];
    dfm.calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
    return dfm;
}

- (NSString *)stringFromDate:(NSDate *)date {
    NSDateFormatter *dfm = [self resultDateFormatter];
    return [dfm stringFromDate:date];
}

- (NSDate *)dateFromString:(NSString *)string {
    NSDateFormatter *dfm = [self resultDateFormatter];
    return [dfm dateFromString:string];
}

- (NSDate *)pickerDefaultDate {
    return (self.defaultDate ? : [NSDate date]);
    
}

- (NSDate *)pickerMinimumDate {
    return self.minimumDate;
}

- (NSDate *)pickerMaximumDate {
    return self.maximumDate;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        ORK_DECODE_ENUM(aDecoder, style);
        ORK_DECODE_OBJ_CLASS(aDecoder, minimumDate, NSDate);
        ORK_DECODE_OBJ_CLASS(aDecoder, maximumDate, NSDate);
        ORK_DECODE_OBJ_CLASS(aDecoder, defaultDate, NSDate);
        ORK_DECODE_OBJ_CLASS(aDecoder, calendar, NSCalendar);
        ORK_DECODE_INTEGER(aDecoder, minuteInterval);
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    [super encodeWithCoder:aCoder];
    ORK_ENCODE_ENUM(aCoder, style);
    ORK_ENCODE_OBJ(aCoder, minimumDate);
    ORK_ENCODE_OBJ(aCoder, maximumDate);
    ORK_ENCODE_OBJ(aCoder, defaultDate);
    ORK_ENCODE_OBJ(aCoder, calendar);
    ORK_ENCODE_INTEGER(aCoder, minuteInterval);
}

- (ORKQuestionType)questionType {
    return (_style == ORKDateAnswerStyleDateAndTime) ? ORKQuestionTypeDateAndTime : ORKQuestionTypeDate;
}

+ (BOOL)supportsSecureCoding {
    return YES;
}

- (NSString *)stringForAnswer:(id)answer {
    return [self stringFromDate:answer];
}

@end


#pragma mark - ORKNumericAnswerFormat

@implementation ORKNumericAnswerFormat

- (Class)questionResultClass {
    return [ORKNumericQuestionResult class];
}

+ (instancetype)new {
    ORKThrowMethodUnavailableException();
}

- (instancetype)init {
    ORKThrowMethodUnavailableException();
}

- (instancetype)initWithStyle:(ORKNumericAnswerStyle)style {
    return [self initWithStyle:style unit:nil minimum:nil maximum:nil];
}

- (instancetype)initWithStyle:(ORKNumericAnswerStyle)style
                         unit:(NSString *)unit
                      minimum:(NSNumber *)minimum
                      maximum:(NSNumber *)maximum {
    return [self initWithStyle:style unit:unit minimum:minimum maximum:maximum maximumFractionDigits:nil];
}

- (instancetype)initWithStyle:(ORKNumericAnswerStyle)style
                         unit:(NSString *)unit
                      minimum:(NSNumber *)minimum
                      maximum:(NSNumber *)maximum
        maximumFractionDigits:(NSNumber *)maximumFractionDigits {
    self = [super init];
    if (self) {
        _style = style;
        _unit = [unit copy];
        _minimum = [minimum copy];
        _maximum = [maximum copy];
        _maximumFractionDigits = [maximumFractionDigits copy];
    }
    return self;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        ORK_DECODE_ENUM(aDecoder, style);
        ORK_DECODE_OBJ_CLASS(aDecoder, unit, NSString);
        ORK_DECODE_OBJ_CLASS(aDecoder, minimum, NSNumber);
        ORK_DECODE_OBJ_CLASS(aDecoder, maximum, NSNumber);
        ORK_DECODE_OBJ_CLASS(aDecoder, defaultNumericAnswer, NSNumber);
        ORK_DECODE_OBJ_CLASS(aDecoder, maximumFractionDigits, NSNumber);
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    [super encodeWithCoder:aCoder];
    ORK_ENCODE_ENUM(aCoder, style);
    ORK_ENCODE_OBJ(aCoder, unit);
    ORK_ENCODE_OBJ(aCoder, minimum);
    ORK_ENCODE_OBJ(aCoder, maximum);
    ORK_ENCODE_OBJ(aCoder, defaultNumericAnswer);
    ORK_ENCODE_OBJ(aCoder, maximumFractionDigits);
}

+ (BOOL)supportsSecureCoding {
    return YES;
}

- (instancetype)copyWithZone:(NSZone *)zone {
    ORKNumericAnswerFormat *answerFormat = [[[self class] allocWithZone:zone] initWithStyle:_style
                                                                                       unit:[_unit copy]
                                                                                    minimum:[_minimum copy]
                                                                                    maximum:[_maximum copy]
                                                                      maximumFractionDigits:[_maximumFractionDigits copy]];
    answerFormat->_defaultNumericAnswer = [_defaultNumericAnswer copy];
    return answerFormat;
}

- (BOOL)isEqual:(id)object {
    BOOL isParentSame = [super isEqual:object];
    
    __typeof(self) castObject = object;
    return (isParentSame &&
            ORKEqualObjects(self.unit, castObject.unit) &&
            ORKEqualObjects(self.minimum, castObject.minimum) &&
            ORKEqualObjects(self.maximum, castObject.maximum) &&
            ORKEqualObjects(self.defaultNumericAnswer, castObject.defaultNumericAnswer) &&
            ORKEqualObjects(self.maximumFractionDigits, castObject.maximumFractionDigits) &&
            (_style == castObject.style));
}

- (NSUInteger)hash {
    // Don't bother including everything - style is the main item
    return [super hash] ^ ([self.unit hash] & _style);
}

- (instancetype)initWithStyle:(ORKNumericAnswerStyle)style unit:(NSString *)unit {
    return [self initWithStyle:style unit:unit minimum:nil maximum:nil];
}

+ (instancetype)decimalAnswerFormatWithUnit:(NSString *)unit {
    return [[ORKNumericAnswerFormat alloc] initWithStyle:ORKNumericAnswerStyleDecimal unit:unit];
}

+ (instancetype)integerAnswerFormatWithUnit:(NSString *)unit {
    return [[ORKNumericAnswerFormat alloc] initWithStyle:ORKNumericAnswerStyleInteger unit:unit];
}

- (ORKQuestionType)questionType {
    return _style == ORKNumericAnswerStyleDecimal ? ORKQuestionTypeDecimal : ORKQuestionTypeInteger;
    
}

- (BOOL)isAnswerValid:(id)answer {
    BOOL isValid = NO;
    if ([answer isKindOfClass:[NSNumber class]]) {
        return [self isAnswerValidWithNumber:(NSNumber *)answer];
    }
    return isValid;
}

- (BOOL)isAnswerValidWithNumber:(NSNumber *)number {
    BOOL isValid = NO;
    if (number) {
        isValid = YES;
        if (isnan(number.doubleValue)) {
            isValid = NO;
        } else if (self.minimum && (self.minimum.doubleValue > number.doubleValue)) {
            isValid = NO;
        } else if (self.maximum && (self.maximum.doubleValue < number.doubleValue)) {
            isValid = NO;
        }
    }
    return isValid;
}

- (BOOL)isAnswerValidWithString:(NSString *)text {
    BOOL isValid = NO;
    if (text.length > 0) {
        NSDecimalNumber *number = [NSDecimalNumber decimalNumberWithString:text locale:[NSLocale currentLocale]];
        isValid = [self isAnswerValidWithNumber:number];
    }
    return isValid;
}

- (NSString *)localizedInvalidValueStringWithAnswerString:(NSString *)text {
    if (!text.length) {
        return nil;
    }
    NSDecimalNumber *num = [NSDecimalNumber decimalNumberWithString:text locale:[NSLocale currentLocale]];
    if (!num) {
        return nil;
    }
    NSString *string = nil;
    NSNumberFormatter *formatter = ORKDecimalNumberFormatter();
    if (self.minimum && (self.minimum.doubleValue > num.doubleValue)) {
        string = [NSString localizedStringWithFormat:ORKLocalizedString(@"RANGE_ALERT_MESSAGE_BELOW_MAXIMUM", nil), text, [formatter stringFromNumber:self.minimum]];
    } else if (self.maximum && (self.maximum.doubleValue < num.doubleValue)) {
        string = [NSString localizedStringWithFormat:ORKLocalizedString(@"RANGE_ALERT_MESSAGE_ABOVE_MAXIMUM", nil), text, [formatter stringFromNumber:self.maximum]];
    } else {
        string = [NSString localizedStringWithFormat:ORKLocalizedString(@"RANGE_ALERT_MESSAGE_OTHER", nil), text];
    }
    return string;
}

- (NSString *)stringForAnswer:(id)answer {
    NSString *answerString = nil;
    if ([self isAnswerValid:answer]) {
        NSNumberFormatter *formatter = ORKDecimalNumberFormatter();
        answerString = [formatter stringFromNumber:answer];
        if (self.unit && self.unit.length > 0) {
            answerString = [NSString stringWithFormat:@"%@ %@", answerString, self.unit];
        }
    }
    return answerString;
}

#pragma mark - Text Sanitization

- (NSString *)removeDecimalSeparatorsFromText:(NSString *)text numAllowed:(NSInteger)numAllowed separator:(NSString *)decimalSeparator {
    NSMutableString *scanningText = [text mutableCopy];
    NSMutableString *sanitizedText = [[NSMutableString alloc] init];
    BOOL finished = NO;
    while (!finished) {
        NSRange range = [scanningText rangeOfString:decimalSeparator];
        if (range.length == 0) {
            // If our range's length is 0, there are no more decimal separators
            [sanitizedText appendString:scanningText];
            finished = YES;
        } else if (numAllowed <= 0) {
            // If we found a decimal separator and no more are allowed, remove the substring
            [scanningText deleteCharactersInRange:range];
        } else {
            NSInteger maxRange = NSMaxRange(range);
            NSString *processedString = [scanningText substringToIndex:maxRange];
            [sanitizedText appendString:processedString];
            [scanningText deleteCharactersInRange:NSMakeRange(0, maxRange)];
            --numAllowed;
        }
    }
    return sanitizedText;
}

- (NSString *)sanitizedTextFieldText:(NSString *)text decimalSeparator:(NSString *)separator {
    NSString *sanitizedText = text;
    if (_style == ORKNumericAnswerStyleDecimal) {
        sanitizedText = [self removeDecimalSeparatorsFromText:text numAllowed:1 separator:(NSString *)separator];
        if (self.maximumFractionDigits) {
            NSArray *components = [sanitizedText componentsSeparatedByString:separator];
            if([components count] >= 2 && [components[1] length] > [self.maximumFractionDigits integerValue]) {
                NSString *rightOfDecimal = [components[1] substringToIndex:[self.maximumFractionDigits integerValue]];
                sanitizedText = [NSString stringWithFormat:@"%@%@%@", components[0], separator, rightOfDecimal];
            }
        }
    } else if (_style == ORKNumericAnswerStyleInteger) {
        sanitizedText = [self removeDecimalSeparatorsFromText:text numAllowed:0 separator:(NSString *)separator];
    }
    return sanitizedText;
}

@end


#pragma mark - ORKScaleAnswerFormat

@implementation ORKScaleAnswerFormat {
    NSNumberFormatter *_numberFormatter;
}

- (Class)questionResultClass {
    return [ORKScaleQuestionResult class];
}

+ (instancetype)new {
    ORKThrowMethodUnavailableException();
}

- (instancetype)init {
    ORKThrowMethodUnavailableException();
}

- (instancetype)initWithMaximumValue:(NSInteger)maximumValue
                        minimumValue:(NSInteger)minimumValue
                        defaultValue:(NSInteger)defaultValue
                                step:(NSInteger)step
                            vertical:(BOOL)vertical
             maximumValueDescription:(nullable NSString *)maximumValueDescription
             minimumValueDescription:(nullable NSString *)minimumValueDescription {
    self = [super init];
    if (self) {
        _minimum = minimumValue;
        _maximum = maximumValue;
        _defaultValue = defaultValue;
        _step = step;
        _vertical = vertical;
        _hideSelectedValue = NO;
        _maximumValueDescription = maximumValueDescription;
        _minimumValueDescription = minimumValueDescription;

        [self validateParameters];
    }
    return self;
}

- (instancetype)initWithMaximumValue:(NSInteger)maximumValue
                        minimumValue:(NSInteger)minimumValue
                        defaultValue:(NSInteger)defaultValue
                                step:(NSInteger)step
                            vertical:(BOOL)vertical {
    return [self initWithMaximumValue:maximumValue
                         minimumValue:minimumValue
                         defaultValue:defaultValue
                                 step:step
                             vertical:vertical
              maximumValueDescription:nil
              minimumValueDescription:nil];
}

- (instancetype)initWithMaximumValue:(NSInteger)maximumValue
                        minimumValue:(NSInteger)minimumValue
                        defaultValue:(NSInteger)defaultValue
                                step:(NSInteger)step {
    return [self initWithMaximumValue:maximumValue
                         minimumValue:minimumValue
                         defaultValue:defaultValue
                                 step:step
                             vertical:NO
              maximumValueDescription:nil
              minimumValueDescription:nil];
}

- (NSNumber *)minimumNumber {
    return @(_minimum);
}
- (NSNumber *)maximumNumber {
    return @(_maximum);
}
- (NSNumber *)defaultAnswer {
    if ( _defaultValue > _maximum || _defaultValue < _minimum) {
        return nil;
    }
    
    NSInteger integer = round( (double)( _defaultValue - _minimum ) / (double)_step ) * _step + _minimum;
    
    return @(integer);
}
- (NSString *)localizedStringForNumber:(NSNumber *)number {
    return [self.numberFormatter stringFromNumber:number];
}

- (NSArray<ORKTextChoice *> *)textChoices {
    return nil;
}

- (NSNumberFormatter *)numberFormatter {
    if (!_numberFormatter) {
        _numberFormatter = [[NSNumberFormatter alloc] init];
        _numberFormatter.numberStyle = NSNumberFormatterDecimalStyle;
        _numberFormatter.locale = [NSLocale autoupdatingCurrentLocale];
        _numberFormatter.maximumFractionDigits = 0;
    }
    return _numberFormatter;
}

- (NSInteger)numberOfSteps {
    return (_maximum - _minimum) / _step;
}

- (NSNumber *)normalizedValueForNumber:(NSNumber *)number {
    return @(number.integerValue);
}

- (void)validateParameters {
    [super validateParameters];
    
    const NSInteger ORKScaleAnswerFormatMinimumStepSize = 1;
    const NSInteger ORKScaleAnswerFormatMinimumStepCount = 1;
    const NSInteger ORKScaleAnswerFormatMaximumStepCount = 13;
    
    const NSInteger ORKScaleAnswerFormatValueLowerbound = -10000;
    const NSInteger ORKScaleAnswerFormatValueUpperbound = 10000;
    
    if (_maximum < _minimum) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"Expect maximumValue larger than minimumValue"] userInfo:nil];
    }
    
    if (_step < ORKScaleAnswerFormatMinimumStepSize) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:[NSString stringWithFormat:@"Expect step value not less than than %@.", @(ORKScaleAnswerFormatMinimumStepSize)]
                                     userInfo:nil];
    }
    
    NSInteger mod = (_maximum - _minimum) % _step;
    if (mod != 0) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"Expect the difference between maximumValue and minimumValue is divisible by step value"] userInfo:nil];
    }
    
    NSInteger steps = (_maximum - _minimum) / _step;
    if (steps < ORKScaleAnswerFormatMinimumStepCount || steps > ORKScaleAnswerFormatMaximumStepCount) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:[NSString stringWithFormat:@"Expect the total number of steps between minimumValue and maximumValue more than %@ and no more than %@.", @(ORKScaleAnswerFormatMinimumStepCount), @(ORKScaleAnswerFormatMaximumStepCount)]
                                     userInfo:nil];
    }
    
    if (_minimum < ORKScaleAnswerFormatValueLowerbound) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:[NSString stringWithFormat:@"minimumValue should not less than %@", @(ORKScaleAnswerFormatValueLowerbound)]
                                     userInfo:nil];
    }
    
    if (_maximum > ORKScaleAnswerFormatValueUpperbound) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:[NSString stringWithFormat:@"maximumValue should not more than %@", @(ORKScaleAnswerFormatValueUpperbound)]
                                     userInfo:nil];
    }
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        ORK_DECODE_INTEGER(aDecoder, maximum);
        ORK_DECODE_INTEGER(aDecoder, minimum);
        ORK_DECODE_INTEGER(aDecoder, step);
        ORK_DECODE_INTEGER(aDecoder, defaultValue);
        ORK_DECODE_BOOL(aDecoder, vertical);
        ORK_DECODE_BOOL(aDecoder, hideSelectedValue);
        ORK_DECODE_OBJ(aDecoder, maximumValueDescription);
        ORK_DECODE_OBJ(aDecoder, minimumValueDescription);
        ORK_DECODE_IMAGE(aDecoder, maximumImage);
        ORK_DECODE_IMAGE(aDecoder, minimumImage);
        ORK_DECODE_OBJ_ARRAY(aDecoder, gradientColors, UIColor);
        ORK_DECODE_OBJ_ARRAY(aDecoder, gradientLocations, NSNumber);
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    [super encodeWithCoder:aCoder];
    ORK_ENCODE_INTEGER(aCoder, maximum);
    ORK_ENCODE_INTEGER(aCoder, minimum);
    ORK_ENCODE_INTEGER(aCoder, step);
    ORK_ENCODE_INTEGER(aCoder, defaultValue);
    ORK_ENCODE_BOOL(aCoder, vertical);
    ORK_ENCODE_BOOL(aCoder, hideSelectedValue);
    ORK_ENCODE_OBJ(aCoder, maximumValueDescription);
    ORK_ENCODE_OBJ(aCoder, minimumValueDescription);
    ORK_ENCODE_IMAGE(aCoder, maximumImage);
    ORK_ENCODE_IMAGE(aCoder, minimumImage);
    ORK_ENCODE_OBJ(aCoder, gradientColors);
    ORK_ENCODE_OBJ(aCoder, gradientLocations);
}

+ (BOOL)supportsSecureCoding {
    return YES;
}

- (BOOL)isEqual:(id)object {
    BOOL isParentSame = [super isEqual:object];
    
    __typeof(self) castObject = object;
    return (isParentSame &&
            (_maximum == castObject.maximum) &&
            (_minimum == castObject.minimum) &&
            (_step == castObject.step) &&
            (_defaultValue == castObject.defaultValue) &&
            ORKEqualObjects(self.maximumValueDescription, castObject.maximumValueDescription) &&
            ORKEqualObjects(self.minimumValueDescription, castObject.minimumValueDescription) &&
            ORKEqualObjects(self.maximumImage, castObject.maximumImage) &&
            ORKEqualObjects(self.minimumImage, castObject.minimumImage) &&
            ORKEqualObjects(self.gradientColors, castObject.gradientColors) &&
            ORKEqualObjects(self.gradientLocations, castObject.gradientLocations));
}

- (ORKQuestionType)questionType {
    return ORKQuestionTypeScale;
}

- (NSString *)stringForAnswer:(id)answer {
    return [self localizedStringForNumber:answer];
}

@end


#pragma mark - ORKContinuousScaleAnswerFormat

@implementation ORKContinuousScaleAnswerFormat {
    NSNumberFormatter *_numberFormatter;
}

- (Class)questionResultClass {
    return [ORKScaleQuestionResult class];
}

+ (instancetype)new {
    ORKThrowMethodUnavailableException();
}

- (instancetype)init {
    ORKThrowMethodUnavailableException();
}

- (instancetype)initWithMaximumValue:(double)maximumValue
                        minimumValue:(double)minimumValue
                        defaultValue:(double)defaultValue
               maximumFractionDigits:(NSInteger)maximumFractionDigits
                            vertical:(BOOL)vertical
             maximumValueDescription:(nullable NSString *)maximumValueDescription
             minimumValueDescription:(nullable NSString *)minimumValueDescription {
    self = [super init];
    if (self) {
        _minimum = minimumValue;
        _maximum = maximumValue;
        _defaultValue = defaultValue;
        _maximumFractionDigits = maximumFractionDigits;
        _vertical = vertical;
        _hideSelectedValue = NO;
        _maximumValueDescription = maximumValueDescription;
        _minimumValueDescription = minimumValueDescription;
        
        [self validateParameters];
    }
    return self;
}

- (instancetype)initWithMaximumValue:(double)maximumValue
                        minimumValue:(double)minimumValue
                        defaultValue:(double)defaultValue
               maximumFractionDigits:(NSInteger)maximumFractionDigits
                            vertical:(BOOL)vertical {
    return [self initWithMaximumValue:maximumValue
                         minimumValue:minimumValue
                         defaultValue:defaultValue
                maximumFractionDigits:maximumFractionDigits
                             vertical:vertical
              maximumValueDescription:nil
              minimumValueDescription:nil];
}

- (instancetype)initWithMaximumValue:(double)maximumValue
                        minimumValue:(double)minimumValue
                        defaultValue:(double)defaultValue
               maximumFractionDigits:(NSInteger)maximumFractionDigits {
    return [self initWithMaximumValue:maximumValue
                         minimumValue:minimumValue
                         defaultValue:defaultValue
                maximumFractionDigits:maximumFractionDigits
                             vertical:NO
              maximumValueDescription:nil
              minimumValueDescription:nil];
}

- (NSNumber *)minimumNumber {
    return @(_minimum);
}
- (NSNumber *)maximumNumber {
    return @(_maximum);
}
- (NSNumber *)defaultAnswer {
    if ( _defaultValue > _maximum || _defaultValue < _minimum) {
        return nil;
    }
    return @(_defaultValue);
}
- (NSString *)localizedStringForNumber:(NSNumber *)number {
    return [self.numberFormatter stringFromNumber:number];
}

- (NSArray<ORKTextChoice *> *)textChoices {
    return nil;
}

- (NSNumberFormatter *)numberFormatter {
    if (!_numberFormatter) {
        _numberFormatter = [[NSNumberFormatter alloc] init];
        _numberFormatter.numberStyle = ORKNumberFormattingStyleConvert(_numberStyle);
        _numberFormatter.maximumFractionDigits = _maximumFractionDigits;
    }
    return _numberFormatter;
}

- (NSInteger)numberOfSteps {
    return 0;
}

- (NSNumber *)normalizedValueForNumber:(NSNumber *)number {
    return number;
}

- (void)validateParameters {
    [super validateParameters];
    
    const double ORKScaleAnswerFormatValueLowerbound = -10000;
    const double ORKScaleAnswerFormatValueUpperbound = 10000;
    
    // Just clamp maximumFractionDigits to be 0-4. This is all aimed at keeping the maximum
    // number of digits down to 6 or less.
    _maximumFractionDigits = MAX(_maximumFractionDigits, 0);
    _maximumFractionDigits = MIN(_maximumFractionDigits, 4);
    
    double effectiveUpperbound = ORKScaleAnswerFormatValueUpperbound * pow(0.1, _maximumFractionDigits);
    double effectiveLowerbound = ORKScaleAnswerFormatValueLowerbound * pow(0.1, _maximumFractionDigits);
    
    if (_maximum <= _minimum) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"Expect maximumValue larger than minimumValue"] userInfo:nil];
    }
    
    if (_minimum < effectiveLowerbound) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:[NSString stringWithFormat:@"minimumValue should not less than %@ with %@ fractional digits", @(effectiveLowerbound), @(_maximumFractionDigits)]
                                     userInfo:nil];
    }
    
    if (_maximum > effectiveUpperbound) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:[NSString stringWithFormat:@"maximumValue should not more than %@ with %@ fractional digits", @(effectiveUpperbound), @(_maximumFractionDigits)]
                                     userInfo:nil];
    }
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        ORK_DECODE_DOUBLE(aDecoder, maximum);
        ORK_DECODE_DOUBLE(aDecoder, minimum);
        ORK_DECODE_DOUBLE(aDecoder, defaultValue);
        ORK_DECODE_INTEGER(aDecoder, maximumFractionDigits);
        ORK_DECODE_BOOL(aDecoder, vertical);
        ORK_DECODE_ENUM(aDecoder, numberStyle);
        ORK_DECODE_BOOL(aDecoder, hideSelectedValue);
        ORK_DECODE_OBJ(aDecoder, maximumValueDescription);
        ORK_DECODE_OBJ(aDecoder, minimumValueDescription);
        ORK_DECODE_IMAGE(aDecoder, maximumImage);
        ORK_DECODE_IMAGE(aDecoder, minimumImage);
        ORK_DECODE_OBJ_ARRAY(aDecoder, gradientColors, UIColor);
        ORK_DECODE_OBJ_ARRAY(aDecoder, gradientLocations, NSNumber);
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    [super encodeWithCoder:aCoder];
    ORK_ENCODE_DOUBLE(aCoder, maximum);
    ORK_ENCODE_DOUBLE(aCoder, minimum);
    ORK_ENCODE_DOUBLE(aCoder, defaultValue);
    ORK_ENCODE_INTEGER(aCoder, maximumFractionDigits);
    ORK_ENCODE_BOOL(aCoder, vertical);
    ORK_ENCODE_BOOL(aCoder, hideSelectedValue);
    ORK_ENCODE_ENUM(aCoder, numberStyle);
    ORK_ENCODE_OBJ(aCoder, maximumValueDescription);
    ORK_ENCODE_OBJ(aCoder, minimumValueDescription);
    ORK_ENCODE_IMAGE(aCoder, maximumImage);
    ORK_ENCODE_IMAGE(aCoder, minimumImage);
    ORK_ENCODE_OBJ(aCoder, gradientColors);
    ORK_ENCODE_OBJ(aCoder, gradientLocations);
}

+ (BOOL)supportsSecureCoding {
    return YES;
}

- (BOOL)isEqual:(id)object {
    BOOL isParentSame = [super isEqual:object];
    
    __typeof(self) castObject = object;
    return (isParentSame &&
            (_maximum == castObject.maximum) &&
            (_minimum == castObject.minimum) &&
            (_defaultValue == castObject.defaultValue) &&
            (_maximumFractionDigits == castObject.maximumFractionDigits) &&
            (_numberStyle == castObject.numberStyle) &&
            ORKEqualObjects(self.maximumValueDescription, castObject.maximumValueDescription) &&
            ORKEqualObjects(self.minimumValueDescription, castObject.minimumValueDescription) &&
            ORKEqualObjects(self.maximumImage, castObject.maximumImage) &&
            ORKEqualObjects(self.minimumImage, castObject.minimumImage) &&
            ORKEqualObjects(self.gradientColors, castObject.gradientColors) &&
            ORKEqualObjects(self.gradientLocations, castObject.gradientLocations));
}

- (ORKQuestionType)questionType {
    return ORKQuestionTypeScale;
}

- (NSString *)stringForAnswer:(id)answer {
    return [self localizedStringForNumber:answer];
}

@end


#pragma mark - ORKTextScaleAnswerFormat

@interface ORKTextScaleAnswerFormat () {
    
    ORKChoiceAnswerFormatHelper *_helper;
}

@end


@implementation ORKTextScaleAnswerFormat {
    NSNumberFormatter *_numberFormatter;
}

- (Class)questionResultClass {
    return [ORKChoiceQuestionResult class];
}

+ (instancetype)new {
    ORKThrowMethodUnavailableException();
}

- (instancetype)init {
    ORKThrowMethodUnavailableException();
}

- (instancetype)initWithTextChoices:(NSArray<ORKTextChoice *> *)textChoices
                       defaultIndex:(NSInteger)defaultIndex
                           vertical:(BOOL)vertical {
    self = [super init];
    if (self) {
        _textChoices = [textChoices copy];
        _defaultIndex = defaultIndex;
        _vertical = vertical;
        _hideSelectedValue = NO;
        _helper = [[ORKChoiceAnswerFormatHelper alloc] initWithAnswerFormat:self];
        
        [self validateParameters];
    }
    return self;
}

- (instancetype)initWithTextChoices:(NSArray<ORKTextChoice *> *)textChoices
                       defaultIndex:(NSInteger)defaultIndex {
    return [self initWithTextChoices:textChoices
                        defaultIndex:defaultIndex
                            vertical:NO];
}

- (NSNumber *)minimumNumber {
    return @(1);
}
- (NSNumber *)maximumNumber {
    return @(_textChoices.count);
}
- (id<NSObject, NSCopying, NSCoding>)defaultAnswer {
    if (_defaultIndex < 0 || _defaultIndex >= _textChoices.count) {
        return nil;
    }
    id<NSCopying, NSCoding, NSObject> value = [self textChoiceAtIndex:_defaultIndex].value;
    return value ? @[value] : nil;
}
- (NSString *)localizedStringForNumber:(NSNumber *)number {
    return [self.numberFormatter stringFromNumber:number];
}
- (NSString *)minimumValueDescription {
    return _textChoices.firstObject.text;
}
- (NSString *)maximumValueDescription {
    return _textChoices.lastObject.text;
}
- (UIImage *)minimumImage {
    return nil;
}
- (UIImage *)maximumImage {
    return nil;
}

- (NSNumberFormatter *)numberFormatter {
    if (!_numberFormatter) {
        _numberFormatter = [[NSNumberFormatter alloc] init];
        _numberFormatter.numberStyle = NSNumberFormatterDecimalStyle;
        _numberFormatter.locale = [NSLocale autoupdatingCurrentLocale];
        _numberFormatter.maximumFractionDigits = 0;
    }
    return _numberFormatter;
}

- (NSInteger)numberOfSteps {
    return _textChoices.count - 1;
}

- (NSNumber *)normalizedValueForNumber:(NSNumber *)number {
    return @([number integerValue]);
}

- (ORKTextChoice *)textChoiceAtIndex:(NSUInteger)index {
    
    if (index >= _textChoices.count) {
        return nil;
    }
    return _textChoices[index];
}

- (ORKTextChoice *)textChoiceForValue:(id<NSCopying, NSCoding, NSObject>)value {
    __block ORKTextChoice *choice = nil;
    
    [_textChoices enumerateObjectsUsingBlock:^(ORKTextChoice * _Nonnull textChoice, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([textChoice.value isEqual:value]) {
            choice = textChoice;
            *stop = YES;
        }
    }];
    
    return choice;
}

- (NSUInteger)textChoiceIndexForValue:(id<NSCopying, NSCoding, NSObject>)value {
    ORKTextChoice *choice = [self textChoiceForValue:value];
    return choice ? [_textChoices indexOfObject:choice] : NSNotFound;
}

- (void)validateParameters {
    [super validateParameters];
    
    if (_textChoices.count < 2) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Must have a minimum of 2 text choices." userInfo:nil];
    } else if (_textChoices.count > 8) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Cannot have more than 8 text choices." userInfo:nil];
    }
    
    ORKValidateArrayForObjectsOfClass(_textChoices, [ORKTextChoice class], @"Text choices must be of class ORKTextChoice.");
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        ORK_DECODE_OBJ_ARRAY(aDecoder, textChoices, ORKTextChoice);
        ORK_DECODE_OBJ_ARRAY(aDecoder, gradientColors, UIColor);
        ORK_DECODE_OBJ_ARRAY(aDecoder, gradientLocations, NSNumber);
        ORK_DECODE_INTEGER(aDecoder, defaultIndex);
        ORK_DECODE_BOOL(aDecoder, vertical);
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    [super encodeWithCoder:aCoder];
    ORK_ENCODE_OBJ(aCoder, textChoices);
    ORK_ENCODE_OBJ(aCoder, gradientColors);
    ORK_ENCODE_OBJ(aCoder, gradientLocations);
    ORK_ENCODE_INTEGER(aCoder, defaultIndex);
    ORK_ENCODE_BOOL(aCoder, vertical);
}

+ (BOOL)supportsSecureCoding {
    return YES;
}

- (BOOL)isEqual:(id)object {
    BOOL isParentSame = [super isEqual:object];
    
    __typeof(self) castObject = object;
    return (isParentSame &&
            ORKEqualObjects(self.textChoices, castObject.textChoices) &&
            (_defaultIndex == castObject.defaultIndex) &&
            (_vertical == castObject.vertical) &&
            ORKEqualObjects(self.gradientColors, castObject.gradientColors) &&
            ORKEqualObjects(self.gradientLocations, castObject.gradientLocations));
}

- (ORKQuestionType)questionType {
    return ORKQuestionTypeScale;
}

- (NSString *)stringForAnswer:(id)answer {
    return [_helper stringForChoiceAnswer:answer];
}

@end


#pragma mark - ORKTextAnswerFormat

@implementation ORKTextAnswerFormat

- (Class)questionResultClass {
    return [ORKTextQuestionResult class];
}

- (void)commonInit {
    _autocapitalizationType = UITextAutocapitalizationTypeSentences;
    _autocorrectionType = UITextAutocorrectionTypeDefault;
    _spellCheckingType = UITextSpellCheckingTypeDefault;
    _keyboardType = UIKeyboardTypeDefault;
    _multipleLines = YES;
}

- (instancetype)initWithMaximumLength:(NSInteger)maximumLength {
    self = [super init];
    if (self) {
        _maximumLength = maximumLength;
        [self commonInit];
    }
    return self;
}

- (instancetype)initWithValidationRegularExpression:(NSRegularExpression *)validationRegularExpression
                                     invalidMessage:(NSString *)invalidMessage {
    self = [super init];
    if (self) {
        _validationRegularExpression = [validationRegularExpression copy];
        _invalidMessage = [invalidMessage copy];
        _maximumLength = 0;
        [self commonInit];
    }
    return self;
}

- (instancetype)init {
    self = [self initWithMaximumLength:0];
    return self;
}

- (ORKQuestionType)questionType {
    return ORKQuestionTypeText;
}

- (void)validateParameters {
    [super validateParameters];
    
    if ( (!self.validationRegularExpression && self.invalidMessage) ||
        (self.validationRegularExpression && !self.invalidMessage) ) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:@"Both regular expression and invalid message properties must be set."
                                     userInfo:nil];
    }
}

- (instancetype)copyWithZone:(NSZone *)zone {
    ORKTextAnswerFormat *answerFormat = [[[self class] allocWithZone:zone] init];
    answerFormat->_maximumLength = _maximumLength;
    answerFormat->_validationRegularExpression = [_validationRegularExpression copy];
    answerFormat->_invalidMessage = [_invalidMessage copy];
    answerFormat->_defaultTextAnswer = [_defaultTextAnswer copy];
    answerFormat->_autocapitalizationType = _autocapitalizationType;
    answerFormat->_autocorrectionType = _autocorrectionType;
    answerFormat->_spellCheckingType = _spellCheckingType;
    answerFormat->_keyboardType = _keyboardType;
    answerFormat->_multipleLines = _multipleLines;
    answerFormat->_secureTextEntry = _secureTextEntry;
    answerFormat->_textContentType = _textContentType;
    
    if (@available(iOS 12.0, *)) {
        answerFormat->_passwordRules = _passwordRules;
    }
    
    return answerFormat;
}

- (BOOL)isAnswerValid:(id)answer {
    BOOL isValid = NO;
    if ([answer isKindOfClass:[NSString class]]) {
        isValid = [self isAnswerValidWithString:(NSString *)answer];
    }
    return isValid;
}

- (BOOL)isAnswerValidWithString:(NSString *)text {
    BOOL isValid = YES;
    if (text && text.length > 0) {
        isValid = ([self isTextLengthValidWithString:text] && [self isTextRegularExpressionValidWithString:text]);
    }
    return isValid;
}

- (BOOL)isTextLengthValidWithString:(NSString *)text {
    return (_maximumLength == 0 || text.length <= _maximumLength);
}

- (BOOL)isTextRegularExpressionValidWithString:(NSString *)text {
    BOOL isValid = YES;
    if (self.validationRegularExpression) {
        NSString *someRegexp = self.validationRegularExpression.pattern;
        NSPredicate *myTest = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", someRegexp];

        if ([myTest evaluateWithObject: text]) {
            return YES;
        } else {
            return NO;
        }
    }
    return isValid;
}

- (NSString *)localizedInvalidValueStringWithAnswerString:(NSString *)text {
    NSString *string = @"";
    if (![self isTextLengthValidWithString:text]) {
        string = [NSString localizedStringWithFormat:ORKLocalizedString(@"TEXT_ANSWER_EXCEEDING_MAX_LENGTH_ALERT_MESSAGE", nil), ORKLocalizedStringFromNumber(@(_maximumLength))];
    }
    if (![self isTextRegularExpressionValidWithString:text]) {
        if (string.length > 0) {
            string = [string stringByAppendingString:@"\n"];
        }
        string = [string stringByAppendingString:[NSString localizedStringWithFormat:ORKLocalizedString(_invalidMessage, nil), text]];
    }
    return string;
}


- (ORKAnswerFormat *)confirmationAnswerFormatWithOriginalItemIdentifier:(NSString *)originalItemIdentifier
                                                           errorMessage:(NSString *)errorMessage {
    
    NSAssert(!self.multipleLines, @"Confirmation Answer Format is not currently defined for ORKTextAnswerFormat with multiple lines.");
    
    ORKTextAnswerFormat *answerFormat = [[ORKConfirmTextAnswerFormat alloc] initWithOriginalItemIdentifier:originalItemIdentifier errorMessage:errorMessage];
    
    // Copy from ORKTextAnswerFormat being confirmed
    answerFormat->_maximumLength = _maximumLength;
    answerFormat->_keyboardType = _keyboardType;
    answerFormat->_multipleLines = _multipleLines;
    answerFormat->_secureTextEntry = _secureTextEntry;
    answerFormat->_autocapitalizationType = _autocapitalizationType;
    answerFormat->_textContentType = _textContentType;
    
    if (@available(iOS 12.0, *)) {
        answerFormat->_passwordRules = _passwordRules;
    }
    
    // Always set to no autocorrection or spell checking
    answerFormat->_autocorrectionType = UITextAutocorrectionTypeNo;
    answerFormat->_spellCheckingType = UITextSpellCheckingTypeNo;
    
    return answerFormat;
}

#pragma mark NSSecureCoding

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        _multipleLines = YES;
        ORK_DECODE_INTEGER(aDecoder, maximumLength);
        ORK_DECODE_OBJ_CLASS(aDecoder, validationRegularExpression, NSRegularExpression);
        ORK_DECODE_OBJ_CLASS(aDecoder, invalidMessage, NSString);
        ORK_DECODE_OBJ_CLASS(aDecoder, defaultTextAnswer, NSString);
        ORK_DECODE_OBJ_CLASS(aDecoder, textContentType, NSString);
        if (@available(iOS 12.0, *)) {
            ORK_DECODE_OBJ_CLASS(aDecoder, passwordRules, UITextInputPasswordRules);
        }
        ORK_DECODE_ENUM(aDecoder, autocapitalizationType);
        ORK_DECODE_ENUM(aDecoder, autocorrectionType);
        ORK_DECODE_ENUM(aDecoder, spellCheckingType);
        ORK_DECODE_ENUM(aDecoder, keyboardType);
        ORK_DECODE_BOOL(aDecoder, multipleLines);
        ORK_DECODE_BOOL(aDecoder, secureTextEntry);
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    [super encodeWithCoder:aCoder];
    ORK_ENCODE_INTEGER(aCoder, maximumLength);
    ORK_ENCODE_OBJ(aCoder, validationRegularExpression);
    ORK_ENCODE_OBJ(aCoder, invalidMessage);
    ORK_ENCODE_OBJ(aCoder, defaultTextAnswer);
    ORK_ENCODE_OBJ(aCoder, textContentType);
    if (@available(iOS 12.0, *)) {
        ORK_ENCODE_OBJ(aCoder, passwordRules);
    }
    ORK_ENCODE_ENUM(aCoder, autocapitalizationType);
    ORK_ENCODE_ENUM(aCoder, autocorrectionType);
    ORK_ENCODE_ENUM(aCoder, spellCheckingType);
    ORK_ENCODE_ENUM(aCoder, keyboardType);
    ORK_ENCODE_BOOL(aCoder, multipleLines);
    ORK_ENCODE_BOOL(aCoder, secureTextEntry);
}

+ (BOOL)supportsSecureCoding {
    return YES;
}

- (BOOL)isEqual:(id)object {
    BOOL isParentSame = [super isEqual:object];
    BOOL equalPasswordRules = YES;
    
    __typeof(self) castObject = object;
    
    if (@available(iOS 12.0, *)) {
        equalPasswordRules = ORKEqualObjects(self.passwordRules, castObject.passwordRules);
    }
    return (isParentSame &&
            (self.maximumLength == castObject.maximumLength &&
             ORKEqualObjects(self.validationRegularExpression, castObject.validationRegularExpression) &&
             ORKEqualObjects(self.invalidMessage, castObject.invalidMessage) &&
             ORKEqualObjects(self.defaultTextAnswer, castObject.defaultTextAnswer) &&
             self.autocapitalizationType == castObject.autocapitalizationType &&
             self.autocorrectionType == castObject.autocorrectionType &&
             self.spellCheckingType == castObject.spellCheckingType &&
             self.keyboardType == castObject.keyboardType &&
             ORKEqualObjects(self.textContentType, castObject.textContentType) &&
             equalPasswordRules &&
             self.multipleLines == castObject.multipleLines) &&
             self.secureTextEntry == castObject.secureTextEntry);
}

static NSString *const kSecureTextEntryEscapeString = @"*";

- (NSString *)stringForAnswer:(id)answer {
    NSString *answerString = nil;
    if ([self isAnswerValid:answer]) {
        answerString = _secureTextEntry ? [@"" stringByPaddingToLength:((NSString *)answer).length withString:kSecureTextEntryEscapeString startingAtIndex:0] : answer;
    }
    return answerString;
}

@end


#pragma mark - ORKEmailAnswerFormat

@implementation ORKEmailAnswerFormat {
    ORKTextAnswerFormat *_impliedAnswerFormat;
}

- (ORKQuestionType)questionType {
    return ORKQuestionTypeText;
}

- (Class)questionResultClass {
    return [ORKTextQuestionResult class];
}

- (ORKAnswerFormat *)impliedAnswerFormat {
    if (!_impliedAnswerFormat) {
        NSRegularExpression *validationRegularExpression =
        [NSRegularExpression regularExpressionWithPattern:EmailValidationRegularExpressionPattern
                                                  options:(NSRegularExpressionOptions)0
                                                    error:nil];
        NSString *invalidMessage = ORKLocalizedString(@"INVALID_EMAIL_ALERT_MESSAGE", nil);
        _impliedAnswerFormat = [ORKTextAnswerFormat textAnswerFormatWithValidationRegularExpression:validationRegularExpression
                                                                                     invalidMessage:invalidMessage];
        _impliedAnswerFormat.keyboardType = UIKeyboardTypeEmailAddress;
        _impliedAnswerFormat.multipleLines = NO;
        _impliedAnswerFormat.spellCheckingType = UITextSpellCheckingTypeNo;
        _impliedAnswerFormat.autocapitalizationType = UITextAutocapitalizationTypeNone;
        _impliedAnswerFormat.autocorrectionType = UITextAutocorrectionTypeNo;
        _impliedAnswerFormat.textContentType = UITextContentTypeEmailAddress;
    }
    return _impliedAnswerFormat;
}

- (NSString *)stringForAnswer:(id)answer {
    return [self.impliedAnswerFormat stringForAnswer:answer];
}

- (void)setUsernameField:(BOOL)usernameField {
    _usernameField = usernameField;
    if ([self.impliedAnswerFormat isMemberOfClass:[ORKTextAnswerFormat class]]) {
        ORKTextAnswerFormat *textFormat = (ORKTextAnswerFormat *)self.impliedAnswerFormat;
        textFormat.textContentType = usernameField ? UITextContentTypeUsername : UITextContentTypeEmailAddress;
    }
}

@end


#pragma mark - ORKConfirmTextAnswerFormat

@implementation ORKConfirmTextAnswerFormat

+ (instancetype)new {
    ORKThrowMethodUnavailableException();
}

// Don't throw on -init nor -initWithMaximumLength: because they're internally used by -copyWithZone:

- (instancetype)initWithValidationRegularExpression:(NSRegularExpression *)validationRegularExpression
                                     invalidMessage:(NSString *)invalidMessage {
    ORKThrowMethodUnavailableException();
}

- (instancetype)initWithOriginalItemIdentifier:(NSString *)originalItemIdentifier
                                  errorMessage:(NSString *)errorMessage {
    
    NSParameterAssert(originalItemIdentifier);
    NSParameterAssert(errorMessage);
    
    self = [super init];
    if (self) {
        _originalItemIdentifier = [originalItemIdentifier copy];
        _errorMessage = [errorMessage copy];
    }
    return self;
}

- (BOOL)isAnswerValid:(id)answer {
    BOOL isValid = NO;
    if ([answer isKindOfClass:[NSString class]]) {
        NSString *stringAnswer = (NSString *)answer;
        isValid = (stringAnswer.length > 0);
    }
    return isValid;
}

- (NSString *)localizedInvalidValueStringWithAnswerString:(NSString *)text {
    return self.errorMessage;
}

- (instancetype)copyWithZone:(NSZone *)zone {
    ORKConfirmTextAnswerFormat *answerFormat = [super copyWithZone:zone];
    answerFormat->_originalItemIdentifier = [_originalItemIdentifier copy];
    answerFormat->_errorMessage = [_errorMessage copy];
    return answerFormat;
}

+ (BOOL)supportsSecureCoding {
    return YES;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        ORK_DECODE_OBJ_CLASS(aDecoder, originalItemIdentifier, NSString);
        ORK_DECODE_OBJ_CLASS(aDecoder, errorMessage, NSString);
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    [super encodeWithCoder:aCoder];
    ORK_ENCODE_OBJ(aCoder, originalItemIdentifier);
    ORK_ENCODE_OBJ(aCoder, errorMessage);
}

- (BOOL)isEqual:(id)object {
    BOOL isParentSame = [super isEqual:object];
    
    __typeof(self) castObject = object;
    return (isParentSame &&
            ORKEqualObjects(self.originalItemIdentifier, castObject.originalItemIdentifier) &&
            ORKEqualObjects(self.errorMessage, castObject.errorMessage));
}

@end


#pragma mark - ORKTimeIntervalAnswerFormat

@implementation ORKTimeIntervalAnswerFormat

- (Class)questionResultClass {
    return [ORKTimeIntervalQuestionResult class];
}

- (instancetype)init {
    self = [self initWithDefaultInterval:0 step:1];
    return self;
}

- (instancetype)initWithDefaultInterval:(NSTimeInterval)defaultInterval step:(NSInteger)step {
    self = [super init];
    if (self) {
        _defaultInterval = defaultInterval;
        _step = step;
        [self validateParameters];
    }
    return self;
}

- (ORKQuestionType)questionType {
    return ORKQuestionTypeTimeInterval;
}

- (NSTimeInterval)pickerDefaultDuration {
    
    NSTimeInterval value = MAX([self defaultInterval], 0);
    
    // imitate UIDatePicker's behavior
    NSTimeInterval stepInSeconds = _step * 60;
    value  = floor(value/stepInSeconds)*stepInSeconds;
    
    return value;
}

- (void)validateParameters {
    [super validateParameters];
    
    const NSInteger ORKTimeIntervalAnswerFormatStepLowerBound = 1;
    const NSInteger ORKTimeIntervalAnswerFormatStepUpperBound = 30;
    
    if (_step < ORKTimeIntervalAnswerFormatStepLowerBound || _step > ORKTimeIntervalAnswerFormatStepUpperBound) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"Step should be between %@ and %@.", @(ORKTimeIntervalAnswerFormatStepLowerBound), @(ORKTimeIntervalAnswerFormatStepUpperBound)] userInfo:nil];
    }
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        ORK_DECODE_DOUBLE(aDecoder, defaultInterval);
        ORK_DECODE_DOUBLE(aDecoder, step);
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    [super encodeWithCoder:aCoder];
    ORK_ENCODE_DOUBLE(aCoder, defaultInterval);
    ORK_ENCODE_DOUBLE(aCoder, step);
}

+ (BOOL)supportsSecureCoding {
    return YES;
}

- (BOOL)isEqual:(id)object {
    BOOL isParentSame = [super isEqual:object];
    
    __typeof(self) castObject = object;
    return (isParentSame &&
            (_defaultInterval == castObject.defaultInterval) &&
            (_step == castObject.step));
}

- (NSString *)stringForAnswer:(id)answer {
    return [ORKTimeIntervalLabelFormatter() stringFromTimeInterval:((NSNumber *)answer).floatValue];
}

@end


#pragma mark - ORKHeightAnswerFormat

@implementation ORKHeightAnswerFormat

- (Class)questionResultClass {
    return [ORKNumericQuestionResult class];
}

- (NSString *)canonicalUnitString {
    return @"cm";
}

- (ORKNumericQuestionResult *)resultWithIdentifier:(NSString *)identifier answer:(NSNumber *)answer {
    ORKNumericQuestionResult *questionResult = (ORKNumericQuestionResult *)[super resultWithIdentifier:identifier answer:answer];
    // Use canonical unit because we expect results to be consistent regardless of the user locale
    questionResult.unit = [self canonicalUnitString];
    return questionResult;
}

- (instancetype)init {
    self = [self initWithMeasurementSystem:ORKMeasurementSystemLocal];
    return self;
}

- (instancetype)initWithMeasurementSystem:(ORKMeasurementSystem)measurementSystem {
    self = [super init];
    if (self) {
        _measurementSystem = measurementSystem;
    }
    return self;
}

- (BOOL)isEqual:(id)object {
    BOOL isParentSame = [super isEqual:object];
    
    __typeof(self) castObject = object;
    return (isParentSame &&
            (self.measurementSystem == castObject.measurementSystem));
}

- (NSUInteger)hash {
    return super.hash ^ _measurementSystem;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        ORK_DECODE_ENUM(aDecoder, measurementSystem);
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    [super encodeWithCoder:aCoder];
    ORK_ENCODE_ENUM(aCoder, measurementSystem);
}

- (ORKQuestionType)questionType {
    return ORKQuestionTypeHeight;
}

+ (BOOL)supportsSecureCoding {
    return YES;
}

- (BOOL)useMetricSystem {
    return _measurementSystem == ORKMeasurementSystemMetric
    || (_measurementSystem == ORKMeasurementSystemLocal && ((NSNumber *)[[NSLocale currentLocale] objectForKey:NSLocaleUsesMetricSystem]).boolValue);
}

- (NSString *)stringForAnswer:(id)answer {
    NSString *answerString = nil;
    
    if (!ORKIsAnswerEmpty(answer)) {
        NSNumberFormatter *formatter = ORKDecimalNumberFormatter();
        if (self.useMetricSystem) {
            answerString = [NSString stringWithFormat:@"%@ %@", [formatter stringFromNumber:answer], ORKLocalizedString(@"MEASURING_UNIT_CM", nil)];
        } else {
            double feet, inches;
            ORKCentimetersToFeetAndInches(((NSNumber *)answer).doubleValue, &feet, &inches);
            NSString *feetString = [formatter stringFromNumber:@(feet)];
            NSString *inchesString = [formatter stringFromNumber:@(inches)];
            answerString = [NSString stringWithFormat:@"%@ %@, %@ %@",
                            feetString, ORKLocalizedString(@"MEASURING_UNIT_FT", nil), inchesString, ORKLocalizedString(@"MEASURING_UNIT_IN", nil)];
        }
    }
    return answerString;
}

@end


#pragma mark - ORKWeightAnswerFormat

@implementation ORKWeightAnswerFormat

- (Class)questionResultClass {
    return [ORKNumericQuestionResult class];
}

- (NSString *)canonicalUnitString {
    return @"kg";
}

- (ORKNumericQuestionResult *)resultWithIdentifier:(NSString *)identifier answer:(NSNumber *)answer {
    ORKNumericQuestionResult *questionResult = (ORKNumericQuestionResult *)[super resultWithIdentifier:identifier answer:answer];
    // Use canonical unit because we expect results to be consistent regardless of the user locale
    questionResult.unit = [self canonicalUnitString];
    return questionResult;
}

- (instancetype)init {
    return [self initWithMeasurementSystem:ORKMeasurementSystemLocal
                          numericPrecision:ORKNumericPrecisionDefault
                              minimumValue:ORKDoubleDefaultValue
                              maximumValue:ORKDoubleDefaultValue
                              defaultValue:ORKDoubleDefaultValue];
}

- (instancetype)initWithMeasurementSystem:(ORKMeasurementSystem)measurementSystem {
    return [self initWithMeasurementSystem:measurementSystem
                          numericPrecision:ORKNumericPrecisionDefault
                              minimumValue:ORKDoubleDefaultValue
                              maximumValue:ORKDoubleDefaultValue
                              defaultValue:ORKDoubleDefaultValue];
}

- (instancetype)initWithMeasurementSystem:(ORKMeasurementSystem)measurementSystem
                         numericPrecision:(ORKNumericPrecision)numericPrecision {
    return [self initWithMeasurementSystem:measurementSystem
                          numericPrecision:numericPrecision
                              minimumValue:ORKDoubleDefaultValue
                              maximumValue:ORKDoubleDefaultValue
                              defaultValue:ORKDoubleDefaultValue];
}

- (instancetype)initWithMeasurementSystem:(ORKMeasurementSystem)measurementSystem
                         numericPrecision:(ORKNumericPrecision)numericPrecision
                             minimumValue:(double)minimumValue
                             maximumValue:(double)maximumValue
                             defaultValue:(double)defaultValue {
    if ((defaultValue != ORKDoubleDefaultValue) && ((defaultValue < minimumValue) || (defaultValue > maximumValue))) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:@"defaultValue must be between minimumValue and maximumValue."
                                     userInfo:nil];
    }
    
    self = [super init];
    if (self) {
        _measurementSystem = measurementSystem;
        _numericPrecision = numericPrecision;
        _minimumValue = minimumValue;
        _maximumValue = maximumValue;
        _defaultValue = defaultValue;
    }
    return self;
}

- (BOOL)isEqual:(id)object {
    BOOL isParentSame = [super isEqual:object];
    
    __typeof(self) castObject = object;
    return (isParentSame &&
            (self.measurementSystem == castObject.measurementSystem) &&
            (self.numericPrecision == castObject.numericPrecision) &&
            (self.minimumValue == castObject.minimumValue) &&
            (self.maximumValue == castObject.maximumValue) &&
            (self.defaultValue == castObject.defaultValue));
}

- (NSUInteger)hash {
    // Ignore minimumValue, maximumValue and defaultValue as they're unimportant
    return super.hash ^ _measurementSystem ^ _numericPrecision;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        ORK_DECODE_ENUM(aDecoder, measurementSystem);
        ORK_DECODE_ENUM(aDecoder, numericPrecision);
        ORK_DECODE_DOUBLE(aDecoder, minimumValue);
        ORK_DECODE_DOUBLE(aDecoder, maximumValue);
        ORK_DECODE_DOUBLE(aDecoder, defaultValue);
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    [super encodeWithCoder:aCoder];
    ORK_ENCODE_ENUM(aCoder, measurementSystem);
    ORK_ENCODE_ENUM(aCoder, numericPrecision);
    ORK_ENCODE_DOUBLE(aCoder, minimumValue);
    ORK_ENCODE_DOUBLE(aCoder, maximumValue);
    ORK_ENCODE_DOUBLE(aCoder, defaultValue);
}

- (ORKQuestionType)questionType {
    return ORKQuestionTypeWeight;
}

+ (BOOL)supportsSecureCoding {
    return YES;
}

- (BOOL)useMetricSystem {
    return _measurementSystem == ORKMeasurementSystemMetric || (_measurementSystem == ORKMeasurementSystemLocal && ((NSNumber *)[[NSLocale currentLocale] objectForKey:NSLocaleUsesMetricSystem]).boolValue);
}

- (NSString *)stringForAnswer:(id)answer {
    NSString *answerString = nil;
    
    if (!ORKIsAnswerEmpty(answer)) {
        NSNumberFormatter *formatter = ORKDecimalNumberFormatter();
        if (self.useMetricSystem) {
            answerString = [NSString stringWithFormat:@"%@ %@", [formatter stringFromNumber:answer], ORKLocalizedString(@"MEASURING_UNIT_KG", nil)];
        } else {
            if (self.numericPrecision != ORKNumericPrecisionHigh) {
                double pounds = ORKKilogramsToPounds(((NSNumber *)answer).doubleValue);
                NSString *poundsString = [formatter stringFromNumber:@(pounds)];
                answerString = [NSString stringWithFormat:@"%@ %@", poundsString, ORKLocalizedString(@"MEASURING_UNIT_LB", nil)];
            } else {
                double pounds, ounces;
                ORKKilogramsToPoundsAndOunces(((NSNumber *)answer).doubleValue, &pounds, &ounces);
                NSString *poundsString = [formatter stringFromNumber:@(pounds)];
                NSString *ouncesString = [formatter stringFromNumber:@(ounces)];
                answerString = [NSString stringWithFormat:@"%@ %@, %@ %@", poundsString, ORKLocalizedString(@"MEASURING_UNIT_LB", nil), ouncesString, ORKLocalizedString(@"MEASURING_UNIT_OZ", nil)];
            }
        }
    }
    return answerString;
}

@end


#pragma mark - ORKLocationAnswerFormat

@implementation ORKLocationAnswerFormat

- (instancetype)init {
    self = [super init];
    if (self) {
        _useCurrentLocation = YES;
    }
    return self;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        ORK_DECODE_BOOL(aDecoder, useCurrentLocation);
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    [super encodeWithCoder:aCoder];
    ORK_ENCODE_BOOL(aCoder, useCurrentLocation);
}

+ (BOOL)supportsSecureCoding {
    return YES;
}

- (ORKQuestionType)questionType {
    return ORKQuestionTypeLocation;
}

- (Class)questionResultClass {
    return [ORKLocationQuestionResult class];
}

- (instancetype)copyWithZone:(NSZone *)zone {
    ORKLocationAnswerFormat *locationAnswerFormat = [[[self class] allocWithZone:zone] init];
    locationAnswerFormat->_useCurrentLocation = _useCurrentLocation;
    return locationAnswerFormat;
}

- (BOOL)isEqual:(id)object {
    BOOL isParentSame = [super isEqual:object];
    
    __typeof(self) castObject = object;
    return (isParentSame &&
            _useCurrentLocation == castObject.useCurrentLocation);
}

static NSString *const formattedAddressLinesKey = @"FormattedAddressLines";

- (NSString *)stringForAnswer:(id)answer {
    NSString *answerString = nil;
    if ([answer isKindOfClass:[ORKLocation class]]) {
        ORKLocation *location = answer;
        // access address dictionary directly since 'ABCreateStringWithAddressDictionary:' is deprecated in iOS9
        NSArray<NSString *> *addressLines = [location.addressDictionary valueForKey:formattedAddressLinesKey];
        answerString = addressLines ? [addressLines componentsJoinedByString:@"\n"] :
        MKStringFromMapPoint(MKMapPointForCoordinate(location.coordinate));
    }
    return answerString;
}

@end
