/* Copyright (c) 2014-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

#if TARGET_OS_IOS
#import "ASMapNode.h"
#import <AsyncDisplayKit/ASDisplayNode+Subclasses.h>
#import <AsyncDisplayKit/ASDisplayNodeExtras.h>
#import <AsyncDisplayKit/ASInsetLayoutSpec.h>
#import <AsyncDisplayKit/ASCenterLayoutSpec.h>
#import <AsyncDisplayKit/ASThread.h>

@interface ASMapNode()
{
  ASDN::RecursiveMutex _propertyLock;
  MKMapSnapshotter *_snapshotter;
  NSArray *_annotations;
  CLLocationCoordinate2D _centerCoordinateOfMap;
}
@end

@implementation ASMapNode

@synthesize needsMapReloadOnBoundsChange = _needsMapReloadOnBoundsChange;
@synthesize mapDelegate = _mapDelegate;
@synthesize options = _options;
@synthesize liveMap = _liveMap;

#pragma mark - Lifecycle
- (instancetype)init
{
  if (!(self = [super init])) {
    return nil;
  }
  self.backgroundColor = ASDisplayNodeDefaultPlaceholderColor();
  self.clipsToBounds = YES;
  
  _needsMapReloadOnBoundsChange = YES;
  _liveMap = NO;
  _centerCoordinateOfMap = kCLLocationCoordinate2DInvalid;
  return self;
}

- (void)didLoad
{
  [super didLoad];
  if (self.isLiveMap) {
    self.userInteractionEnabled = YES;
    [self addLiveMap];
  }
}

- (void)setLayerBacked:(BOOL)layerBacked
{
  ASDisplayNodeAssert(!self.isLiveMap, @"ASMapNode can not be layer backed whilst .liveMap = YES, set .liveMap = NO to use layer backing.");
  [super setLayerBacked:layerBacked];
}

- (void)fetchData
{
  [super fetchData];
  if (self.isLiveMap) {
    [self addLiveMap];
  } else {
    [self takeSnapshot];
  }
}

- (void)clearContents
{
  [super clearContents];
  if (self.isLiveMap) {
    [self removeLiveMap];
  }
}

#pragma mark - Settings

- (BOOL)isLiveMap
{
  ASDN::MutexLocker l(_propertyLock);
  return _liveMap;
}

- (void)setLiveMap:(BOOL)liveMap
{
  ASDisplayNodeAssert(!self.isLayerBacked, @"ASMapNode can not use the interactive map feature whilst .isLayerBacked = YES, set .layerBacked = NO to use the interactive map feature.");
  ASDN::MutexLocker l(_propertyLock);
  if (liveMap == _liveMap) {
    return;
  }
  _liveMap = liveMap;
  if (self.nodeLoaded) {
    liveMap ? [self addLiveMap] : [self removeLiveMap];
  }
}

- (BOOL)needsMapReloadOnBoundsChange
{
  ASDN::MutexLocker l(_propertyLock);
  return _needsMapReloadOnBoundsChange;
}

- (void)setNeedsMapReloadOnBoundsChange:(BOOL)needsMapReloadOnBoundsChange
{
  ASDN::MutexLocker l(_propertyLock);
  _needsMapReloadOnBoundsChange = needsMapReloadOnBoundsChange;
}

- (MKMapSnapshotOptions *)options
{
  ASDN::MutexLocker l(_propertyLock);
  if (!_options) {
    _options = [[MKMapSnapshotOptions alloc] init];
    _options.region = MKCoordinateRegionForMapRect(MKMapRectWorld);
    CGSize calculatedSize = self.calculatedSize;
    if (!CGSizeEqualToSize(calculatedSize, CGSizeZero)) {
      _options.size = calculatedSize;
    }
  }
  return _options;
}

- (void)setOptions:(MKMapSnapshotOptions *)options
{
  ASDN::MutexLocker l(_propertyLock);
  _options = options;
  if (self.isLiveMap) {
    [self applySnapshotOptions];
  } else {
    [self resetSnapshotter];
    [self takeSnapshot];
  }
}

- (MKCoordinateRegion)region
{
  return self.options.region;
}

- (void)setRegion:(MKCoordinateRegion)region
{
  self.options.region = region;
}

#pragma mark - Snapshotter

- (void)takeSnapshot
{
  if (!_snapshotter) {
    [self setUpSnapshotter];
  }
  [_snapshotter cancel];
  [_snapshotter startWithCompletionHandler:^(MKMapSnapshot *snapshot, NSError *error) {
    if (!error) {
      UIImage *image = snapshot.image;
      CGRect finalImageRect = CGRectMake(0, 0, image.size.width, image.size.height);
      
      UIGraphicsBeginImageContextWithOptions(image.size, YES, image.scale);
      [image drawAtPoint:CGPointMake(0, 0)];
      
      if (_annotations.count > 0 ) {
        // Get a standard annotation view pin. Future implementations should use a custom annotation image property.
        MKAnnotationView *pin = [[MKPinAnnotationView alloc] initWithAnnotation:nil reuseIdentifier:@""];
        UIImage *pinImage = pin.image;
        for (id<MKAnnotation>annotation in _annotations)
        {
          CGPoint point = [snapshot pointForCoordinate:annotation.coordinate];
          if (CGRectContainsPoint(finalImageRect, point))
          {
            CGPoint pinCenterOffset = pin.centerOffset;
            point.x -= pin.bounds.size.width / 2.0;
            point.y -= pin.bounds.size.height / 2.0;
            point.x += pinCenterOffset.x;
            point.y += pinCenterOffset.y;
            [pinImage drawAtPoint:point];
          }
        }
      }
      
      UIImage *finalImage = UIGraphicsGetImageFromCurrentImageContext();
      UIGraphicsEndImageContext();
      self.image = finalImage;
    }
  }];
}

- (void)setUpSnapshotter
{
  ASDisplayNodeAssert(!CGSizeEqualToSize(CGSizeZero, self.calculatedSize), @"self.calculatedSize can not be zero. Make sure that you are setting a preferredFrameSize or wrapping ASMapNode in a ASRatioLayoutSpec or similar.");
  _snapshotter = [[MKMapSnapshotter alloc] initWithOptions:self.options];
}

- (void)resetSnapshotter
{
  // FIXME: The semantics of this method / name would suggest that we cancel + destroy the snapshotter,
  // but not that we create a new one.  We should probably only create the new one in -takeSnapshot or something.
  [_snapshotter cancel];
  _snapshotter = [[MKMapSnapshotter alloc] initWithOptions:self.options];
}

- (void)applySnapshotOptions
{
  MKMapSnapshotOptions *options = self.options;
  [_mapView setCamera:options.camera animated:YES];
  [_mapView setRegion:options.region animated:YES];
  [_mapView setMapType:options.mapType];
  _mapView.showsBuildings = options.showsBuildings;
  _mapView.showsPointsOfInterest = options.showsPointsOfInterest;
}

#pragma mark - Actions
- (void)addLiveMap
{
  ASDisplayNodeAssertMainThread();
  if (!_mapView) {
    __weak ASMapNode *weakSelf = self;
    _mapView = [[MKMapView alloc] initWithFrame:CGRectZero];
    _mapView.delegate = weakSelf.mapDelegate;
    [weakSelf applySnapshotOptions];
    [_mapView addAnnotations:_annotations];
    [weakSelf setNeedsLayout];
    [weakSelf.view addSubview:_mapView];
    
    if (CLLocationCoordinate2DIsValid(_centerCoordinateOfMap)) {
      [_mapView setCenterCoordinate:_centerCoordinateOfMap];
    }
  }
}

- (void)removeLiveMap
{
  // FIXME: With MKCoordinateRegion, isn't the center coordinate fully specified?  Do we need this?
  _centerCoordinateOfMap = _mapView.centerCoordinate;
  [_mapView removeFromSuperview];
  _mapView = nil;
}

- (void)setAnnotations:(NSArray *)annotations
{
  ASDN::MutexLocker l(_propertyLock);
  _annotations = [annotations copy];
  if (self.isLiveMap) {
    [_mapView removeAnnotations:_mapView.annotations];
    [_mapView addAnnotations:annotations];
  } else {
    [self takeSnapshot];
  }
}

#pragma mark - Layout
- (void)setSnapshotSizeIfNeeded:(CGSize)snapshotSize
{
  if (!CGSizeEqualToSize(self.options.size, snapshotSize)) {
    _options.size = snapshotSize;
    [self resetSnapshotter];
  }
}

- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize
{
  CGSize size = self.preferredFrameSize;
  if (CGSizeEqualToSize(size, CGSizeZero)) {
    size = constrainedSize;
  }
  [self setSnapshotSizeIfNeeded:size];
  return constrainedSize;
}

// Layout isn't usually needed in the box model, but since we are making use of MKMapView this is preferred.
- (void)layout
{
  [super layout];
  if (self.isLiveMap) {
    _mapView.frame = CGRectMake(0.0f, 0.0f, self.calculatedSize.width, self.calculatedSize.height);
  } else {
    // If our bounds.size is different from our current snapshot size, then let's request a new image from MKMapSnapshotter.
    if (_needsMapReloadOnBoundsChange) {
      [self setSnapshotSizeIfNeeded:self.bounds.size];
      // FIXME: Adding a check for FetchData here seems to cause intermittent map load failures, but shouldn't.
      // if (ASInterfaceStateIncludesFetchData(self.interfaceState)) {
      [self takeSnapshot];
    }
  }
}
@end
#endif