/* clang-format off */
/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* clang-format on */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#import "MOXAccessibleBase.h"

#import "MacSelectorMap.h"

#include "nsObjCExceptions.h"
#include "xpcAccessibleMacInterface.h"
#include "mozilla/Logging.h"
#include "gfxPlatform.h"

using namespace mozilla;
using namespace mozilla::a11y;

#undef LOG
mozilla::LogModule* GetMacAccessibilityLog() {
  static mozilla::LazyLogModule sLog("MacAccessibility");

  return sLog;
}
#define LOG(type, format, ...)                                             \
  do {                                                                     \
    if (MOZ_LOG_TEST(GetMacAccessibilityLog(), type)) {                    \
      NSString* msg = [NSString stringWithFormat:(format), ##__VA_ARGS__]; \
      MOZ_LOG(GetMacAccessibilityLog(), type, ("%s", [msg UTF8String]));   \
    }                                                                      \
  } while (0)

@interface NSObject (MOXAccessible)

// This NSObject conforms to MOXAccessible.
// This is needed to we know to mutate the value
// (get represented view, check isAccessibilityElement)
// before forwarding it to NSAccessibility.
- (BOOL)isMOXAccessible;

// Same as above, but this checks if the NSObject is an array with
// mozAccessible conforming objects.
- (BOOL)hasMOXAccessibles;

@end

@implementation NSObject (MOXAccessible)

- (BOOL)isMOXAccessible {
  return [self conformsToProtocol:@protocol(MOXAccessible)];
}

- (BOOL)hasMOXAccessibles {
  return [self isKindOfClass:[NSArray class]] &&
         [[(NSArray*)self firstObject] isMOXAccessible];
}

@end

// Private methods
@interface MOXAccessibleBase ()

- (BOOL)isSelectorSupported:(SEL)selector;

@end

@implementation MOXAccessibleBase

#pragma mark - mozAccessible/widget

- (BOOL)hasRepresentedView {
  return NO;
}

- (id)representedView {
  return nil;
}

- (BOOL)isRoot {
  return NO;
}

#pragma mark - mozAccessible/NSAccessibility

- (NSArray*)accessibilityAttributeNames {
  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;

  if ([self isExpired]) {
    return nil;
  }

  static NSMutableDictionary* attributesForEachClass = nil;

  if (!attributesForEachClass) {
    attributesForEachClass = [[NSMutableDictionary alloc] init];
  }

  NSMutableArray* attributes =
      attributesForEachClass [[self class]]
          ?: [[[NSMutableArray alloc] init] autorelease];

  NSDictionary* getters = mac::AttributeGetters();
  if (![attributes count]) {
    // Go through all our attribute getters, if they are supported by this class
    // advertise the attribute name.
    for (NSString* attribute in getters) {
      SEL selector = NSSelectorFromString(getters[attribute]);
      if ([self isSelectorSupported:selector]) {
        [attributes addObject:attribute];
      }
    }

    // If we have a delegate add all the text marker attributes.
    if ([self moxTextMarkerDelegate]) {
      [attributes addObjectsFromArray:[mac::TextAttributeGetters() allKeys]];
    }

    // We store a hash table with types as keys, and atttribute lists as values.
    // This lets us cache the atttribute list of each subclass so we only
    // need to gather its MOXAccessible methods once.
    // XXX: Uncomment when accessibilityAttributeNames is removed from all
    // subclasses. attributesForEachClass[[self class]] = attributes;
  }

  return attributes;

  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
}

- (id)accessibilityAttributeValue:(NSString*)attribute {
  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
  if ([self isExpired]) {
    return nil;
  }

  id value = nil;
  NSDictionary* getters = mac::AttributeGetters();
  if (getters[attribute]) {
    SEL selector = NSSelectorFromString(getters[attribute]);
    if ([self isSelectorSupported:selector]) {
      value = [self performSelector:selector];
    }
  } else if (id textMarkerDelegate = [self moxTextMarkerDelegate]) {
    // If we have a delegate, check if attribute is a text marker
    // attribute and call the associated selector on the delegate
    // if so.
    NSDictionary* textMarkerGetters = mac::TextAttributeGetters();
    if (textMarkerGetters[attribute]) {
      SEL selector = NSSelectorFromString(textMarkerGetters[attribute]);
      if ([textMarkerDelegate respondsToSelector:selector]) {
        value = [textMarkerDelegate performSelector:selector];
      }
    }
  }

  if ([value isMOXAccessible]) {
    // If this is a MOXAccessible, get its represented view or filter it if
    // it should be ignored.
    value = [value isAccessibilityElement] ? GetObjectOrRepresentedView(value)
                                           : nil;
  }

  if ([value hasMOXAccessibles]) {
    // If this is an array of mozAccessibles, get each element's represented
    // view and remove it from the returned array if it should be ignored.
    NSUInteger arrSize = [value count];
    NSMutableArray* arr =
        [[[NSMutableArray alloc] initWithCapacity:arrSize] autorelease];
    for (NSUInteger i = 0; i < arrSize; i++) {
      id<mozAccessible> mozAcc = GetObjectOrRepresentedView(value[i]);
      if ([mozAcc isAccessibilityElement]) {
        [arr addObject:mozAcc];
      }
    }

    value = arr;
  }

  if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Debug)) {
    if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Verbose)) {
      LOG(LogLevel::Verbose, @"%@ attributeValue %@ => %@", self, attribute,
          value);
    } else if (![attribute isEqualToString:@"AXParent"] &&
               ![attribute isEqualToString:@"AXRole"] &&
               ![attribute isEqualToString:@"AXSubrole"] &&
               ![attribute isEqualToString:@"AXSize"] &&
               ![attribute isEqualToString:@"AXPosition"] &&
               ![attribute isEqualToString:@"AXRole"] &&
               ![attribute isEqualToString:@"AXChildren"]) {
      LOG(LogLevel::Debug, @"%@ attributeValue %@", self, attribute);
    }
  }

  return value;

  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
}

- (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute {
  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;

  if ([self isExpired]) {
    return NO;
  }

  NSDictionary* setters = mac::AttributeSetters();
  if (setters[attribute]) {
    SEL selector = NSSelectorFromString(setters[attribute]);
    if ([self isSelectorSupported:selector]) {
      return YES;
    }
  } else if (id textMarkerDelegate = [self moxTextMarkerDelegate]) {
    // If we have a delegate, check text setters on delegate
    NSDictionary* textMarkerSetters = mac::TextAttributeSetters();
    if (textMarkerSetters[attribute]) {
      SEL selector = NSSelectorFromString(textMarkerSetters[attribute]);
      if ([textMarkerDelegate respondsToSelector:selector]) {
        return YES;
      }
    }
  }

  NS_OBJC_END_TRY_BLOCK_RETURN(NO);
}

- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute {
  NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;

  if ([self isExpired]) {
    return;
  }

  LOG(LogLevel::Debug, @"%@ setValueForattribute %@ = %@", self, attribute,
      value);

  NSDictionary* setters = mac::AttributeSetters();
  if (setters[attribute]) {
    SEL selector = NSSelectorFromString(setters[attribute]);
    if ([self isSelectorSupported:selector]) {
      [self performSelector:selector withObject:value];
    }
  } else if (id textMarkerDelegate = [self moxTextMarkerDelegate]) {
    // If we have a delegate, check if attribute is a text marker
    // attribute and call the associated selector on the delegate
    // if so.
    NSDictionary* textMarkerSetters = mac::TextAttributeSetters();
    if (textMarkerSetters[attribute]) {
      SEL selector = NSSelectorFromString(textMarkerSetters[attribute]);
      if ([textMarkerDelegate respondsToSelector:selector]) {
        [textMarkerDelegate performSelector:selector withObject:value];
      }
    }
  }

  NS_OBJC_END_TRY_IGNORE_BLOCK;
}

- (NSArray*)accessibilityActionNames {
  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;

  if ([self isExpired]) {
    return nil;
  }

  NSMutableArray* actionNames = [[[NSMutableArray alloc] init] autorelease];

  NSDictionary* actions = mac::Actions();
  for (NSString* action in actions) {
    SEL selector = NSSelectorFromString(actions[action]);
    if ([self isSelectorSupported:selector]) {
      [actionNames addObject:action];
    }
  }

  return actionNames;

  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
}

- (void)accessibilityPerformAction:(NSString*)action {
  NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;

  if ([self isExpired]) {
    return;
  }

  LOG(LogLevel::Debug, @"%@ performAction %@ ", self, action);

  NSDictionary* actions = mac::Actions();
  if (actions[action]) {
    SEL selector = NSSelectorFromString(actions[action]);
    if ([self isSelectorSupported:selector]) {
      [self performSelector:selector];
    }
  }

  NS_OBJC_END_TRY_IGNORE_BLOCK;
}

- (NSString*)accessibilityActionDescription:(NSString*)action {
  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
  // by default we return whatever the MacOS API know about.
  // if you have custom actions, override.
  return NSAccessibilityActionDescription(action);
  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
}

- (NSArray*)accessibilityParameterizedAttributeNames {
  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;

  if ([self isExpired]) {
    return nil;
  }

  NSMutableArray* attributeNames = [[[NSMutableArray alloc] init] autorelease];

  NSDictionary* attributes = mac::ParameterizedAttributeGetters();
  for (NSString* attribute in attributes) {
    SEL selector = NSSelectorFromString(attributes[attribute]);
    if ([self isSelectorSupported:selector]) {
      [attributeNames addObject:attribute];
    }
  }

  // If we have a delegate add all the text marker attributes.
  if ([self moxTextMarkerDelegate]) {
    [attributeNames
        addObjectsFromArray:[mac::ParameterizedTextAttributeGetters() allKeys]];
  }

  return attributeNames;

  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
}

- (id)accessibilityAttributeValue:(NSString*)attribute
                     forParameter:(id)parameter {
  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;

  if ([self isExpired]) {
    return nil;
  }

  id value = nil;

  NSDictionary* getters = mac::ParameterizedAttributeGetters();
  if (getters[attribute]) {
    SEL selector = NSSelectorFromString(getters[attribute]);
    if ([self isSelectorSupported:selector]) {
      value = [self performSelector:selector withObject:parameter];
    }
  } else if (id textMarkerDelegate = [self moxTextMarkerDelegate]) {
    // If we have a delegate, check if attribute is a text marker
    // attribute and call the associated selector on the delegate
    // if so.
    NSDictionary* textMarkerGetters = mac::ParameterizedTextAttributeGetters();
    if (textMarkerGetters[attribute]) {
      SEL selector = NSSelectorFromString(textMarkerGetters[attribute]);
      if ([textMarkerDelegate respondsToSelector:selector]) {
        value = [textMarkerDelegate performSelector:selector
                                         withObject:parameter];
      }
    }
  }

  if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Verbose)) {
    LOG(LogLevel::Verbose, @"%@ attributeValueForParam %@(%@) => %@", self,
        attribute, parameter, value);
  } else {
    LOG(LogLevel::Debug, @"%@ attributeValueForParam %@", self, attribute);
  }

  return value;

  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
}

- (id)accessibilityHitTest:(NSPoint)point {
  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
  return GetObjectOrRepresentedView([self moxHitTest:point]);
  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
}

- (id)accessibilityFocusedUIElement {
  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
  return GetObjectOrRepresentedView([self moxFocusedUIElement]);
  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
}

- (BOOL)isAccessibilityElement {
  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;

  if ([self isExpired]) {
    return YES;
  }

  id parent = [self moxParent];
  if (![parent isMOXAccessible]) {
    return YES;
  }

  return ![self moxIgnoreWithParent:parent];

  NS_OBJC_END_TRY_BLOCK_RETURN(NO);
}

- (BOOL)accessibilityNotifiesWhenDestroyed {
  return YES;
}

#pragma mark - MOXAccessible protocol

- (NSNumber*)moxIndexForChildUIElement:(id)child {
  return @([[self moxUnignoredChildren] indexOfObject:child]);
}

- (id)moxTopLevelUIElement {
  return [self moxWindow];
}

- (id)moxHitTest:(NSPoint)point {
  return self;
}

- (id)moxFocusedUIElement {
  return self;
}

- (void)moxPostNotification:(NSString*)notification {
  [self moxPostNotification:notification withUserInfo:nil];
}

- (void)moxPostNotification:(NSString*)notification
               withUserInfo:(NSDictionary*)userInfo {
  if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Verbose)) {
    LOG(LogLevel::Verbose, @"%@ notify %@ %@", self, notification, userInfo);
  } else {
    LOG(LogLevel::Debug, @"%@ notify %@", self, notification);
  }

  // This sends events via nsIObserverService to be consumed by our mochitests.
  xpcAccessibleMacEvent::FireEvent(self, notification, userInfo);

  if (gfxPlatform::IsHeadless()) {
    // Using a headless toolkit for tests and whatnot, posting accessibility
    // notification won't work.
    return;
  }

  if (![self isAccessibilityElement]) {
    // If this is an ignored object, don't expose it to system.
    return;
  }

  if (userInfo) {
    NSAccessibilityPostNotificationWithUserInfo(
        GetObjectOrRepresentedView(self), notification, userInfo);
  } else {
    NSAccessibilityPostNotification(GetObjectOrRepresentedView(self),
                                    notification);
  }
}

- (BOOL)moxBlockSelector:(SEL)selector {
  return NO;
}

- (NSArray*)moxChildren {
  return @[];
}

- (NSArray*)moxUnignoredChildren {
  NSMutableArray* unignoredChildren =
      [[[NSMutableArray alloc] init] autorelease];
  NSArray* allChildren = [self moxChildren];

  for (MOXAccessibleBase* nativeChild in allChildren) {
    if ([nativeChild moxIgnoreWithParent:self]) {
      // If this child should be ignored get its unignored children.
      // This will in turn recurse to any unignored descendants if the
      // child is ignored.
      [unignoredChildren
          addObjectsFromArray:[nativeChild moxUnignoredChildren]];
    } else {
      [unignoredChildren addObject:nativeChild];
    }
  }

  return unignoredChildren;
}

- (id<mozAccessible>)moxParent {
  return nil;
}

- (id<mozAccessible>)moxUnignoredParent {
  id<mozAccessible> nativeParent = [self moxParent];
  if (!nativeParent) {
    return nil;
  }

  if (![nativeParent isAccessibilityElement]) {
    if ([nativeParent conformsToProtocol:@protocol(MOXAccessible)] &&
        [nativeParent respondsToSelector:@selector(moxUnignoredParent)]) {
      // Cast away the protocol so we can cast to another protocol.
      id bareNativeParent = nativeParent;
      id<MOXAccessible> moxNativeParent = bareNativeParent;
      return [moxNativeParent moxUnignoredParent];
    }
  }

  return GetObjectOrRepresentedView(nativeParent);
}

- (BOOL)moxIgnoreWithParent:(MOXAccessibleBase*)parent {
  return [parent moxIgnoreChild:self];
}

- (BOOL)moxIgnoreChild:(MOXAccessibleBase*)child {
  return NO;
}

- (id<MOXTextMarkerSupport>)moxTextMarkerDelegate {
  return nil;
}

- (BOOL)moxIsLiveRegion {
  return NO;
}

#pragma mark -

// objc-style description (from NSObject); not to be confused with the
// accessible description above.
- (NSString*)description {
  if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Debug)) {
    if ([self isSelectorSupported:@selector(moxMozDebugDescription)]) {
      return [self moxMozDebugDescription];
    }
  }

  return [NSString stringWithFormat:@"<%@: %p %@>",
                                    NSStringFromClass([self class]), self,
                                    [self moxRole]];
}

- (BOOL)isExpired {
  return mIsExpired;
}

- (void)expire {
  MOZ_ASSERT(!mIsExpired, "expire called an expired mozAccessible!");

  mIsExpired = YES;

  [self moxPostNotification:NSAccessibilityUIElementDestroyedNotification];
}

- (id<MOXAccessible>)moxFindAncestor:(BOOL (^)(id<MOXAccessible> moxAcc,
                                               BOOL* stop))findBlock {
  for (id element = self; [element conformsToProtocol:@protocol(MOXAccessible)];
       element = [element moxUnignoredParent]) {
    BOOL stop = NO;
    if (findBlock(element, &stop)) {
      return element;
    }

    if (stop || ![element respondsToSelector:@selector(moxUnignoredParent)]) {
      break;
    }
  }

  return nil;
}

#pragma mark - Private

- (BOOL)isSelectorSupported:(SEL)selector {
  return
      [self respondsToSelector:selector] && ![self moxBlockSelector:selector];
}

@end
