/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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"

using namespace mozilla::a11y;

@interface NSObject (MOXAccessible)

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

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

@end

@implementation NSObject (MOXAccessible)

- (BOOL)isMozAccessible {
  return [self conformsToProtocol:@protocol(mozAccessible)];
}

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

@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_ABORT_BLOCK_NIL;

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

  static NSMutableDictionary* attributesForEachClass = nil;

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

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

  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];
      }
    }

    // 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_ABORT_BLOCK_NIL;
}

- (id)accessibilityAttributeValue:(NSString*)attribute {
  NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
  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];
    }
  }

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

  if ([value hasMozAccessibles]) {
    // 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];
    for (NSUInteger i = 0; i < arrSize; i++) {
      id<mozAccessible> mozAcc = GetObjectOrRepresentedView(value[i]);
      if ([mozAcc isAccessibilityElement]) {
        [arr addObject:mozAcc];
      }
    }

    return arr;
  }

  return value;

  NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
}

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

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

  NSDictionary* setters = mac::AttributeSetters();
  if (setters[attribute]) {
    SEL selector = NSSelectorFromString(setters[attribute]);
    return ([self isSelectorSupported:selector]);
  }

  NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NO);
}

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

  if ([self isExpired]) {
    return;
  }

  NSDictionary* setters = mac::AttributeSetters();
  if (setters[attribute]) {
    SEL selector = NSSelectorFromString(setters[attribute]);
    if ([self isSelectorSupported:selector]) {
      [self performSelector:selector withObject:value];
    }
  }

  NS_OBJC_END_TRY_ABORT_BLOCK;
}

- (NSArray*)accessibilityActionNames {
  NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;

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

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

  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_ABORT_BLOCK_NIL;
}

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

  if ([self isExpired]) {
    return;
  }

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

  NS_OBJC_END_TRY_ABORT_BLOCK;
}

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

- (NSArray*)accessibilityParameterizedAttributeNames {
  NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
  return @[];
  NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
}

- (id)accessibilityAttributeValue:(NSString*)attribute forParameter:(id)parameter {
  NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
  return nil;
  NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
}

- (id)accessibilityHitTest:(NSPoint)point {
  NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
  return [self moxHitTest:point];
  NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
}

- (id)accessibilityFocusedUIElement {
  NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
  return [self moxFocusedUIElement];
  NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
}

- (BOOL)isAccessibilityElement {
  return YES;
}

- (BOOL)accessibilityNotifiesWhenDestroyed {
  return YES;
}

#pragma mark - MOXAccessible protocol

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

- (id)moxFocusedUIElement {
  return GetObjectOrRepresentedView(self);
}

- (void)moxPostNotification:(NSString*)notification {
  // This sends events via nsIObserverService to be consumed by our mochitests.
  xpcAccessibleMacInterface::FireEvent(self, notification);

  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;
  }

  NSAccessibilityPostNotification(GetObjectOrRepresentedView(self), notification);
}

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

#pragma mark -

- (BOOL)isExpired {
  return mIsExpired;
}

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

  mIsExpired = YES;

  [self moxPostNotification:NSAccessibilityUIElementDestroyedNotification];
}

#pragma mark - Private

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

@end
