// Copyright 2008 Cyrus Najmabadi
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#import "ConcreteExtensionField.h"
#import "CodedOutputStream.h"
#import "CodedInputStream.h"
#import "Message_Builder.h"
#import "Message.h"
#import "ExtendableMessage_Builder.h"

@interface PBConcreteExtensionField()
@property PBExtensionType type;
@property Class extendedClass;
@property int32_t fieldNumber;
@property (retain) id defaultValue;
@property Class messageOrGroupClass;
@property BOOL isRepeated;
@property BOOL isPacked;
@property BOOL isMessageSetWireFormat;
@end

@implementation PBConcreteExtensionField

@synthesize type;
@synthesize extendedClass;
@synthesize fieldNumber;
@synthesize defaultValue;
@synthesize messageOrGroupClass;
@synthesize isRepeated;
@synthesize isPacked;
@synthesize isMessageSetWireFormat;

- (void) dealloc {
  self.type = 0;
  self.extendedClass = nil;
  self.fieldNumber = 0;
  self.defaultValue = nil;
  self.messageOrGroupClass = nil;
  self.isRepeated = NO;
  self.isPacked = NO;
  self.isMessageSetWireFormat = NO;
}


- (id)     initWithType:(PBExtensionType) type_
          extendedClass:(Class) extendedClass_
            fieldNumber:(int32_t) fieldNumber_
           defaultValue:(id) defaultValue_
    messageOrGroupClass:(Class) messageOrGroupClass_
             isRepeated:(BOOL) isRepeated_
               isPacked:(BOOL) isPacked_
    isMessageSetWireFormat:(BOOL) isMessageSetWireFormat_ {
  if ((self = [super init])) {
    self.type = type_;
    self.extendedClass = extendedClass_;
    self.fieldNumber = fieldNumber_;
    self.defaultValue = defaultValue_;
    self.messageOrGroupClass = messageOrGroupClass_;
    self.isRepeated = isRepeated_;
    self.isPacked = isPacked_;
    self.isMessageSetWireFormat = isMessageSetWireFormat_;
  }

  return self;
}


+ (PBConcreteExtensionField*) extensionWithType:(PBExtensionType) type
                                extendedClass:(Class) extendedClass
                                  fieldNumber:(int32_t) fieldNumber
                                 defaultValue:(id) defaultValue
                    messageOrGroupClass:(Class) messageOrGroupClass
                                   isRepeated:(BOOL) isRepeated
                                     isPacked:(BOOL) isPacked
                       isMessageSetWireFormat:(BOOL) isMessageSetWireFormat {
  return [[PBConcreteExtensionField alloc] initWithType:type
                                         extendedClass:extendedClass
                                           fieldNumber:fieldNumber
                                          defaultValue:defaultValue
                             messageOrGroupClass:messageOrGroupClass
                                            isRepeated:isRepeated
                                              isPacked:isPacked
                                isMessageSetWireFormat:isMessageSetWireFormat];
}


- (PBWireFormat) wireType {
  if (isPacked) {
    return PBWireFormatLengthDelimited;
  }

  switch (type) {
    case PBExtensionTypeBool:     return PBWireFormatVarint;
    case PBExtensionTypeFixed32:  return PBWireFormatFixed32;
    case PBExtensionTypeSFixed32: return PBWireFormatFixed32;
    case PBExtensionTypeFloat:    return PBWireFormatFixed32;
    case PBExtensionTypeFixed64:  return PBWireFormatFixed64;
    case PBExtensionTypeSFixed64: return PBWireFormatFixed64;
    case PBExtensionTypeDouble:   return PBWireFormatFixed64;
    case PBExtensionTypeInt32:    return PBWireFormatVarint;
    case PBExtensionTypeInt64:    return PBWireFormatVarint;
    case PBExtensionTypeSInt32:   return PBWireFormatVarint;
    case PBExtensionTypeSInt64:   return PBWireFormatVarint;
    case PBExtensionTypeUInt32:   return PBWireFormatVarint;
    case PBExtensionTypeUInt64:   return PBWireFormatVarint;
    case PBExtensionTypeBytes:    return PBWireFormatLengthDelimited;
    case PBExtensionTypeString:   return PBWireFormatLengthDelimited;
    case PBExtensionTypeMessage:  return PBWireFormatLengthDelimited;
    case PBExtensionTypeGroup:    return PBWireFormatStartGroup;
    case PBExtensionTypeEnum:     return PBWireFormatVarint;
  }

  @throw [NSException exceptionWithName:@"InternalError" reason:@"" userInfo:nil];
}


BOOL typeIsFixedSize(PBExtensionType type);
BOOL typeIsFixedSize(PBExtensionType type) {
  switch (type) {
    case PBExtensionTypeBool:
    case PBExtensionTypeFixed32:
    case PBExtensionTypeSFixed32:
    case PBExtensionTypeFloat:
    case PBExtensionTypeFixed64:
    case PBExtensionTypeSFixed64:
    case PBExtensionTypeDouble:
      return YES;
    default:
      return NO;
  }
}


int32_t typeSize(PBExtensionType type);
int32_t typeSize(PBExtensionType type) {
  switch (type) {
    case PBExtensionTypeBool:
      return 1;
    case PBExtensionTypeFixed32:
    case PBExtensionTypeSFixed32:
    case PBExtensionTypeFloat:
      return 4;
    case PBExtensionTypeFixed64:
    case PBExtensionTypeSFixed64:
    case PBExtensionTypeDouble:
      return 8;
    default:
      @throw [NSException exceptionWithName:@"InternalError" reason:@"" userInfo:nil];
  }
}


- (void)           writeSingleValue:(id) value
    includingTagToCodedOutputStream:(PBCodedOutputStream*) output {
  switch (type) {
    case PBExtensionTypeBool:
      [output writeBool:fieldNumber value:[value boolValue]];
      return;
    case PBExtensionTypeFixed32:
      [output writeFixed32:fieldNumber value:[value intValue]];
      return;
    case PBExtensionTypeSFixed32:
      [output writeSFixed32:fieldNumber value:[value intValue]];
      return;
    case PBExtensionTypeFloat:
      [output writeFloat:fieldNumber value:[value floatValue]];
      return;
    case PBExtensionTypeFixed64:
      [output writeFixed64:fieldNumber value:[value longLongValue]];
      return;
    case PBExtensionTypeSFixed64:
      [output writeSFixed64:fieldNumber value:[value longLongValue]];
      return;
    case PBExtensionTypeDouble:
      [output writeDouble:fieldNumber value:[value doubleValue]];
      return;
    case PBExtensionTypeInt32:
      [output writeInt32:fieldNumber value:[value intValue]];
      return;
    case PBExtensionTypeInt64:
      [output writeInt64:fieldNumber value:[value longLongValue]];
      return;
    case PBExtensionTypeSInt32:
      [output writeSInt32:fieldNumber value:[value intValue]];
      return;
    case PBExtensionTypeSInt64:
      [output writeSInt64:fieldNumber value:[value longLongValue]];
      return;
    case PBExtensionTypeUInt32:
      [output writeUInt32:fieldNumber value:[value intValue]];
      return;
    case PBExtensionTypeUInt64:
      [output writeUInt64:fieldNumber value:[value longLongValue]];
      return;
    case PBExtensionTypeBytes:
      [output writeData:fieldNumber value:value];
      return;
    case PBExtensionTypeString:
      [output writeString:fieldNumber value:value];
      return;
    case PBExtensionTypeGroup:
      [output writeGroup:fieldNumber value:value];
      return;
    case PBExtensionTypeEnum:
      [output writeEnum:fieldNumber value:[value intValue]];
      return;
    case PBExtensionTypeMessage:
      if (isMessageSetWireFormat) {
        [output writeMessageSetExtension:fieldNumber value:value];
      } else {
        [output writeMessage:fieldNumber value:value];
      }
      return;
  }

  @throw [NSException exceptionWithName:@"InternalError" reason:@"" userInfo:nil];
}


- (void)           writeSingleValue:(id) value
    noTagToCodedOutputStream:(PBCodedOutputStream*) output {
  switch (type) {
    case PBExtensionTypeBool:
      [output writeBoolNoTag:[value boolValue]];
      return;
    case PBExtensionTypeFixed32:
      [output writeFixed32NoTag:[value intValue]];
      return;
    case PBExtensionTypeSFixed32:
      [output writeSFixed32NoTag:[value intValue]];
      return;
    case PBExtensionTypeFloat:
      [output writeFloatNoTag:[value floatValue]];
      return;
    case PBExtensionTypeFixed64:
      [output writeFixed64NoTag:[value longLongValue]];
      return;
    case PBExtensionTypeSFixed64:
      [output writeSFixed64NoTag:[value longLongValue]];
      return;
    case PBExtensionTypeDouble:
      [output writeDoubleNoTag:[value doubleValue]];
      return;
    case PBExtensionTypeInt32:
      [output writeInt32NoTag:[value intValue]];
      return;
    case PBExtensionTypeInt64:
      [output writeInt64NoTag:[value longLongValue]];
      return;
    case PBExtensionTypeSInt32:
      [output writeSInt32NoTag:[value intValue]];
      return;
    case PBExtensionTypeSInt64:
      [output writeSInt64NoTag:[value longLongValue]];
      return;
    case PBExtensionTypeUInt32:
      [output writeUInt32NoTag:[value intValue]];
      return;
    case PBExtensionTypeUInt64:
      [output writeUInt64NoTag:[value longLongValue]];
      return;
    case PBExtensionTypeBytes:
      [output writeDataNoTag:value];
      return;
    case PBExtensionTypeString:
      [output writeStringNoTag:value];
      return;
    case PBExtensionTypeGroup:
      [output writeGroupNoTag:fieldNumber value:value];
      return;
    case PBExtensionTypeEnum:
      [output writeEnumNoTag:[value intValue]];
      return;
    case PBExtensionTypeMessage:
      [output writeMessageNoTag:value];
      return;
  }

  @throw [NSException exceptionWithName:@"InternalError" reason:@"" userInfo:nil];
}


- (int32_t) computeSingleSerializedSizeNoTag:(id) value {
  switch (type) {
    case PBExtensionTypeBool:     return computeBoolSizeNoTag([value boolValue]);
    case PBExtensionTypeFixed32:  return computeFixed32SizeNoTag([value intValue]);
    case PBExtensionTypeSFixed32: return computeSFixed32SizeNoTag([value intValue]);
    case PBExtensionTypeFloat:    return computeFloatSizeNoTag([value floatValue]);
    case PBExtensionTypeFixed64:  return computeFixed64SizeNoTag([value longLongValue]);
    case PBExtensionTypeSFixed64: return computeSFixed64SizeNoTag([value longLongValue]);
    case PBExtensionTypeDouble:   return computeDoubleSizeNoTag([value doubleValue]);
    case PBExtensionTypeInt32:    return computeInt32SizeNoTag([value intValue]);
    case PBExtensionTypeInt64:    return computeInt64SizeNoTag([value longLongValue]);
    case PBExtensionTypeSInt32:   return computeSInt32SizeNoTag([value intValue]);
    case PBExtensionTypeSInt64:   return computeSInt64SizeNoTag([value longLongValue]);
    case PBExtensionTypeUInt32:   return computeUInt32SizeNoTag([value intValue]);
    case PBExtensionTypeUInt64:   return computeUInt64SizeNoTag([value longLongValue]);
    case PBExtensionTypeBytes:    return computeDataSizeNoTag(value);
    case PBExtensionTypeString:   return computeStringSizeNoTag(value);
    case PBExtensionTypeGroup:    return computeGroupSizeNoTag(value);
    case PBExtensionTypeEnum:     return computeEnumSizeNoTag([value intValue]);
    case PBExtensionTypeMessage:  return computeMessageSizeNoTag(value);
  }

  @throw [NSException exceptionWithName:@"InternalError" reason:@"" userInfo:nil];
}


- (int32_t) computeSingleSerializedSizeIncludingTag:(id) value {
  switch (type) {
    case PBExtensionTypeBool:     return computeBoolSize(fieldNumber, [value boolValue]);
    case PBExtensionTypeFixed32:  return computeFixed32Size(fieldNumber, [value intValue]);
    case PBExtensionTypeSFixed32: return computeSFixed32Size(fieldNumber, [value intValue]);
    case PBExtensionTypeFloat:    return computeFloatSize(fieldNumber, [value floatValue]);
    case PBExtensionTypeFixed64:  return computeFixed64Size(fieldNumber, [value longLongValue]);
    case PBExtensionTypeSFixed64: return computeSFixed64Size(fieldNumber, [value longLongValue]);
    case PBExtensionTypeDouble:   return computeDoubleSize(fieldNumber, [value doubleValue]);
    case PBExtensionTypeInt32:    return computeInt32Size(fieldNumber, [value intValue]);
    case PBExtensionTypeInt64:    return computeInt64Size(fieldNumber, [value longLongValue]);
    case PBExtensionTypeSInt32:   return computeSInt32Size(fieldNumber, [value intValue]);
    case PBExtensionTypeSInt64:   return computeSInt64Size(fieldNumber, [value longLongValue]);
    case PBExtensionTypeUInt32:   return computeUInt32Size(fieldNumber, [value intValue]);
    case PBExtensionTypeUInt64:   return computeUInt64Size(fieldNumber, [value longLongValue]);
    case PBExtensionTypeBytes:    return computeDataSize(fieldNumber, value);
    case PBExtensionTypeString:   return computeStringSize(fieldNumber, value);
    case PBExtensionTypeGroup:    return computeGroupSize(fieldNumber, value);
    case PBExtensionTypeEnum:     return computeEnumSize(fieldNumber, [value intValue]);
    case PBExtensionTypeMessage:
      if (isMessageSetWireFormat) {
        return computeMessageSetExtensionSize(fieldNumber, value);
      } else {
        return computeMessageSize(fieldNumber, value);
      }
  }

  @throw [NSException exceptionWithName:@"InternalError" reason:@"" userInfo:nil];
}


- (void)         writeRepeatedValues:(NSArray*) values
    includingTagsToCodedOutputStream:(PBCodedOutputStream*) output {
  if (isPacked) {
    [output writeTag:fieldNumber format:PBWireFormatLengthDelimited];
    int32_t dataSize = 0;
    if (typeIsFixedSize(type)) {
      dataSize = (int32_t)values.count * typeSize(type);
    } else {
      for (id value in values) {
        dataSize += [self computeSingleSerializedSizeNoTag:value];
      }
    }
    [output writeRawVarint32:dataSize];
    for (id value in values) {
      [self writeSingleValue:value noTagToCodedOutputStream:output];
    }
  } else {
    for (id value in values) {
      [self writeSingleValue:value includingTagToCodedOutputStream:output];
    }
  }
}


- (void) writeValue:(id) value includingTagToCodedOutputStream:(PBCodedOutputStream*) output {
  if (isRepeated) {
    [self writeRepeatedValues:value includingTagsToCodedOutputStream:output];
  } else {
    [self writeSingleValue:value includingTagToCodedOutputStream:output];
  }
}


- (int32_t) computeRepeatedSerializedSizeIncludingTags:(NSArray*) values {
  if (isPacked) {
    int32_t size = 0;
    if (typeIsFixedSize(type)) {
      size = (int32_t)values.count * typeSize(type);
    } else {
      for (id value in values) {
        size += [self computeSingleSerializedSizeNoTag:value];
      }
    }
    return size + computeTagSize(fieldNumber) + computeRawVarint32Size(size);
  } else {
    int32_t size = 0;
    for (id value in values) {
      size += [self computeSingleSerializedSizeIncludingTag:value];
    }
    return size;
  }
}


- (int32_t) computeSerializedSizeIncludingTag:(id) value {
  if (isRepeated) {
    return [self computeRepeatedSerializedSizeIncludingTags:value];
  } else {
    return [self computeSingleSerializedSizeIncludingTag:value];
  }
}


- (void) mergeMessageSetExtentionFromCodedInputStream:(PBCodedInputStream*) input
                                        unknownFields:(PBUnknownFieldSet_Builder*) unknownFields {
  @throw [NSException exceptionWithName:@"NYI" reason:@"" userInfo:nil];

  // The wire format for MessageSet is:
  //   message MessageSet {
  //     repeated group Item = 1 {
  //       required int32 typeId = 2;
  //       required bytes message = 3;
  //     }
  //   }
  // "typeId" is the extension's field number.  The extension can only be
  // a message type, where "message" contains the encoded bytes of that
  // message.
  //
  // In practice, we will probably never see a MessageSet item in which
  // the message appears before the type ID, or where either field does not
  // appear exactly once.  However, in theory such cases are valid, so we
  // should be prepared to accept them.

  //int typeId = 0;
//  ByteString rawBytes = null;
//
//  while (true) {
//    final int tag = input.readTag();
//    if (tag == 0) {
//      break;
//    }
//
//    if (tag == WireFormat.MESSAGE_SET_TYPE_ID_TAG) {
//      typeId = input.readUInt32();
//      // Zero is not a valid type ID.
//      if (typeId != 0) {
//        if (rawBytes != null) {
//          unknownFields.mergeField(typeId,
//                                   UnknownFieldSet.Field.newBuilder()
//                                   .addLengthDelimited(rawBytes)
//                                   .build());
//          rawBytes = null;
//        }
//      }
//    } else if (tag == WireFormat.MESSAGE_SET_MESSAGE_TAG) {
//      if (typeId == 0) {
//        // We haven't seen a type ID yet, so we have to store the raw bytes
//        // for now.
//        rawBytes = input.readBytes();
//      } else {
//        unknownFields.mergeField(typeId,
//                                 UnknownFieldSet.Field.newBuilder()
//                                 .addLengthDelimited(input.readBytes())
//                                 .build());
//      }
//    } else {
//      // Unknown fieldNumber.  Skip it.
//      if (!input.skipField(tag)) {
//        break;  // end of group
//      }
//    }
//  }
//
//  input.checkLastTagWas(WireFormat.MESSAGE_SET_ITEM_END_TAG);
}


- (id) readSingleValueFromCodedInputStream:(PBCodedInputStream*) input
                         extensionRegistry:(PBExtensionRegistry*) extensionRegistry {
  switch (type) {
    case PBExtensionTypeBool:     return [NSNumber numberWithBool:[input readBool]];
    case PBExtensionTypeFixed32:  return [NSNumber numberWithInt:[input readFixed32]];
    case PBExtensionTypeSFixed32: return [NSNumber numberWithInt:[input readSFixed32]];
    case PBExtensionTypeFloat:    return [NSNumber numberWithFloat:[input readFloat]];
    case PBExtensionTypeFixed64:  return [NSNumber numberWithLongLong:[input readFixed64]];
    case PBExtensionTypeSFixed64: return [NSNumber numberWithLongLong:[input readSFixed64]];
    case PBExtensionTypeDouble:   return [NSNumber numberWithDouble:[input readDouble]];
    case PBExtensionTypeInt32:    return [NSNumber numberWithInt:[input readInt32]];
    case PBExtensionTypeInt64:    return [NSNumber numberWithLongLong:[input readInt64]];
    case PBExtensionTypeSInt32:   return [NSNumber numberWithInt:[input readSInt32]];
    case PBExtensionTypeSInt64:   return [NSNumber numberWithLongLong:[input readSInt64]];
    case PBExtensionTypeUInt32:   return [NSNumber numberWithInt:[input readUInt32]];
    case PBExtensionTypeUInt64:   return [NSNumber numberWithLongLong:[input readUInt64]];
    case PBExtensionTypeBytes:    return [input readData];
    case PBExtensionTypeString:   return [input readString];
    case PBExtensionTypeEnum:     return [NSNumber numberWithInt:[input readEnum]];
    case PBExtensionTypeGroup:
    {
      id<PBMessage_Builder> builder = [messageOrGroupClass builder];
      [input readGroup:fieldNumber builder:builder extensionRegistry:extensionRegistry];
      return [builder build];
    }

    case PBExtensionTypeMessage:
    {
      id<PBMessage_Builder> builder = [messageOrGroupClass builder];
      [input readMessage:builder extensionRegistry:extensionRegistry];
      return [builder build];
    }
  }

  @throw [NSException exceptionWithName:@"InternalError" reason:@"" userInfo:nil];
}


- (void) mergeFromCodedInputStream:(PBCodedInputStream*) input
                     unknownFields:(PBUnknownFieldSet_Builder*) unknownFields
     extensionRegistry:(PBExtensionRegistry*) extensionRegistry
    builder:(PBExtendableMessage_Builder*) builder
                               tag:(int32_t) tag {
  if (isPacked) {
    int32_t length = [input readRawVarint32];
    int32_t limit = [input pushLimit:length];
    while ([input bytesUntilLimit] > 0) {
      id value = [self readSingleValueFromCodedInputStream:input extensionRegistry:extensionRegistry];
      [builder addExtension:self value:value];
    }
    [input popLimit:limit];
  } else if (isMessageSetWireFormat) {
    [self mergeMessageSetExtentionFromCodedInputStream:input
                                         unknownFields:unknownFields];
  } else {
    id value = [self readSingleValueFromCodedInputStream:input extensionRegistry:extensionRegistry];
    if (isRepeated) {
      [builder addExtension:self value:value];
    } else {
      [builder setExtension:self value:value];
    }
  }
}


@end
