// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/showcase/common/protocol_alerter.h"

#import <objc/runtime.h>

#import "base/logging.h"
#import "base/strings/sys_string_conversions.h"

#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif

namespace {
// Opaque value to use as an associated object key.
char kAssociatedProtocolNameKey;
}

@interface NSInvocation (Description)
// Returns a string description of the invocation consisting of the selector
// name interspersed with argument values.
- (NSString*)crsc_description;
@end

@interface ProtocolAlerter () {
  NSSet<Protocol*>* _protocols;
  // Selectors for which no logging should be done.
  NSMutableSet<NSValue*>* _ignoredSelectors;
}
@end

@implementation ProtocolAlerter

@synthesize baseViewController = _baseViewController;

- (instancetype)initWithProtocols:(NSArray<Protocol*>*)protocols {
  if (!protocols)
    return nil;
  // NSProxy isn't a subclass of NSObject, and has no superclass, so
  // there's no [super init] to call.
  _protocols = [[NSSet<Protocol*> alloc] initWithArray:protocols];
  _ignoredSelectors = [NSMutableSet set];
  return self;
}

- (void)ignoreSelector:(SEL)sel {
  [_ignoredSelectors addObject:[NSValue valueWithPointer:sel]];
}

#pragma mark - NSProxy

- (NSMethodSignature*)methodSignatureForSelector:(SEL)sel {
  for (Protocol* protocol in _protocols) {
    const BOOL kYesAndNoArray[] = {YES, NO};
    for (BOOL required : kYesAndNoArray) {
      struct objc_method_description method = protocol_getMethodDescription(
          protocol, sel, required, YES /* an instance method */);
      if (method.name != NULL) {
        NSMethodSignature* signature =
            [NSMethodSignature signatureWithObjCTypes:method.types];
        // Tag the method signature with the protocol name.
        objc_setAssociatedObject(signature,
                                 &kAssociatedProtocolNameKey,
                                 NSStringFromProtocol(protocol),
                                 OBJC_ASSOCIATION_COPY_NONATOMIC);
        return signature;
      }
    }
  }
  return nil;
}

- (void)forwardInvocation:(NSInvocation*)invocation {
  if ([_ignoredSelectors
          containsObject:[NSValue valueWithPointer:invocation.selector]]) {
    return;
  }
  // Instead of actually doing anything the protocol method would normally
  // do, instead just generate a title and description and display an alert or
  // log a message.
  NSString* protocolName = objc_getAssociatedObject(
      [self methodSignatureForSelector:invocation.selector],
      &kAssociatedProtocolNameKey);
  NSString* description = [invocation crsc_description];
  if (self.baseViewController) {
    [self showAlertWithTitle:protocolName message:description];
  } else {
    VLOG(0) << "Alerter -- protocol:"
            << base::SysNSStringToUTF8(protocolName);
    VLOG(0) << "Alerter -- invocation:"
            << base::SysNSStringToUTF8(description);
  }
}

#pragma mark - NSObject

- (BOOL)conformsToProtocol:(Protocol*)aProtocol {
  for (Protocol* protocol in _protocols) {
    // Handle protocols that conform to other protocols.
    if (protocol_conformsToProtocol(protocol, aProtocol))
      return YES;
  }
  return NO;
}

- (BOOL)respondsToSelector:(SEL)aSelector {
  return [self methodSignatureForSelector:aSelector] != nil;
}

#pragma mark - Private

// Helper to show simple alert.
- (void)showAlertWithTitle:(NSString*)title message:(NSString*)message {
  UIAlertController* alertController =
      [UIAlertController alertControllerWithTitle:title
                                          message:message
                                   preferredStyle:UIAlertControllerStyleAlert];
  UIAlertAction* action =
      [UIAlertAction actionWithTitle:@"Done"
                               style:UIAlertActionStyleCancel
                             handler:nil];
  [action setAccessibilityLabel:@"protocol_alerter_done"];
  [alertController addAction:action];
  [self.baseViewController presentViewController:alertController
                                        animated:YES
                                      completion:nil];
}

@end

@implementation NSInvocation (Description)

- (NSString*)crsc_description {
  NSInteger arguments = self.methodSignature.numberOfArguments;
  NSString* selector = NSStringFromSelector(self.selector);

  // NSInvocation's first two arguments are |self| and |_cmd|; if they are the
  // only ones, then the invocation has no actual arguments.
  if (arguments == 2)
    return selector;

  // Get the parts of the selector name by splitting on /:/, and dropping the
  // last (empty) part.
  NSArray* keywords = [[selector componentsSeparatedByString:@":"]
      subarrayWithRange:NSMakeRange(0, arguments - 2)];
  NSMutableString* description = [[NSMutableString alloc] init];
  NSInteger argumentIndex = 2;
  for (NSString* keyword in keywords) {
    // Insert a space before each keyword after the first one.
    if (description.length)
      [description appendString:@" "];
    [description appendString:keyword];
    [description appendString:@":"];
    [description
        appendString:[self crsc_argumentDescriptionAtIndex:argumentIndex]];
    argumentIndex++;
  }

  return description;
}

// Return a string describing the argument value at |index|.
// (|index| is in NSInvocation's argument array).
- (NSString*)crsc_argumentDescriptionAtIndex:(NSInteger)index {
  const char* type = [self.methodSignature getArgumentTypeAtIndex:index];

  switch (*type) {
    case '@':
      return [self crsc_objectDescriptionAtIndex:index];
    case 'q':
      return [self crsc_longLongDescriptionAtIndex:index];
    case 'Q':
      return [self crsc_unsignedLongLongDescriptionAtIndex:index];
    // Add cases as needed here.
    default:
      return [NSString stringWithFormat:@"<Unknown Type:%s>", type];
  }
}

// Return a string describing an argument at |index| that's known to be an
// objective-C object.
- (NSString*)crsc_objectDescriptionAtIndex:(NSInteger)index {
  __unsafe_unretained id object;

  [self getArgument:&object atIndex:index];
  if (!object)
    return @"nil";

  NSString* description = [object description];
  NSString* className = NSStringFromClass([object class]);
  if (!description) {
    return
        [NSString stringWithFormat:@"<%@ object, no description>", className];
  }

  // Wrap strings in @" ... ".
  if ([object isKindOfClass:[NSString class]])
    return [NSString stringWithFormat:@"@\"%@\"", description];

  // Remove the address of objects from their descriptions, so (for example):
  //   <NSObject: 0xc00lf0ccac1a>
  // becomes just:
  //   <NSObject>
  NSRange range = NSMakeRange(0, description.length);
  NSString* classPlusAddress =
      [className stringByAppendingString:@": 0x[0-9a-c]+"];
  return [description
      stringByReplacingOccurrencesOfString:classPlusAddress
                                withString:className
                                   options:NSRegularExpressionSearch
                                     range:range];
}

// Returns a string describing an argument at |index| that is known to be a long
// long.
- (NSString*)crsc_longLongDescriptionAtIndex:(NSInteger)index {
  long long value;

  [self getArgument:&value atIndex:index];
  return [NSString stringWithFormat:@"%lld", value];
}

// Returns a string describing an argument at |index| that is known to be an
// unsigned long long.
- (NSString*)crsc_unsignedLongLongDescriptionAtIndex:(NSInteger)index {
  unsigned long long value;

  [self getArgument:&value atIndex:index];
  return [NSString stringWithFormat:@"%llu", value];
}

@end