/*
 * Copyright 2017 Google
 *
 * 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 "FirebaseDatabase/Sources/Snapshot/FChildrenNode.h"
#import "FirebaseDatabase/Sources/Constants/FConstants.h"
#import "FirebaseDatabase/Sources/FMaxNode.h"
#import "FirebaseDatabase/Sources/FNamedNode.h"
#import "FirebaseDatabase/Sources/FPriorityIndex.h"
#import "FirebaseDatabase/Sources/FTransformedEnumerator.h"
#import "FirebaseDatabase/Sources/Snapshot/FEmptyNode.h"
#import "FirebaseDatabase/Sources/Snapshot/FSnapshotUtilities.h"
#import "FirebaseDatabase/Sources/Utilities/FStringUtilities.h"
#import "FirebaseDatabase/Sources/Utilities/FUtilities.h"

@interface FChildrenNode ()
@property(nonatomic, strong) NSString *lazyHash;
@end

@implementation FChildrenNode

// Note: The only reason we allow nil priority is to for EmptyNode, since we
// can't use EmptyNode as the priority of EmptyNode.  We might want to consider
// making EmptyNode its own class instead of an empty ChildrenNode.

- (id)init {
    return [self
        initWithPriority:nil
                children:[FImmutableSortedDictionary
                             dictionaryWithComparator:[FUtilities
                                                          keyComparator]]];
}

- (id)initWithChildren:(FImmutableSortedDictionary *)someChildren {
    return [self initWithPriority:nil children:someChildren];
}

- (id)initWithPriority:(id<FNode>)aPriority
              children:(FImmutableSortedDictionary *)someChildren {
    if (someChildren.isEmpty && aPriority != nil && ![aPriority isEmpty]) {
        [NSException raise:NSInvalidArgumentException
                    format:@"Can't create empty node with priority!"];
    }
    self = [super init];
    if (self) {
        self.children = someChildren;
        self.priorityNode = aPriority;
    }
    return self;
}

- (NSString *)description {
    return [[self valForExport:YES] description];
}

#pragma mark -
#pragma mark FNode methods

- (BOOL)isLeafNode {
    return NO;
}

- (id<FNode>)getPriority {
    if (self.priorityNode) {
        return self.priorityNode;
    } else {
        return [FEmptyNode emptyNode];
    }
}

- (id<FNode>)updatePriority:(id<FNode>)aPriority {
    if ([self.children isEmpty]) {
        return [FEmptyNode emptyNode];
    } else {
        return [[FChildrenNode alloc] initWithPriority:aPriority
                                              children:self.children];
    }
}

- (id<FNode>)getImmediateChild:(NSString *)childName {
    if ([childName isEqualToString:@".priority"]) {
        return [self getPriority];
    } else {
        id<FNode> child = [self.children objectForKey:childName];
        return (child == nil) ? [FEmptyNode emptyNode] : child;
    }
}

- (id<FNode>)getChild:(FPath *)path {
    NSString *front = [path getFront];
    if (front == nil) {
        return self;
    } else {
        return [[self getImmediateChild:front] getChild:[path popFront]];
    }
}

- (BOOL)hasChild:(NSString *)childName {
    return ![self getImmediateChild:childName].isEmpty;
}

- (id<FNode>)updateImmediateChild:(NSString *)childName
                     withNewChild:(id<FNode>)newChildNode {
    NSAssert(newChildNode != nil, @"Should always be passing nodes.");

    if ([childName isEqualToString:@".priority"]) {
        return [self updatePriority:newChildNode];
    } else {
        FImmutableSortedDictionary *newChildren;
        if (newChildNode.isEmpty) {
            newChildren = [self.children removeObjectForKey:childName];
        } else {
            newChildren = [self.children setObject:newChildNode
                                            forKey:childName];
        }
        if (newChildren.isEmpty) {
            return [FEmptyNode emptyNode];
        } else {
            return [[FChildrenNode alloc] initWithPriority:self.getPriority
                                                  children:newChildren];
        }
    }
}

- (id<FNode>)updateChild:(FPath *)path withNewChild:(id<FNode>)newChildNode {
    NSString *front = [path getFront];
    if (front == nil) {
        return newChildNode;
    } else {
        NSAssert(![front isEqualToString:@".priority"] || path.length == 1,
                 @".priority must be the last token in a path.");
        id<FNode> newImmediateChild =
            [[self getImmediateChild:front] updateChild:[path popFront]
                                           withNewChild:newChildNode];
        return [self updateImmediateChild:front withNewChild:newImmediateChild];
    }
}

- (BOOL)isEmpty {
    return [self.children isEmpty];
}

- (int)numChildren {
    return [self.children count];
}

- (id)val {
    return [self valForExport:NO];
}

- (id)valForExport:(BOOL)exp {
    if ([self isEmpty]) {
        return [NSNull null];
    }

    __block int numKeys = 0;
    __block NSInteger maxKey = 0;
    __block BOOL allIntegerKeys = YES;

    NSMutableDictionary *obj =
        [[NSMutableDictionary alloc] initWithCapacity:[self.children count]];
    [self enumerateChildrenUsingBlock:^(NSString *key, id<FNode> childNode,
                                        BOOL *stop) {
      [obj setObject:[childNode valForExport:exp] forKey:key];

      numKeys++;

      // If we already found a string key, don't bother with any of this
      if (!allIntegerKeys) {
          return;
      }

      // Treat leading zeroes that are not exactly "0" as strings
      NSString *firstChar = [key substringWithRange:NSMakeRange(0, 1)];
      if ([firstChar isEqualToString:@"0"] && [key length] > 1) {
          allIntegerKeys = NO;
      } else {
          NSNumber *keyAsNum = [FUtilities intForString:key];
          if (keyAsNum != nil) {
              NSInteger keyAsInt = [keyAsNum integerValue];
              if (keyAsInt > maxKey) {
                  maxKey = keyAsInt;
              }
          } else {
              allIntegerKeys = NO;
          }
      }
    }];

    if (!exp && allIntegerKeys && maxKey < 2 * numKeys) {
        // convert to an array
        NSMutableArray *array =
            [[NSMutableArray alloc] initWithCapacity:maxKey + 1];
        for (int i = 0; i <= maxKey; ++i) {
            NSString *keyString = [NSString stringWithFormat:@"%i", i];
            id child = obj[keyString];
            if (child != nil) {
                [array addObject:child];
            } else {
                [array addObject:[NSNull null]];
            }
        }
        return array;
    } else {

        if (exp && [self getPriority] != nil && !self.getPriority.isEmpty) {
            obj[kPayloadPriority] = [self.getPriority val];
        }

        return obj;
    }
}

- (NSString *)dataHash {
    if (self.lazyHash == nil) {
        NSMutableString *toHash = [[NSMutableString alloc] init];

        if (!self.getPriority.isEmpty) {
            [toHash appendString:@"priority:"];
            [FSnapshotUtilities
                appendHashRepresentationForLeafNode:(FLeafNode *)
                                                        self.getPriority
                                           toString:toHash
                                        hashVersion:FDataHashVersionV1];
            [toHash appendString:@":"];
        }

        __block BOOL sawPriority = NO;
        [self enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node,
                                            BOOL *stop) {
          sawPriority = sawPriority || [[node getPriority] isEmpty];
          *stop = sawPriority;
        }];
        if (sawPriority) {
            NSMutableArray *array = [NSMutableArray array];
            [self enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node,
                                                BOOL *stop) {
              FNamedNode *namedNode = [[FNamedNode alloc] initWithName:key
                                                               andNode:node];
              [array addObject:namedNode];
            }];
            [array sortUsingComparator:^NSComparisonResult(
                       FNamedNode *namedNode1, FNamedNode *namedNode2) {
              return
                  [[FPriorityIndex priorityIndex] compareNamedNode:namedNode1
                                                       toNamedNode:namedNode2];
            }];
            [array enumerateObjectsUsingBlock:^(FNamedNode *namedNode,
                                                NSUInteger idx, BOOL *stop) {
              NSString *childHash = [namedNode.node dataHash];
              if (![childHash isEqualToString:@""]) {
                  [toHash appendFormat:@":%@:%@", namedNode.name, childHash];
              }
            }];
        } else {
            [self enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node,
                                                BOOL *stop) {
              NSString *childHash = [node dataHash];
              if (![childHash isEqualToString:@""]) {
                  [toHash appendFormat:@":%@:%@", key, childHash];
              }
            }];
        }
        self.lazyHash = [toHash isEqualToString:@""]
                            ? @""
                            : [FStringUtilities base64EncodedSha1:toHash];
    }
    return self.lazyHash;
}

- (NSComparisonResult)compare:(id<FNode>)other {
    // children nodes come last, unless this is actually an empty node, then we
    // come first.
    if (self.isEmpty) {
        if (other.isEmpty) {
            return NSOrderedSame;
        } else {
            return NSOrderedAscending;
        }
    } else if (other.isLeafNode || other.isEmpty) {
        return NSOrderedDescending;
    } else if (other == [FMaxNode maxNode]) {
        return NSOrderedAscending;
    } else {
        // Must be another node with children.
        return NSOrderedSame;
    }
}

- (BOOL)isEqual:(id<FNode>)other {
    if (other == self) {
        return YES;
    } else if (other == nil) {
        return NO;
    } else if (other.isLeafNode) {
        return NO;
    } else if (self.isEmpty && [other isEmpty]) {
        // Empty nodes do not have priority
        return YES;
    } else {
        FChildrenNode *otherChildrenNode = other;
        if (![self.getPriority isEqual:other.getPriority]) {
            return NO;
        } else if (self.children.count == otherChildrenNode.children.count) {
            __block BOOL equal = YES;
            [self enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node,
                                                BOOL *stop) {
              id<FNode> child = [otherChildrenNode getImmediateChild:key];
              if (![child isEqual:node]) {
                  equal = NO;
                  *stop = YES;
              }
            }];
            return equal;
        } else {
            return NO;
        }
    }
}

- (NSUInteger)hash {
    __block NSUInteger hashCode = 0;
    [self enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node,
                                        BOOL *stop) {
      hashCode = 31 * hashCode + key.hash;
      hashCode = 17 * hashCode + node.hash;
    }];
    return 17 * hashCode + self.priorityNode.hash;
}

- (void)enumerateChildrenAndPriorityUsingBlock:(void (^)(NSString *, id<FNode>,
                                                         BOOL *))block {
    if ([self.getPriority isEmpty]) {
        [self enumerateChildrenUsingBlock:block];
    } else {
        __block BOOL passedPriorityKey = NO;
        [self enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node,
                                            BOOL *stop) {
          if (!passedPriorityKey &&
              [FUtilities compareKey:key
                               toKey:@".priority"] == NSOrderedDescending) {
              passedPriorityKey = YES;
              BOOL stopAfterPriority = NO;
              block(@".priority", [self getPriority], &stopAfterPriority);
              if (stopAfterPriority)
                  return;
          }
          block(key, node, stop);
        }];
    }
}

- (void)enumerateChildrenUsingBlock:(void (^)(NSString *, id<FNode>,
                                              BOOL *))block {
    [self.children enumerateKeysAndObjectsUsingBlock:block];
}

- (void)enumerateChildrenReverse:(BOOL)reverse
                      usingBlock:
                          (void (^)(NSString *, id<FNode>, BOOL *))block {
    [self.children enumerateKeysAndObjectsReverse:reverse usingBlock:block];
}

- (NSEnumerator *)childEnumerator {
    return [[FTransformedEnumerator alloc]
        initWithEnumerator:self.children.keyEnumerator
              andTransform:^id(NSString *key) {
                return [FNamedNode nodeWithName:key
                                           node:[self getImmediateChild:key]];
              }];
}

- (NSString *)predecessorChildKey:(NSString *)childKey {
    return [self.children getPredecessorKey:childKey];
}

#pragma mark -
#pragma mark FChildrenNode specific methods

- (id)childrenGetter:(id)key {
    return [self.children objectForKey:key];
}

- (FNamedNode *)firstChild {
    NSString *childKey = self.children.minKey;
    if (childKey) {
        return
            [[FNamedNode alloc] initWithName:childKey
                                     andNode:[self getImmediateChild:childKey]];
    } else {
        return nil;
    }
}

- (FNamedNode *)lastChild {
    NSString *childKey = self.children.maxKey;
    if (childKey) {
        return
            [[FNamedNode alloc] initWithName:childKey
                                     andNode:[self getImmediateChild:childKey]];
    } else {
        return nil;
    }
}

@end
