/*
 * Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License, version 2.0,
 * as published by the Free Software Foundation.
 *
 * This program is also distributed with certain software (including
 * but not limited to OpenSSL) that is licensed under separate terms, as
 * designated in a particular file or component or in included license
 * documentation.  The authors of MySQL hereby grant you an additional
 * permission to link the program and your derivative works with the
 * separately licensed software that they have included with MySQL.
 * This program is distributed in the hope that it will be useful,  but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See
 * the GNU General Public License, version 2.0, for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 
 */

#include "utilities.h"

#include "accessible.h"

#import "Carbon/Carbon.h" // For keyboard constants.

using namespace aal;
using namespace geometry;

extern "C" AXError _AXUIElementGetWindow(AXUIElementRef, CGWindowID *out);

// Used for conversion of CFStringRef instances to std::string.
#define toString(ref) [(__bridge NSString *)ref UTF8String]

std::shared_ptr<Accessible> Accessible::_systemRoot = nullptr;

//----------------------------------------------------------------------------------------------------------------------

static std::string getNativeRole(AXUIElementRef ref, CFStringRef type = kAXRoleAttribute) {
  CFTypeRef result;
  AXError error = AXUIElementCopyAttributeValue(ref, type, &result);

  // Roles are supported for all elements. So, if there is an error it must be something else.
  if (error == kAXErrorCannotComplete || result == nullptr)
    return "";

  std::string roleString = toString(result);
  CFRelease(result);

  return roleString;
}

//----------------------------------------------------------------------------------------------------------------------

static bool nativeRoleIsOneOf(AXUIElementRef ref, std::vector<CFStringRef> roles, CFStringRef type = kAXRoleAttribute) {
  CFTypeRef value;
  AXError error = AXUIElementCopyAttributeValue(ref, type, &value);
  if (error != kAXErrorSuccess)
    return false;

  bool result = false;
  for (auto &role : roles) {
    if (CFEqual(value, role)) {
      result = true;
      break;
    }
  }
  CFRelease(value);

  return result;
}

//----------------------------------------------------------------------------------------------------------------------

Accessible::Accessible(AXUIElementRef accessible) : _native(accessible), _role(Role::Unknown) {
  if (_native != nullptr) {
    CFRetain(_native);
    _role = determineRole(_native);
  }
}

//----------------------------------------------------------------------------------------------------------------------

Accessible::~Accessible() {
  if (_native != nullptr)
    CFRelease(_native);
}

//----------------------------------------------------------------------------------------------------------------------

bool Accessible::accessibilitySetup() {
  NSDictionary *options = @{ (__bridge NSString *)kAXTrustedCheckOptionPrompt: @YES };
  BOOL accessibilityEnabled = AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options) == TRUE;
  return accessibilityEnabled;
}

//----------------------------------------------------------------------------------------------------------------------

AccessibleRef Accessible::getByPid(const int pid) {
  AXUIElementRef element = AXUIElementCreateApplication(pid);
  auto result = std::unique_ptr<Accessible>(new Accessible(element));
  CFRelease(element);

  // Try to get the role of the app (which is always valid, unless we cannot connect).
  if (!result)
    return nullptr;

  if (getNativeRole(result->_native).empty())
    return nullptr;

  return result;
}

//----------------------------------------------------------------------------------------------------------------------

AccessibleRef Accessible::clone() const {
  return AccessibleRef(new Accessible(_native));
}

//----------------------------------------------------------------------------------------------------------------------

bool Accessible::isRoot() const {
  AXUIElementRef parent = nullptr;
  AXError error = AXUIElementCopyAttributeValue(_native, kAXParentAttribute, (CFTypeRef *)&parent);
  bool result = (error != kAXErrorSuccess) || parent == nullptr;
  if (parent != nullptr)
    CFRelease(parent);

  return result;
}

//----------------------------------------------------------------------------------------------------------------------

bool Accessible::isValid() const {
  if (_native == nullptr)
    return false;

  CFTypeRef value;
  AXError error = AXUIElementCopyAttributeValue(_native, kAXRoleAttribute, &value);
  if (error == kAXErrorSuccess) {
    CFRelease(value);
    return true;
  }
  return false;
}

//----------------------------------------------------------------------------------------------------------------------

size_t Accessible::getHash() const {
  std::hash<std::string> stringHash;
  std::hash<size_t> numberHash;
  size_t result = 17;

  result = result * 31 + stringHash(getName());
  result = result * 31 + numberHash(static_cast<size_t>(getRole()));
  result = result * 31 + stringHash(getID());

  auto parent = getParent();
  if (parent)
    result = result * 31 + parent->getHash();

  return result;
}

//----------------------------------------------------------------------------------------------------------------------

bool Accessible::canFocus()const {
  // Don't check only for settable values. Sometimes focus cannot be set when it is already.
  return isSettable(_native, kAXFocusedAttribute) || getBoolValue(_native, kAXFocusedAttribute, "focused", true);
}

//----------------------------------------------------------------------------------------------------------------------

bool Accessible::isFocused() const {
  return getBoolValue(_native, kAXFocusedAttribute, "focused");
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::setFocused() {
  // Cannot set focus twice, so check if it is set already.
  if (!getBoolValue(_native, kAXFocusedAttribute, "focused"))
    setBoolValue(_native, kAXFocusedAttribute, true, "focused");
}

//----------------------------------------------------------------------------------------------------------------------

bool Accessible::isEnabled() const {
  return getBoolValue(_native, kAXEnabledAttribute, "enabled");
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::setEnabled(bool value) {
  return setBoolValue(_native, kAXEnabledAttribute, value, "enabled");
}

//----------------------------------------------------------------------------------------------------------------------

/**
 * This value is not the opposite of isReadOnly(). Being read-only means you cannot change the element's value at all.
 * Being not-editable means you cannot change the text (caption) of the element via typing or direct assignment, but
 * instead only via it's associated drop down.
 * It's a value only related to combobox types (NSComboBox + NSPopupButton here).
 */
bool Accessible::isEditable() const {
  return nativeRoleIsOneOf(_native, { kAXComboBoxRole });
}

//----------------------------------------------------------------------------------------------------------------------

bool Accessible::isReadOnly() const {
  return !isSettable(_native, kAXValueAttribute);
}

//----------------------------------------------------------------------------------------------------------------------

bool Accessible::isSecure() const {
  if (_role != Role::TextBox)
    throw std::runtime_error("The secure mode is only supported for text boxes.");

  CFTypeRef value;
  AXUIElementCopyAttributeValue(_native, kAXSubroleAttribute, &value);
  if (value == nullptr)
    return false;

  bool result = CFEqual(value, kAXSecureTextFieldSubrole);
  CFRelease(value);

  return result;
}

//----------------------------------------------------------------------------------------------------------------------

bool Accessible::isHorizontal() const {
  switch (_role) {
    case Role::ScrollBar:
    case Role::Slider: {
      bool result = false;
      CFTypeRef orientation;
      AXUIElementCopyAttributeValue(_native, kAXOrientationAttribute, &orientation);
      if (orientation != nullptr) {
        result = CFEqual(orientation, kAXHorizontalOrientationValue);
        CFRelease(orientation);
      }
      return result;
    }

    case Role::SplitContainer: {
      // Determining the splitter (group) orientation requires to get the first native splitter child element
      // and check its orientation. This is also orthogonal to the splitter group's orientation.
      CFArrayRef splitters;
      AXUIElementCopyAttributeValues(_native, kAXSplittersAttribute, 0, 1, &splitters);
      if (splitters == nullptr)
        return false;

      bool result = false;
      AXUIElementRef splitter = static_cast<AXUIElementRef>(CFArrayGetValueAtIndex(splitters, 0));

      CFTypeRef orientation;
      AXUIElementCopyAttributeValue(splitter, kAXOrientationAttribute, &orientation);
      if (orientation != nullptr) {
        result = CFEqual(orientation, kAXVerticalOrientationValue); // Orthogonal to the group's orientation.
        CFRelease(orientation);
      }
      CFRelease(splitters);

      return result;
    }

    default:
      throw std::runtime_error("This element does not support layout informations.");
  }
}

//----------------------------------------------------------------------------------------------------------------------

CheckState Accessible::getCheckState() const {
  if (_role != Role::CheckBox && _role != Role::RadioButton)
    throw std::runtime_error("Check states not supported by this element.");

  switch (getNumberValue(_native, kAXValueAttribute, "value").intValue) {
    case 0:
      return CheckState::Unchecked;
    case 1:
      return CheckState::Checked;
    default:
      return CheckState::Indeterminate;
  }
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::setCheckState(CheckState state) {
  if (_role != Role::CheckBox && _role != Role::RadioButton)
    throw std::runtime_error("Check states not supported by this element.");

  switch (state) {
    case CheckState::Unchecked:
      setValue(0);
      break;
    case CheckState::Checked:
      setValue(1);
      break;
    default:
      setValue(2);
      break;
  }
}

//----------------------------------------------------------------------------------------------------------------------

bool Accessible::isExpandable() const {
  switch (_role) {
    case Role::ComboBox:
      return true;
    case Role::Row:
    case Role::Expander:
      return isSupported(_native, kAXDisclosingAttribute);
    default:
      throw std::runtime_error("Expand state not supported by this element.");
  }
  return false;
}

//----------------------------------------------------------------------------------------------------------------------

bool Accessible::isExpanded() {
  switch (_role) {
    case Role::ComboBox: { // We treat comboboxes and popup buttons both as comboboxes here.
      if (nativeRoleIsOneOf(_native, { kAXComboBoxRole }))
        return getBoolValue(_native, kAXExpandedAttribute, "expanded");

      auto children = getChildren(_native);
      return children.count > 0;
    }
    case Role::Row:
    case Role::Expander:
      return getBoolValue(_native, kAXDisclosingAttribute, "expanded");
    default:
      throw std::runtime_error("Expand state not supported by this element.");
  }

  return false;
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::setExpanded(bool value) {
  switch (_role) {
    case Role::ComboBox:
      if (nativeRoleIsOneOf(_native, { kAXComboBoxRole }))
        setBoolValue(_native, kAXExpandedAttribute, value, "expanded");
      else {
        if (value)
          AXUIElementPerformAction(_native, kAXShowMenuAction);
        else {
          auto children = getChildren(_native);
          if (children.count > 0) {
            AXUIElementRef menu = (__bridge AXUIElementRef)children[0];
            AXUIElementPerformAction(menu, kAXCancelAction);
          }
        }
      }
      break;
    case Role::Row:
    case Role::Expander:
      setBoolValue(_native, kAXDisclosingAttribute, value, "expanded");
      break;
    default:
      throw std::runtime_error("Expand state not supported by this element.");
  }
}

//----------------------------------------------------------------------------------------------------------------------

double Accessible::getValue() const {
  return [getNumberValue(_native, kAXValueAttribute, "value") doubleValue];
}

//----------------------------------------------------------------------------------------------------------------------

double Accessible::getMaxValue() const {
  return [getNumberValue(_native, kAXMaxValueAttribute, "max value") doubleValue];
}

//----------------------------------------------------------------------------------------------------------------------

double Accessible::getMinValue() const {
  return [getNumberValue(_native, kAXMinValueAttribute, "min value") doubleValue];
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::setValue(double value) {
  setNumberValue(_native, kAXValueAttribute, [NSNumber numberWithDouble: value], "value");
}

//----------------------------------------------------------------------------------------------------------------------

double Accessible::getRange() const {
  if (_role != Role::ScrollBar)
    throw std::runtime_error("The range attribute is only supported for scrollbars.");

  //return getMaxValue() - getMinValue();
  return 0; // No known way to determine this value.
}

//----------------------------------------------------------------------------------------------------------------------

std::string Accessible::getActiveTabPage() const {
  if (_role != Role::TabView)
    throw std::runtime_error("Only tab views have tab pages.");

  for (auto &page : tabPages()) {
    if (page->getValue() == 1)
      return page->getTitle();
  }
  return "";
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::setActiveTabPage(std::string const& name) {
  if (_role != Role::TabView)
    throw std::runtime_error("Only tab views have tab pages.");

  for (auto &page : tabPages()) {
    if (page->getTitle() == name)
      page->click();
      //page->setFocused(); Works too, but is slower.
  }
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::activate() {
  if (_role != Role::TabPage && _role != Role::MenuItem && _role != Role::Menu)
    throw std::runtime_error("Cannot activate this element.");

  auto native = _native;
  if (_role == Role::Menu)
    native = getParent(_native);
  press(native);
}

//----------------------------------------------------------------------------------------------------------------------

bool Accessible::isActiveTab() const {
  if (_role != Role::TabPage)
    throw std::runtime_error("This element has no activity status.");

  return getBoolValue(_native, kAXValueAttribute, "value");
}

//----------------------------------------------------------------------------------------------------------------------

bool Accessible::isSelected() const {
  if (_role != Role::Row && _role != Role::Column)
    throw std::runtime_error("This element cannot be selected.");

  return getBoolValue(_native, kAXSelectedAttribute, "selected");
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::setSelected(bool value) {
  if (_role != Role::Row && _role != Role::Column)
    throw std::runtime_error("This element cannot be selected.");

  setBoolValue(_native, kAXSelectedAttribute, value, "selected");
}

//----------------------------------------------------------------------------------------------------------------------

double Accessible::getScrollPosition() const {
  if (_role != Role::ScrollBar)
    throw std::runtime_error("The scroll position is only supported by scrollbars.");

  return [getNumberValue(_native, kAXValueAttribute, "scroll position") doubleValue];
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::setScrollPosition(double value) {
  if (_role != Role::ScrollBar)
    throw std::runtime_error("The scroll position is only supported by scrollbars.");

  setNumberValue(_native, kAXValueAttribute, [NSNumber numberWithDouble: value], "scroll position");
}

//----------------------------------------------------------------------------------------------------------------------

/**
 * Show to element's (context) menu, if supported.
 */
void Accessible::showMenu() const {
  AXUIElementSetMessagingTimeout(_native, 0.1);
  AXError error = AXUIElementPerformAction(_native, kAXShowMenuAction);
  AXUIElementSetMessagingTimeout(_native, 0);
  handleUnsupportedError(error, "context menu");
}

//----------------------------------------------------------------------------------------------------------------------

bool Accessible::menuShown() const {
  if (_role != Role::Menu)
    throw std::runtime_error("Shown attribute only valid for menus.");

  auto visibleChildren = getChildren(_native, 10, true);
  return visibleChildren.count > 0;
}

//----------------------------------------------------------------------------------------------------------------------

std::string Accessible::getID() const {
  return getStringValue(_native, kAXIdentifierAttribute, "id", true);
}

//----------------------------------------------------------------------------------------------------------------------

std::string Accessible::getName() const {
  // Accessibility on macOS is pretty confusing. There's a field `accessibilityLabel`, which when set uses the
  // description key (not the label value key as one would think). Still the content of the description is shown as
  // label in the accessibility inspector. If a title is set via `accessibilityTitle`, the label is set instead if no
  // explicit label value has been assigned.
  auto result = getStringValue(_native, kAXDescriptionAttribute, "name", true);
  if (result.empty()) // Some elements (particularly those created internally) have no description/label.
    result = getStringValue(_native, kAXTitleAttribute, "name", true);

  return result;
}

//----------------------------------------------------------------------------------------------------------------------

std::string Accessible::getHelp() const {
  return getStringValue(_native, kAXHelpAttribute, "help");
}

//----------------------------------------------------------------------------------------------------------------------

/**
 * Returns true if this is an internal element, like a window zoom button or other implicitly added content.
 */
bool Accessible::isInternal() const {
  // There are quite a few containers that are implicitely created (e.g. windows + scrollboxes), which would qualify
  // as internal elements. However, if they are taken out we lose all their (potentially not-internal) child elements.
  CFIndex count;
  AXError error = AXUIElementGetAttributeValueCount(_native, kAXChildrenAttribute, &count);
  if (error == kAXErrorSuccess && count > 0) {
    return false;
  }

  if (getName().empty()) {
    auto identifier = getID();
    if (mga::Utilities::hasPrefix(identifier, "_NS:"))
      return true;
  }

  // Some internal elements have no internal identifier.
  if (nativeRoleIsOneOf(_native,
    { kAXCloseButtonSubrole, kAXZoomButtonSubrole, kAXFullScreenButtonSubrole, kAXMinimizeButtonSubrole },
    kAXSubroleAttribute)) {
    return true;
  }

  return false;
}

//----------------------------------------------------------------------------------------------------------------------

bool Accessible::equals(Accessible *other) const {
  return CFEqual(this->_native, other->_native);
}

//----------------------------------------------------------------------------------------------------------------------

AccessibleRef Accessible::getParent() const {
  AXUIElementRef parent = getParent(_native);
  if (parent == nullptr)
    return nullptr;

  Accessible *accessible = new Accessible(parent);
  CFRelease(parent);

  return AccessibleRef(accessible);
}

//----------------------------------------------------------------------------------------------------------------------

/**
 * If this element is part of a treeview or grid this function returns the row to which the element belongs
 * (otherwise an invalid reference is returned). Rows + columns are not everywhere natively supported in which case
 * we fake an instance to make this concept consistent accross platforms.
 */
AccessibleRef Accessible::getContainingRow() const {
  if (_role == Role::Row)
    return clone();

  auto run = getParent(_native);
  while (run != nullptr) {
    if (nativeRoleIsOneOf(run, { kAXRowRole })) {
      auto result = AccessibleRef(new Accessible(run));
      CFRelease(run);
      return result;
    }

    auto temp = getParent(run);
    CFRelease(run);
    run = temp;
  }

  return nullptr;
}

//----------------------------------------------------------------------------------------------------------------------

AccessibleRef Accessible::getHorizontalScrollBar() const {
  CFTypeRef value;
  AXError error = AXUIElementCopyAttributeValue(_native, kAXHorizontalScrollBarAttribute, &value);
  handleUnsupportedError(error, "horizontal scrollbar");

  auto result = AccessibleRef(new Accessible(static_cast<AXUIElementRef>(value)));
  CFRelease(value);
  return result;
}

//----------------------------------------------------------------------------------------------------------------------

AccessibleRef Accessible::getVerticalScrollBar() const {
  CFTypeRef value;
  AXError error = AXUIElementCopyAttributeValue(_native, kAXVerticalScrollBarAttribute, &value);
  handleUnsupportedError(error, "vertical scrollbar");

  auto result = AccessibleRef(new Accessible(static_cast<AXUIElementRef>(value)));
  CFRelease(value);
  return result;
}

//----------------------------------------------------------------------------------------------------------------------

AccessibleRef Accessible::getHeader() const {
  if (_role != Role::Column)
    throw std::runtime_error("This element does not have a heading.");

  CFTypeRef value;
  AXError error = AXUIElementCopyAttributeValue(_native, kAXHeaderAttribute, &value);
  handleUnsupportedError(error, "header");

  auto result = AccessibleRef(new Accessible(static_cast<AXUIElementRef>(value)));
  CFRelease(value);
  return result;
}

//----------------------------------------------------------------------------------------------------------------------

AccessibleRef Accessible::getCloseButton() const {
  if (_role != Role::TabPage)
    throw std::runtime_error("This element does not have a close button.");

  CFTypeRef value;
  AXError error = AXUIElementCopyAttributeValue(_native, kAXCloseButtonAttribute, &value);
  if (value == nullptr)
    return nullptr;
  handleUnsupportedError(error, "close button");

  auto result = AccessibleRef(new Accessible(static_cast<AXUIElementRef>(value)));
  CFRelease(value);
  return result;
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::show() {
  NOT_IMPLEMENTED;
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::bringToFront() {
  switch (_role) {
    case Role::Window: {
      AXError error = AXUIElementPerformAction(_native, kAXRaiseAction);
      handleUnsupportedError(error, "bringToFront");
      // fallthrough
    }

    case Role::Application: {
      pid_t pid;
      AXUIElementGetPid(_native, &pid);
      NSRunningApplication *application = [NSRunningApplication runningApplicationWithProcessIdentifier: pid];
      [application activateWithOptions: NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps];
      break;
    }

    default: {
      throw std::runtime_error("Action not supported by this element");
      break;
    }
  }
}

//----------------------------------------------------------------------------------------------------------------------

@interface HighlightWindow : NSWindow
@end

@implementation HighlightWindow
- (NSTimeInterval)animationResizeTime: (NSRect)newFrame {
  return 0.05;
}

@end

static HighlightWindow *highlightWindow = nil;

void Accessible::highlight(NSColor *color) const {
  if (isValid() && hasBounds(_native)) {
    Rectangle bounds = getBounds(true);
    NSRect frame = NSMakeRect(bounds.position.x, bounds.position.y, bounds.size.width, bounds.size.height);
    float screenHeight = NSMaxY([[NSScreen.screens objectAtIndex: 0] frame]);
    frame.origin.y = screenHeight - (bounds.position.y + bounds.size.height);

    if (highlightWindow == nil) {
      highlightWindow = [[HighlightWindow alloc] initWithContentRect: frame
                                                           styleMask: NSWindowStyleMaskBorderless
                                                             backing: NSBackingStoreBuffered
                                                               defer: YES];
      highlightWindow.level = NSScreenSaverWindowLevel;
      highlightWindow.hasShadow = NO;
      highlightWindow.opaque = NO;
      highlightWindow.backgroundColor = color;
      highlightWindow.alphaValue = 0.5;
      highlightWindow.ignoresMouseEvents = YES;
      [highlightWindow orderFront: nil];
    } else {
      highlightWindow.backgroundColor = color;
      [highlightWindow setFrame: frame display: YES animate: YES];
      [highlightWindow orderFront: nil];
    }
    //[NSRunLoop.currentRunLoop runMode: NSDefaultRunLoopMode
    //                       beforeDate: [NSDate.date dateByAddingTimeInterval: 0.010]];
  }
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::removeHighlight() const {
  if (highlightWindow != nil) {
    [highlightWindow orderOut: nil];
    //[NSRunLoop.currentRunLoop runMode: NSDefaultRunLoopMode
    //                       beforeDate: [NSDate.date dateByAddingTimeInterval: 0.010]];
  }
}

//----------------------------------------------------------------------------------------------------------------------

bool Accessible::isHighlightActive() const {
  return highlightWindow.visible;
}

//----------------------------------------------------------------------------------------------------------------------

std::string Accessible::getPlatformRoleName() const {
  return getNativeRole(_native);
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::printNativeInfo() const {
  printInfo(_native);
}

//----------------------------------------------------------------------------------------------------------------------

static std::map<std::string, std::string> attributeToPropertyMap = {
  // informational attributes
  { toString(kAXTitleAttribute), "Title" },
  { toString(kAXDescriptionAttribute), "Description" },
  { toString(kAXHelpAttribute), "Help" },
  { toString(kAXIdentifierAttribute), "Internal Identifier" },

  // hierarchy or relationship attributes
  { toString(kAXParentAttribute), "Parent" },
  { toString(kAXChildrenAttribute), "Children" },
  { toString(kAXSelectedChildrenAttribute), "Selected Children" },
  { toString(kAXVisibleChildrenAttribute), "Visible Children" },
  { toString(kAXWindowAttribute), "Window" },
  { toString(kAXTopLevelUIElementAttribute), "Top Level UI Element" },
  { toString(kAXTitleUIElementAttribute), "Title UI Element" },

  // visual state attributes
  { toString(kAXEnabledAttribute), "Enabled" },
  { toString(kAXFocusedAttribute), "Focused" },
  { toString(kAXPositionAttribute), "Position" },
  { toString(kAXSizeAttribute), "Size" },

  // value attributes
  { toString(kAXValueAttribute), "Value" },
  { toString(kAXValueDescriptionAttribute), "" },
  { toString(kAXMinValueAttribute), "Min Value" },
  { toString(kAXMaxValueAttribute), "Max Value" },
  { toString(kAXValueIncrementAttribute), "Value Increment" },
  { toString(kAXValueWrapsAttribute), "Wraps Content" },

  // text-specific attributes
  { toString(kAXSelectedTextAttribute), "Selected Text" },
  { toString(kAXSelectedTextRangeAttribute), "Selected Text Range" },
  { toString(kAXSelectedTextRangesAttribute), "Selected Text Ranges" },
  { toString(kAXVisibleCharacterRangeAttribute), "Visible Character Range" },
  { toString(kAXNumberOfCharactersAttribute), "Character Count" },

  // window, sheet, or drawer-specific attributes
  { toString(kAXMainAttribute), "Main Window" },
  { toString(kAXMinimizedAttribute), "Minimized" },
  { toString(kAXCloseButtonAttribute), "Close Button" },
  { toString(kAXZoomButtonAttribute), "Zoom Button" },
  { toString(kAXMinimizeButtonAttribute), "Minimize Button" },
  { toString(kAXToolbarButtonAttribute), "Toolbar Button" },
  { toString(kAXFullScreenButtonAttribute), "Full Screen Button" },
  { toString(kAXGrowAreaAttribute), "Grow Area" },
  { toString(kAXModalAttribute), "Modal" },
  { toString(kAXDefaultButtonAttribute), "Default Button" },
  { toString(kAXCancelButtonAttribute), "Cancel Button" },

  // menu or menu item-specific attributes
  { toString(kAXMenuItemCmdCharAttribute), "" },
  { toString(kAXMenuItemCmdVirtualKeyAttribute), "" },
  { toString(kAXMenuItemCmdGlyphAttribute), "" },
  { toString(kAXMenuItemCmdModifiersAttribute), "" },
  { toString(kAXMenuItemMarkCharAttribute), "" },
  { toString(kAXMenuItemPrimaryUIElementAttribute), "" },

  // application element-specific attributes
  { toString(kAXMenuBarAttribute), "Menu Bar" },
  { toString(kAXWindowsAttribute), "Windows" },
  { toString(kAXFrontmostAttribute), "Front Most Application" },
  { toString(kAXHiddenAttribute), "Hidden" },
  { toString(kAXMainWindowAttribute), "Main Window" },
  { toString(kAXFocusedWindowAttribute), "Focused Window" },
  { toString(kAXFocusedUIElementAttribute), "Focused Element" },

  // date/time-specific attributes
  { toString(kAXHourFieldAttribute), "Hour Field" },
  { toString(kAXMinuteFieldAttribute), "Minute Field" },
  { toString(kAXSecondFieldAttribute), "Second Field" },
  { toString(kAXAMPMFieldAttribute), "AM/PM Field" },
  { toString(kAXDayFieldAttribute), "Day Field" },
  { toString(kAXMonthFieldAttribute), "Month Field" },
  { toString(kAXYearFieldAttribute), "Year Field" },

  // table, outline, or browser-specific attributes
  { toString(kAXRowsAttribute), "Rows" },
  { toString(kAXVisibleRowsAttribute), "Visible Rows" },
  { toString(kAXSelectedRowsAttribute), "Selected Rows" },
  { toString(kAXColumnsAttribute), "Columns" },
  { toString(kAXVisibleColumnsAttribute), "Visible Columns" },
  { toString(kAXSelectedColumnsAttribute), "Selected Columns" },
  { toString(kAXSortDirectionAttribute), "Sort Direction" },
  { toString(kAXColumnHeaderUIElementsAttribute), "Column Headers" },
  { toString(kAXIndexAttribute), "Index" },
  { toString(kAXDisclosingAttribute), "Has Disclosing Arrow" },
  { toString(kAXDisclosedRowsAttribute), "Disclosed Rows" },

  // miscellaneous or role-specific attributes
  { toString(kAXHorizontalScrollBarAttribute), "Horizontal Scrollbar" },
  { toString(kAXVerticalScrollBarAttribute), "Vertical Scrollbar" },
  { toString(kAXOrientationAttribute), "Orientation" },
  { toString(kAXHeaderAttribute), "Header" },
  { toString(kAXEditedAttribute), "Edited" },
  { toString(kAXTabsAttribute), "Tabs" },
  { toString(kAXOverflowButtonAttribute), "Overflow Button" },
  { toString(kAXFilenameAttribute), "Filename" },
  { toString(kAXExpandedAttribute), "Expanded" },
  { toString(kAXSelectedAttribute), "Selected" },
  { toString(kAXSplittersAttribute), "Splitters" },
  { toString(kAXContentsAttribute), "Contents" },
  { toString(kAXNextContentsAttribute), "Following Elements" },
  { toString(kAXPreviousContentsAttribute), "Preceding Elements" },
  { toString(kAXDocumentAttribute), "Document" },
  { toString(kAXIncrementorAttribute), "Incrementor" },
  { toString(kAXDecrementButtonAttribute), "Decrement Button" },
  { toString(kAXIncrementButtonAttribute), "Increment Button" },
  { toString(kAXColumnTitleAttribute), "Column Title" },
  { toString(kAXURLAttribute), "URL" },
  { toString(kAXLabelValueAttribute), "Label Value" },
  { toString(kAXShownMenuUIElementAttribute), "Context Menu Items" },
  { toString(kAXIsApplicationRunningAttribute), "Application Running" },
  { toString(kAXFocusedApplicationAttribute), "Application Focused" },
  { toString(kAXElementBusyAttribute), "Busy" },

  // Attributes without a constant.
  { "AXEnhancedUserInterface", "Enhanced UI" },
  { "AXFullScreen", "Full Screen" },
  { "AXFrame", "Bounds" },
  { "AXPlaceholderValue", "Placeholder Value" },
};

static std::set<std::string> ignoredAttributes = {
  toString(kAXRoleAttribute),
  toString(kAXRoleDescriptionAttribute),
  toString(kAXSubroleAttribute),
  toString(kAXExtrasMenuBarAttribute),
  toString(kAXInsertionPointLineNumberAttribute),
  toString(kAXInsertionPointLineNumberAttribute),
  toString(kAXGrowAreaAttribute),
  toString(kAXProxyAttribute),
  toString(kAXDisclosedByRowAttribute),
  toString(kAXServesAsTitleForUIElementsAttribute),
  toString(kAXLinkedUIElementsAttribute),
  toString(kAXSharedFocusElementsAttribute),
  toString(kAXAllowedValuesAttribute),
  toString(kAXSharedTextUIElementsAttribute),
  toString(kAXSharedCharacterRangeAttribute),
  toString(kAXProxyAttribute),
  toString(kAXExtrasMenuBarAttribute),

  // matte-specific attributes
  toString(kAXMatteHoleAttribute),
  toString(kAXMatteContentUIElementAttribute),

  // ruler-specific attributes
  toString(kAXMarkerUIElementsAttribute),
  toString(kAXUnitsAttribute),
  toString(kAXUnitDescriptionAttribute),
  toString(kAXMarkerTypeAttribute),
  toString(kAXMarkerTypeDescriptionAttribute),

  toString(kAXLabelUIElementsAttribute),
  toString(kAXAlternateUIVisibleAttribute),

  "AXFunctionRowTopLevelElements",
  "AXChildrenInNavigationOrder",
  "AXTextInputMarkedRange",
  "AXAuditIssues",
  "AXSections",
};

static std::map<std::string, std::string> subRoleMap = {
  // standard subroles
  { toString(kAXCloseButtonSubrole), "Close Button" },
  { toString(kAXMinimizeButtonSubrole), "Minimize Button" },
  { toString(kAXZoomButtonSubrole), "Zoom Button" },
  { toString(kAXToolbarButtonSubrole), "Toolbar Button" },
  { toString(kAXFullScreenButtonSubrole), "Full Screen Button" },
  { toString(kAXSecureTextFieldSubrole), "Secure Text Field" },
  { toString(kAXTableRowSubrole), "Table Row" },
  { toString(kAXOutlineRowSubrole), "Outline Row" },

  // new subroles
  { toString(kAXStandardWindowSubrole), "Standard Window" },
  { toString(kAXDialogSubrole), "Dialog" },
  { toString(kAXSystemDialogSubrole), "System Dialog" },
  { toString(kAXFloatingWindowSubrole), "Floating Window" },
  { toString(kAXSystemFloatingWindowSubrole), "System Floating Window" },
  { toString(kAXIncrementArrowSubrole), "Increment Arrow" },
  { toString(kAXDecrementArrowSubrole), "Decrement Arrow" },
  { toString(kAXIncrementPageSubrole), "Increment Page" },
  { toString(kAXDecrementPageSubrole), "Decrement Page" },
  { toString(kAXSortButtonSubrole), "Sort Button" },
  { toString(kAXSearchFieldSubrole), "Search Field" },
  { toString(kAXTimelineSubrole), "Time Line" },
  { toString(kAXRatingIndicatorSubrole), "Rating Indicator" },
  { toString(kAXContentListSubrole), "Content List" },
  { toString(kAXDefinitionListSubrole), "Definition List" },
  { toString(kAXDescriptionListSubrole), "Description List" },
  { toString(kAXToggleSubrole), "Toggle" },
  { toString(kAXSwitchSubrole), "Switch" },

  // dock subroles
  { toString(kAXApplicationDockItemSubrole), "Application Dock Item" },
  { toString(kAXDocumentDockItemSubrole), "Document Dock Item" },
  { toString(kAXFolderDockItemSubrole), "Folder Dock Item" },
  { toString(kAXMinimizedWindowDockItemSubrole), "Minimized Window Dock Item" },
  { toString(kAXURLDockItemSubrole), "Dock Item" },
  { toString(kAXDockExtraDockItemSubrole), "Dock Extra Item" },
  { toString(kAXTrashDockItemSubrole), "Trash Dock Item" },
  { toString(kAXSeparatorDockItemSubrole), "Separator Dock Item" },
  { toString(kAXProcessSwitcherListSubrole), "Process Switcher List" },

  // Others
  { "AXTabButton", "Tab" },
  { "AXTextLink", "Hyperlink" },
};

// Attributes refering to other UI elements.
static std::set<std::string> references = {
 toString(kAXParentAttribute),
 toString(kAXChildrenAttribute),
 toString(kAXSelectedChildrenAttribute),
 toString(kAXVisibleChildrenAttribute),
 toString(kAXWindowAttribute),
 toString(kAXTopLevelUIElementAttribute),
 toString(kAXTitleUIElementAttribute),
 toString(kAXMainAttribute),
 toString(kAXCloseButtonAttribute),
 toString(kAXZoomButtonAttribute),
 toString(kAXMinimizeButtonAttribute),
 toString(kAXToolbarButtonAttribute),
 toString(kAXFullScreenButtonAttribute),
 toString(kAXGrowAreaAttribute),
 toString(kAXDefaultButtonAttribute),
 toString(kAXCancelButtonAttribute),
 toString(kAXMenuBarAttribute),
 toString(kAXWindowsAttribute),
 toString(kAXMainWindowAttribute),
 toString(kAXFocusedWindowAttribute),
 toString(kAXFocusedUIElementAttribute),
 toString(kAXHourFieldAttribute),
 toString(kAXMinuteFieldAttribute),
 toString(kAXSecondFieldAttribute),
 toString(kAXAMPMFieldAttribute),
 toString(kAXDayFieldAttribute),
 toString(kAXMonthFieldAttribute),
 toString(kAXYearFieldAttribute),
 toString(kAXRowsAttribute),
 toString(kAXVisibleRowsAttribute),
 toString(kAXSelectedRowsAttribute),
 toString(kAXColumnsAttribute),
 toString(kAXVisibleColumnsAttribute),
 toString(kAXSelectedColumnsAttribute),
 toString(kAXColumnHeaderUIElementsAttribute),
 toString(kAXDisclosedRowsAttribute),
 toString(kAXHorizontalScrollBarAttribute),
 toString(kAXVerticalScrollBarAttribute),
 toString(kAXHeaderAttribute),
 toString(kAXTabsAttribute),
 toString(kAXOverflowButtonAttribute),
 toString(kAXSplittersAttribute),
 toString(kAXContentsAttribute),
 toString(kAXNextContentsAttribute),
 toString(kAXPreviousContentsAttribute),
 toString(kAXDocumentAttribute),
 toString(kAXIncrementorAttribute),
 toString(kAXDecrementButtonAttribute),
 toString(kAXIncrementButtonAttribute),
 toString(kAXShownMenuUIElementAttribute),
};

static std::map<std::string, std::string> actionMap = {
  { toString(kAXPressAction), "Click Element"},
  { toString(kAXIncrementAction), "Increment Value" },
  { toString(kAXDecrementAction), "Decrement Value" },
  { toString(kAXConfirmAction), "Confirm" },
  { toString(kAXCancelAction), "Cancel" },
  { toString(kAXShowAlternateUIAction), "Show Alternate UI" },
  { toString(kAXShowDefaultUIAction), "Show Default UI" },
  { toString(kAXRaiseAction), "Bring to Front" },
  { toString(kAXShowMenuAction), "Show Menu" },
  { toString(kAXPickAction), "Select Item" },
  { "AXScrollLeftByPage", "Scroll Page Left" },
  { "AXScrollRightByPage", "Scroll Page Right" },
  { "AXScrollUpByPage", "Scroll Page Up" },
  { "AXScrollDownByPage", "Scroll Page Down" },
};

/**
 * Returns human readable details about this instance.
 */
AccessibleDetails Accessible::getDetails() const {
  std::string subRole = getNativeRole(_native, kAXSubroleAttribute);
  auto subRoleIterator = subRoleMap.find(subRole);
  if (subRoleIterator != subRoleMap.end())
    subRole = subRoleIterator->second;
  AccessibleDetails result = { roleToFriendlyString(_role), subRole, {}, {} };

  CFArrayRef array;
  AXUIElementCopyAttributeNames(_native, &array);
  if (array != nil) {
    NSArray *properties = (__bridge NSArray *)array;
    for (NSString *property in properties) {
      std::string name = property.UTF8String;
      if (ignoredAttributes.count(name) > 0)
        continue;

      bool containsReference = references.count(name) > 0;
      auto iterator = attributeToPropertyMap.find(name);
      if (!iterator->second.empty()) {
        if (iterator != attributeToPropertyMap.end())
          name = iterator->second;
        Boolean settable = false;
        AXUIElementIsAttributeSettable(_native, (CFStringRef)property, &settable);

        std::string stringValue;
        CFTypeRef value;
        AXError error = AXUIElementCopyAttributeValue(_native, (CFStringRef)property, &value);
        if (error == kAXErrorSuccess) {
          stringValue = valueDescription((AXValueRef)value);
        }

        result.properties.push_back({ name, stringValue, !settable, containsReference });
      }
    }

    CFRelease(array);
  }

  AXUIElementCopyActionNames(_native, &array);
  if (array != nil) {
    NSArray *actions = (__bridge NSArray *)array;
    for (NSString *action in actions) {
      std::string description = "<empty>";
      CFStringRef value;
      AXError error = AXUIElementCopyActionDescription(_native, (CFStringRef)action, &value);
      if (error == kAXErrorSuccess)
        description = toString(value);

      std::string name = action.UTF8String;
      auto iterator = actionMap.find(name);
      if (iterator != actionMap.end())
        name = iterator->second;
      result.actions.push_back({ name, description });
    }
    CFRelease(array);
  }

  return result;
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::takeScreenShot(std::string const& path, bool onlyWindow, geometry::Rectangle rect) const {
  CGImageRef image = nullptr;
  CGRect r = CGRectMake(rect.position.x, rect.position.y, rect.size.width, rect.size.height);
  if (onlyWindow) {
    // If getting a screenshot for a window only the given coordinates are in the window coordinate system
    // and must be converted to screen coordinates first.
    CFTypeRef pointRef;
    CGPoint point;
    AXError error = AXUIElementCopyAttributeValue(_native, kAXPositionAttribute, &pointRef);
    handleUnsupportedError(error, "position");

    AXValueGetValue(static_cast<AXValueRef>(pointRef), static_cast<AXValueType>(kAXValueCGPointType), (void *)&point);
    CFRelease(pointRef);
    r.origin.x += point.x;
    r.origin.y += point.y;

    CGWindowID windowId;
    if (_AXUIElementGetWindow(_native, &windowId) == kAXErrorSuccess) {
      image = CGWindowListCreateImage(r, kCGWindowListOptionIncludingWindow, windowId,
                                      kCGWindowImageBoundsIgnoreFraming);
    } else
      throw std::runtime_error("Can't get window id");
  } else {
    image = CGWindowListCreateImage(r, kCGWindowListOptionAll, kCGNullWindowID, kCGWindowImageDefault);
  }

  if (image) {
    writeImageToFile(image, [NSString stringWithUTF8String: path.c_str()]);
    CFRelease(image);
  } else
    throw std::runtime_error("Can't take screenshot");
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::saveImage(std::string const& path) const {
  if (_role != Role::Image)
    throw std::runtime_error("Can only save the content of an image element.");

  NOT_IMPLEMENTED;
}

//----------------------------------------------------------------------------------------------------------------------

/**
 * Retrieves the frame of the view represented by this accessible.
 * Origin is the upper left corner of the screen holding the menu bar or the upper left corner of this UI element,
 * depending on the screenCoordinates parameter (so +y points down).
 */
Rectangle Accessible::getBounds(bool screenCoordinates) const {
  return Accessible::getBounds(_native, screenCoordinates);
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::setBounds(geometry::Rectangle const& bounds) {
  Accessible::setBounds(_native, bounds);
}

//----------------------------------------------------------------------------------------------------------------------

std::string Accessible::getText() const {
  return getStringValue(_native, kAXValueAttribute, "text");
}

//----------------------------------------------------------------------------------------------------------------------

std::string Accessible::getTitle() const {
  if (_role == Role::GroupBox) {
    CFTypeRef titleElement;
    AXError error = AXUIElementCopyAttributeValue(_native, kAXTitleUIElementAttribute, &titleElement);
    if (error != kAXErrorSuccess || titleElement == nullptr) {
      return nullptr;
    }
    std::string title = getStringValue(static_cast<AXUIElementRef>(titleElement), kAXValueAttribute, "title");
    return title;
  }

  return getStringValue(_native, kAXTitleAttribute, "title");
}

//----------------------------------------------------------------------------------------------------------------------

std::size_t Accessible::getCaretPosition() const {
  AXValueRef theValue;
  AXError error = AXUIElementCopyAttributeValue(_native, kAXSelectedTextRangeAttribute, (CFTypeRef *)&theValue);
  handleUnsupportedError(error, "selected text range");

  NSRange range;
  AXValueGetValue(theValue, static_cast<AXValueType>(kAXValueCFRangeType), (void *)&range);
  return static_cast<std::size_t>(range.location);
}

//----------------------------------------------------------------------------------------------------------------------

/**
 * Setting the caret position clears the current selection range.
 */
void Accessible::setCaretPosition(size_t position) {
  NSRange range;
  range.location = position;
  range.length = 0;

  AXValueRef valueRef = AXValueCreate(static_cast<AXValueType>(kAXValueCFRangeType), static_cast<const void *>(&range));
  AXUIElementSetAttributeValue(_native, kAXSelectedTextRangeAttribute, valueRef);
}

//----------------------------------------------------------------------------------------------------------------------

std::size_t Accessible::getCharacterCount() const {
  return getStringValue(_native, kAXValueAttribute, "value").size();
}

//----------------------------------------------------------------------------------------------------------------------

std::set<size_t> Accessible::getSelectedIndexes() const {
  if (_role != Role::ComboBox && _role != Role::IconView)
    throw std::runtime_error("This element does not support selected indexes.");

  std::set<size_t> result;
  if (_native) {
    AXUIElementRef list = _native;
    NSArray *contentList = nil;

    if (_role == Role::ComboBox) {
      if (nativeRoleIsOneOf(_native, { kAXPopUpButtonRole })) {
        // A popup button then. Can have a single child (a menu).
        AXUIElementRef menu = getFirstChild(_native);
        if (menu != nullptr) {
          auto children = getChildren(menu);
          CFRelease(menu);
          for (NSUInteger i = 0; i < children.count; ++i) {
            if (!getStringValue((__bridge AXUIElementRef)children[i], kAXMenuItemMarkCharAttribute,
                                "menu item marker").empty()) {
              result.insert(i);
            }
          }
        }
        return result;
      } else {
        auto subParts = getChildren(_native, 2);

        // Selected indices can only be determined if the combobox is expanded currently.
        if (subParts.count < 2)
          return result;

        AXUIElementRef scrollArea = (__bridge AXUIElementRef)(subParts[1]);
        CFTypeRef content;
        AXUIElementCopyAttributeValue(scrollArea, kAXContentsAttribute, &content);
        if (!content)
          return result;

        contentList = (NSArray *)CFBridgingRelease(content);
        list = (__bridge AXUIElementRef)(contentList[0]);
      }
    }

    auto children = getChildren(list);
    if (children == nullptr) {
      return result;
    }

    CFArrayRef selected;
    AXUIElementCopyAttributeValues(list, kAXSelectedChildrenAttribute, 0, 99999, &selected);
    if (selected == nullptr) {
      return result;
    }

    NSArray *selectedList = (NSArray *)CFBridgingRelease(selected);
    for  (id entry in selectedList) {
      result.insert([children indexOfObject: entry]);
    }
  }

  return result;
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::setSelectedIndexes(std::set<size_t> const& indexes) {
  if (_role != Role::ComboBox && _role != Role::IconView)
    throw std::runtime_error("This element does not support selected indexes.");

  if (_native) {
    AXUIElementRef list = _native;
    NSArray *contentList = nil;

    if (_role == Role::ComboBox) {
      if (nativeRoleIsOneOf(_native, { kAXPopUpButtonRole })) {
        AXUIElementRef menu = getFirstChild(_native);
        if (menu != nullptr) {
          auto children = getChildren(menu);
          CFRelease(menu);
          if (!indexes.empty() && *indexes.begin() < children.count) {
            press((__bridge AXUIElementRef)children[*indexes.begin()]);
          }
        }
        return;
      } else {
        auto subParts = getChildren(_native, 2);
        if (subParts.count < 2)
          return;

        AXUIElementRef scrollArea = (__bridge AXUIElementRef)(subParts[1]);
        CFTypeRef content;
        AXUIElementCopyAttributeValue(scrollArea, kAXContentsAttribute, &content);
        if (!content)
          return;

        contentList = (NSArray *)CFBridgingRelease(content);
        list = (__bridge AXUIElementRef)contentList[0];
      }
    }

    auto children = getChildren(list);
    if (children == nil) {
      return;
    }

    NSMutableArray *selection = [NSMutableArray new];
    for (size_t index : indexes) {
      [selection addObject: [children objectAtIndex: index]];
    }

    AXError error = AXUIElementSetAttributeValue(list, kAXSelectedChildrenAttribute, (__bridge CFTypeRef)selection);
    handleUnsupportedError(error, "selected indexes");
  }
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::insertText(const std::size_t offset, const std::string &text) {
  NSRange range;
  range.location = offset;
  range.length = offset;
  AXValueRef valueRef = AXValueCreate(static_cast<AXValueType>(kAXValueCFRangeType), static_cast<const void *>(&range));
  AXUIElementSetAttributeValue(_native, kAXSelectedTextRangeAttribute, valueRef);

  setStringValue(_native, kAXSelectedTextAttribute, text, "text");
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::setText(std::string const& text) {
  setStringValue(_native, kAXValueAttribute, text, "text");
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::setTitle(std::string const& text) {
  setStringValue(_native, kAXTitleAttribute, text, "title");
}

//----------------------------------------------------------------------------------------------------------------------

std::string Accessible::getDescription() const {
  return "";
}

//----------------------------------------------------------------------------------------------------------------------

std::string Accessible::getSelectedText() const {
  return getStringValue(_native, kAXSelectedTextAttribute, "selected text");
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::setSelectedText(std::string const& text) {
  return setStringValue(_native, kAXSelectedTextAttribute, text, "selected text");
}

//----------------------------------------------------------------------------------------------------------------------

aal::TextRange Accessible::getSelectionRange() const {
  AXValueRef theValue;
  AXError error = AXUIElementCopyAttributeValue(_native, kAXSelectedTextRangeAttribute, (CFTypeRef *)&theValue);
  handleUnsupportedError(error, "selected text range");

  NSRange range;
  AXValueGetValue(theValue, static_cast<AXValueType>(kAXValueCFRangeType), (void *)&range);
  return aal::TextRange(range.location, range.location + range.length);
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::setSelectionRange(TextRange range) {
  NSRange nativeRange;
  nativeRange.location = range.start;
  nativeRange.length = range.end - range.start;
  AXValueRef valueRef = AXValueCreate((AXValueType)kAXValueCFRangeType, (const void *)&nativeRange);
  AXUIElementSetAttributeValue(_native, kAXSelectedTextRangeAttribute, valueRef);
}

//----------------------------------------------------------------------------------------------------------------------

std::string Accessible::getDate() const {
  if (_role != Role::DatePicker)
    throw std::runtime_error("This element does not support date values.");

  return getStringValue(_native, kAXValueAttribute, "value");
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::setDate(std::string const& date) {
  if (_role != Role::DatePicker)
    throw std::runtime_error("This element does not support date values.");

  setStringValue(_native, kAXValueAttribute, date, "value");
}

//----------------------------------------------------------------------------------------------------------------------

static CGEventType eventTypeFromButton(MouseButton button, bool down) {
  switch (button) {
      case MouseButton::Right:
      return down ? kCGEventRightMouseDown : kCGEventRightMouseUp;
    case MouseButton::Middle:
      return down ? kCGEventOtherMouseDown : kCGEventOtherMouseUp;
    default:
      return down ? kCGEventLeftMouseDown : kCGEventLeftMouseUp;
  }
}

//----------------------------------------------------------------------------------------------------------------------

static void sendMouseEvent(CGEventType type, CGPoint position, CGMouseButton button) {
  // The mouse button parameter is ignored, except for "other" mouse buttons.
  CGEventRef mouseEvent = CGEventCreateMouseEvent(nullptr, type, position, button);
  CGEventSetFlags(mouseEvent, kCGEventFlagMaskNonCoalesced);
  CGEventPost(kCGSessionEventTap, mouseEvent);
  CFRelease(mouseEvent);
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::mouseDown(geometry::Point pos, MouseButton button) {
  CGEventType eventType = eventTypeFromButton(button, true);
  sendMouseEvent(eventType, CGPointMake(pos.x, pos.y), kCGMouseButtonRight);
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::mouseUp(geometry::Point pos, MouseButton button) {
  CGEventType eventType = eventTypeFromButton(button, false);
  sendMouseEvent(eventType, CGPointMake(pos.x, pos.y), kCGMouseButtonRight);
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::mouseMove(geometry::Point pos) const {
  CGEventRef event = CGEventCreate(nil);
  CGPoint currentPos = CGEventGetLocation(event);
  CFRelease(event);
  sendMouseEvent(kCGEventMouseMoved, CGPointMake(currentPos.x + pos.x, currentPos.y + pos.y), kCGMouseButtonLeft);
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::mouseMoveTo(geometry::Point pos) const {
  sendMouseEvent(kCGEventMouseMoved, CGPointMake(pos.x, pos.y), kCGMouseButtonLeft);
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::mouseDrag(geometry::Point source, geometry::Point target, MouseButton button) {
  if (button != MouseButton::Left && button != MouseButton::Right)
    return;

  CGEventType downEventType = button == MouseButton::Left ? kCGEventLeftMouseDown : kCGEventRightMouseDown;
  CGEventType dragEventType = button == MouseButton::Left ? kCGEventLeftMouseDragged : kCGEventRightMouseDragged;
  CGPoint sourcePoint = CGPointMake(source.x, source.y);
  CGPoint targetPoint = CGPointMake(target.x, target.y);

  // Start with a mouse move event (which isn't strictly necessary) to make code working that determines
  // elements on mouse hover.
  CGEventRef mouseMoveEvent = CGEventCreateMouseEvent(NULL, kCGEventMouseMoved, sourcePoint, kCGMouseButtonLeft);
  CGEventPost(kCGSessionEventTap, mouseMoveEvent);
  CFRelease(mouseMoveEvent);
  [NSThread sleepForTimeInterval: 0.1];

  CGEventRef mouseDownEvent = CGEventCreateMouseEvent(NULL, downEventType, sourcePoint, kCGMouseButtonLeft);
  CGEventPost(kCGSessionEventTap, mouseDownEvent);
  CFRelease(mouseDownEvent);

  [NSThread sleepForTimeInterval: 0.1];

  CGEventRef mouseDragEvent = CGEventCreateMouseEvent(NULL, dragEventType, targetPoint, kCGMouseButtonLeft);
  CGEventPost(kCGSessionEventTap, mouseDragEvent);
  CFRelease(mouseDragEvent);
  [NSThread sleepForTimeInterval: 0.1];

  CGEventRef mouseUpEvent = CGEventCreateMouseEvent(NULL, kCGEventLeftMouseUp, sourcePoint, kCGMouseButtonLeft);
  CGEventPost(kCGSessionEventTap, mouseUpEvent);
  CFRelease(mouseUpEvent);
  [NSThread sleepForTimeInterval: 0.1];

}

//----------------------------------------------------------------------------------------------------------------------

geometry::Point Accessible::getMousePosition() const {
  CGEventRef event = CGEventCreate(nil);
  CGPoint currentPos = CGEventGetLocation(event);
  CFRelease(event);
  return geometry::Point(currentPos.x, currentPos.y);
}

//----------------------------------------------------------------------------------------------------------------------

// Key enum -> native keycodes map. Index is aal::Key.
static std::vector<CGKeyCode> keyCodeMap = {
  0xFFFF,
  kVK_ANSI_0, kVK_ANSI_1, kVK_ANSI_2, kVK_ANSI_3, kVK_ANSI_4, kVK_ANSI_5, kVK_ANSI_6, kVK_ANSI_7, kVK_ANSI_8, kVK_ANSI_9,
  kVK_ANSI_KeypadPlus, kVK_ANSI_KeypadMinus,
  kVK_ANSI_A, kVK_ANSI_B, kVK_ANSI_C, kVK_ANSI_D, kVK_ANSI_E, kVK_ANSI_F, kVK_ANSI_G, kVK_ANSI_H, kVK_ANSI_I,
  kVK_ANSI_J, kVK_ANSI_K, kVK_ANSI_L, kVK_ANSI_M, kVK_ANSI_N, kVK_ANSI_O, kVK_ANSI_P, kVK_ANSI_Q, kVK_ANSI_R,
  kVK_ANSI_S, kVK_ANSI_T, kVK_ANSI_U, kVK_ANSI_V, kVK_ANSI_W, kVK_ANSI_X, kVK_ANSI_Y, kVK_ANSI_Z,

  kVK_Tab, 0 /* backspace */, kVK_Return, kVK_ANSI_Period, kVK_ANSI_Comma, kVK_ANSI_Semicolon, kVK_ANSI_Slash,
  kVK_ANSI_Backslash, kVK_ANSI_LeftBracket, kVK_ANSI_RightBracket,

  kVK_Delete, kVK_UpArrow, kVK_Escape, kVK_DownArrow, kVK_LeftArrow, kVK_RightArrow, kVK_PageUp, kVK_PageDown, kVK_End,
  kVK_Home, kVK_Space,

  kVK_F1, kVK_F2, kVK_F3, kVK_F4, kVK_F5, kVK_F6, kVK_F7, kVK_F8, kVK_F9, kVK_F10, kVK_F11, kVK_F12
};

//----------------------------------------------------------------------------------------------------------------------

static CGEventFlags modifierToFlags(aal::Modifier modifier) {
  CGEventFlags result = 0;

  if (containsModifier(modifier, aal::Modifier::ShiftLeft) || containsModifier(modifier, aal::Modifier::ShiftRight))
    result |= kCGEventFlagMaskShift;
  if (containsModifier(modifier, aal::Modifier::ControlLeft) || containsModifier(modifier, aal::Modifier::ControlRight))
    result |= kCGEventFlagMaskControl;
  if (containsModifier(modifier, aal::Modifier::AltLeft) || containsModifier(modifier, aal::Modifier::AltRight))
    result |= kCGEventFlagMaskAlternate;
  if (containsModifier(modifier, aal::Modifier::MetaLeft) || containsModifier(modifier, aal::Modifier::MetaRight))
    result |= kCGEventFlagMaskCommand;

  return result;
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::keyDown(aal::Key k, aal::Modifier modifier) const {
  CGEventRef keyDownEvent = CGEventCreateKeyboardEvent(NULL, keyCodeMap[static_cast<size_t>(k)], true);
  CGEventSetFlags(keyDownEvent, modifierToFlags(modifier));

  pid_t pid;
  AXUIElementGetPid(_native, &pid);
  CGEventPostToPid(pid, keyDownEvent);

  CFRelease(keyDownEvent);
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::keyUp(aal::Key k, aal::Modifier modifier) const {
  CGEventRef keyUpEvent = CGEventCreateKeyboardEvent(NULL, keyCodeMap[static_cast<size_t>(k)], false);
  CGEventSetFlags(keyUpEvent, modifierToFlags(modifier));

  pid_t pid;
  AXUIElementGetPid(_native, &pid);
  CGEventPostToPid(pid, keyUpEvent);

  CFRelease(keyUpEvent);
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::keyPress(aal::Key k, aal::Modifier modifier) const {
  keyDown(k, modifier);
  keyUp(k, modifier);
}

//----------------------------------------------------------------------------------------------------------------------

/**
 * Sends key events to the target, generated from a given string. This way there's no need to deal with keyboard layouts.
 */
void Accessible::typeString(std::string const& input) const {
  if (input.empty())
    return;

  // Apparently there must be an initial HID event for an application (or just the modal runloop?) before the artificial
  // key events work actually. Otherwise the set unicode string call below might close modal windows under certain
  // circumstances (e.g. when running as sub process from another GUI app, like XCode), failing so the entire input
  // method.
  mouseMove({ 0, -10 });
  mouseMove({ 0, 10 });

  std::wstring utf16 = mga::Utilities::s2ws(input);
  CGEventSourceRef eventSource = CGEventSourceCreate(kCGEventSourceStateCombinedSessionState);

  CGEventRef keyDownEvent = CGEventCreateKeyboardEvent(eventSource, 0, true);
  CGEventRef keyUpEvent = CGEventCreateKeyboardEvent(eventSource, 0, false);

  for (auto iterator : utf16) {
    UniChar temp = iterator;
    CGEventKeyboardSetUnicodeString(keyDownEvent, 1, &temp);
    CGEventPost(kCGSessionEventTap, keyDownEvent);
    CGEventKeyboardSetUnicodeString(keyUpEvent, 1, &temp);
    CGEventPost(kCGSessionEventTap, keyUpEvent);
  }

  CFRelease(keyDownEvent);
  CFRelease(keyUpEvent);
  CFRelease(eventSource);
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::click() {
  press(_native);
}

//----------------------------------------------------------------------------------------------------------------------

/**
 * Simulates pressing <return>, which is used when setting a selection index in a combobox (which can be both NSCombobox
 * and NSPopupButton, the latter doesn't support confirm).
 */
void Accessible::confirm(bool checkError) {
  AXError error = AXUIElementPerformAction(_native, kAXConfirmAction);
  if (checkError) // Not always wanted.
    handleUnsupportedError(error, "confirm");
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::stepUp() {
  if (_role != Role::Stepper)
    throw std::runtime_error("Only stepper elements support this action.");

  AXError error = AXUIElementPerformAction(_native, kAXIncrementAction);
  handleUnsupportedError(error, "increment");
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::stepDown() {
  if (_role != Role::Stepper)
    throw std::runtime_error("Only stepper elements support this action.");

  AXError error = AXUIElementPerformAction(_native, kAXDecrementAction);
  handleUnsupportedError(error, "decrement");
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::scrollLeft() {
  if (_role != Role::ScrollBox)
    throw std::runtime_error("Only scrollbox elements support this action.");

  // Seems there are no constants for these actions.
  // Ignore errors here (we already checked the role). On macOS 10.13 it reports an unsupported attribut error
  // which is weird, given that we perform an action.
  AXUIElementPerformAction(_native, CFSTR("AXScrollLeftByPage"));
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::scrollRight() {
  if (_role != Role::ScrollBox)
    throw std::runtime_error("Only scrollbox elements support this action.");

  // Seems there are no constants for these actions.
  AXUIElementPerformAction(_native, CFSTR("AXScrollRightByPage"));
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::scrollUp() {
  if (_role != Role::ScrollBox)
    throw std::runtime_error("Only scrollbox elements support this action.");

  AXUIElementPerformAction(_native, CFSTR("AXScrollUpByPage"));
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::scrollDown() {
  if (_role != Role::ScrollBox)
    throw std::runtime_error("Only scrollbox elements support this action.");

  AXUIElementPerformAction(_native, CFSTR("AXScrollDownByPage"));
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::increment() {
  if (_role != Role::Slider)
    throw std::runtime_error("Only slider elements support this action.");

  AXUIElementPerformAction(_native, kAXIncrementAction);
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::decrement() {
  if (_role != Role::Slider)
    throw std::runtime_error("Only slider elements support this action.");

  AXError error = AXUIElementPerformAction(_native, kAXDecrementAction);
  handleUnsupportedError(error, "decrement");
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::children(AccessibleList &result, bool recursive) const {
  if (_native) {
    auto children = getChildren(_native);
    if (children == nil)
      return;

    for (NSUInteger i = 0; i < children.count; ++i) {
      AXUIElementRef ref = (__bridge AXUIElementRef)(children[i]);
      Accessible *childAcc = new Accessible(ref);
      result.emplace_back(childAcc);

      if (recursive)
        childAcc->children(result, recursive);
    }
  }
}

//----------------------------------------------------------------------------------------------------------------------

AccessibleList Accessible::children() const {
  AccessibleList result;
  if (_native) {
    auto children = getChildren(_native);
    if (children == nil)
      return result;

    for (NSUInteger i = 0; i < children.count; ++i) {
      AXUIElementRef ref = (__bridge AXUIElementRef)(children[i]);
      result.emplace_back(new Accessible(ref));
    }
  }
  return result;
}

//----------------------------------------------------------------------------------------------------------------------

AccessibleList Accessible::windows() const {
  AccessibleList result;
  if (_native) {
    CFArrayRef array;
    AXUIElementCopyAttributeValues(_native, kAXWindowsAttribute, 0, 99999, &array);

    if (array == nullptr)
      return result;

    NSArray *windows = (NSArray *)CFBridgingRelease(array);
    for (NSUInteger i = 0; i < windows.count; ++i) {
      AXUIElementRef ref = (__bridge AXUIElementRef)(windows[i]);
      result.emplace_back(new Accessible(ref));
    }
  }
  return result;
}

//----------------------------------------------------------------------------------------------------------------------

AccessibleList Accessible::tabPages() const {
  if (_role != Role::TabView)
    throw std::runtime_error("This element has no tabs.");

  AccessibleList result;
  if (_native) {
    CFTypeRef array;
    AXUIElementCopyAttributeValue(_native, kAXTabsAttribute, &array);

    if (array == nullptr)
      return result;

    NSArray *tabs = (NSArray *)CFBridgingRelease(array);
    for (NSUInteger i = 0; i < tabs.count; ++i) {
      AXUIElementRef ref = (__bridge AXUIElementRef)(tabs[i]);
      result.emplace_back(new Accessible(ref));

      // Need to set the role explicitly, as on macOS tabpages are in reality buttons.
      // There are no "physical" tabpages. They are simulated by the tab buttons.
      result.back()->_role = Role::TabPage;
    }
  }
  return result;
}

//----------------------------------------------------------------------------------------------------------------------

AccessibleList Accessible::rows() const {
  if (_role != Role::TreeView && _role != Role::Grid)
    throw std::runtime_error("This element has no rows.");

  AccessibleList result;
  if (_native) {
    CFArrayRef rows;
    AXUIElementCopyAttributeValues(_native, kAXRowsAttribute, 0, 99999, &rows);

    if (rows == nullptr)
      return result;

    CFIndex i, c = CFArrayGetCount(rows);
    for (i = 0; i < c; ++i) {
      AXUIElementRef ref = static_cast<AXUIElementRef>(CFArrayGetValueAtIndex(rows, i));
      result.emplace_back(new Accessible(ref));
      result.back()->_role = Role::Row;
    }

    CFRelease(rows);
  }
  return result;
}

//----------------------------------------------------------------------------------------------------------------------

AccessibleList Accessible::rowEntries() const {
  if (_role != Role::Row)
    throw std::runtime_error("This element has no row entries.");

  AccessibleList result;
  if (_native) {
    auto children = getChildren(_native);
    if (children == nil)
      return result;

    // The first child entry is usually a group consisting of the disclosure triangle and the other content.
    // We only return that other content.
    if (children.count > 0) {
      AXUIElementRef first = (__bridge AXUIElementRef)(children[0]);
      if (nativeRoleIsOneOf(first, { kAXGroupRole })) {
        auto groupEntries = getChildren(first);
        for (NSUInteger i = 1; i < groupEntries.count; ++i) {
          AXUIElementRef entryRef = (__bridge AXUIElementRef)(groupEntries[i]);
          result.emplace_back(new Accessible(entryRef));
        }
      } else {
        result.emplace_back(new Accessible(first));
      }

      for (NSUInteger i = 1; i < children.count; ++i) {
        AXUIElementRef ref = (__bridge AXUIElementRef)(children[i]);
        result.emplace_back(new Accessible(ref));
      }
    }
  }

  return result;
}

//----------------------------------------------------------------------------------------------------------------------

AccessibleList Accessible::columns() const {
  if (_role != Role::TreeView && _role != Role::Grid)
    throw std::runtime_error("This element has no columns.");
  
  AccessibleList result;
  if (_native) {
    CFArrayRef columns;
    AXUIElementCopyAttributeValues(_native, kAXColumnsAttribute, 0, 99999, &columns);

    if (columns == nullptr)
      return result;

    CFIndex i, c = CFArrayGetCount(columns);
    for (i = 0; i < c; ++i) {
      AXUIElementRef ref = static_cast<AXUIElementRef>(CFArrayGetValueAtIndex(columns, i));
      result.emplace_back(new Accessible(ref));
    }

    CFRelease(columns);
  }
  return result;
}

//----------------------------------------------------------------------------------------------------------------------

AccessibleList Accessible::columnEntries() const {
  if (_role != Role::Column)
    throw std::runtime_error("This element has no column entries.");

  AccessibleList result;
  if (_native) {
    CFArrayRef rows;
    AXUIElementCopyAttributeValues(_native, kAXRowsAttribute, 0, 99999, &rows);

    if (rows == nullptr)
      return result;

    CFIndex i, c = CFArrayGetCount(rows);
    for (i = 0; i < c; ++i) {
      AXUIElementRef ref = static_cast<AXUIElementRef>(CFArrayGetValueAtIndex(rows, i));
      if (nativeRoleIsOneOf(ref, { kAXGroupRole })) {
        auto groupEntries = getChildren(ref);
        for (NSUInteger j = 1; j < groupEntries.count; ++j) {
          AXUIElementRef entryRef = (__bridge AXUIElementRef)(groupEntries[j]);
          result.emplace_back(new Accessible(entryRef));
        }
      } else {
        result.emplace_back(new Accessible(ref));
      }
    }

    CFRelease(rows);
  }
  return result;
}

//----------------------------------------------------------------------------------------------------------------------

AccessibleRef Accessible::fromPoint(geometry::Point point, Accessible *application) {
  if (_systemRoot == nullptr)
    _systemRoot.reset(new Accessible(AXUIElementCreateSystemWide()));

  AXUIElementRef element;
  if (AXUIElementCopyElementAtPosition(_systemRoot->_native, point.x, point.y, &element) != kAXErrorSuccess)
    return nullptr;

  pid_t elementPid;
  if (AXUIElementGetPid(element, &elementPid) != kAXErrorSuccess)
    return nullptr;

  pid_t pid;
  if (AXUIElementGetPid(application->_native, &pid) != kAXErrorSuccess || pid != elementPid)
    return nullptr;

  auto result = AccessibleRef(new Accessible(element));
  CFRelease(element);

  return result;
}

//----------------------------------------------------------------------------------------------------------------------

Role Accessible::determineRole(AXUIElementRef element) {
  static std::map<std::string, Role> roleMap = {
    { toString(kAXApplicationRole), Role::Application },
    { toString(kAXWindowRole), Role::Window },
    { toString(kAXButtonRole), Role::Button },
    { toString(kAXRadioButtonRole), Role::RadioButton },
    { toString(kAXRadioGroupRole), Role::RadioGroup },
    { toString(kAXCheckBoxRole), Role::CheckBox },
    { toString(kAXComboBoxRole), Role::ComboBox },
    { toString(kAXPopUpButtonRole), Role::ComboBox },
    { toString(kAXDisclosureTriangleRole), Role::Expander },
    { toString(kAXTableRole), Role::Grid },
    { toString(kAXTextFieldRole), Role::TextBox },
    { toString(kAXTextAreaRole), Role::TextBox },
    { toString(kAXOutlineRole), Role::TreeView },
    { toString(kAXStaticTextRole), Role::Label },
    { toString(kAXMenuRole), Role::Menu },
    { toString(kAXMenuBarRole), Role::MenuBar },
    { toString(kAXMenuBarItemRole), Role::MenuItem },
    { toString(kAXMenuItemRole), Role::MenuItem },
    { toString(kAXSplitGroupRole), Role::SplitContainer },
    { toString(kAXSplitterRole), Role::Splitter },
    { toString(kAXGroupRole), Role::GroupBox },
    { toString(kAXImageRole), Role::Image },
    { toString(kAXTabGroupRole), Role::TabView },
    { "AXDateTimeArea", Role::DatePicker }, // Can't find a constant for this role.
    { toString(kAXRowRole), Role::Row },
    { toString(kAXColumnRole), Role::Column },
    { toString(kAXCellRole), Role::Column },
    { toString(kAXScrollAreaRole), Role::ScrollBox },
    { toString(kAXSliderRole), Role::Slider },
    { toString(kAXIncrementorRole), Role::Stepper },
    { toString(kAXListRole), Role::List },
    { toString(kAXGridRole), Role::IconView },
    { toString(kAXProgressIndicatorRole), Role::ProgressIndicator },
    { toString(kAXBusyIndicatorRole), Role::BusyIndicator },
    { toString(kAXScrollBarRole), Role::ScrollBar },
    { toString(kAXValueIndicatorRole), Role::ScrollThumb },
    { "AXLink", Role::HyperLink },
  };

  // For certain elements we use the subrole to get a better role description.
  std::string subRoleString = getNativeRole(element, kAXSubroleAttribute);
  if (subRoleString == "AXTabButton")
    return Role::TabPage;

  std::string roleString = getNativeRole(element);
  if (roleMap.find(roleString) != roleMap.end())
    return roleMap[roleString];

  return Role::Unknown;
}

//----------------------------------------------------------------------------------------------------------------------

NSArray* Accessible::getChildren(AXUIElementRef ref, size_t count, bool visibleOnly) {
  auto attribute = kAXChildrenAttribute;
  if (visibleOnly)
    attribute = kAXVisibleChildrenAttribute;
  return getArrayValue(ref, attribute, count);
}

//----------------------------------------------------------------------------------------------------------------------

NSArray* Accessible::getArrayValue(AXUIElementRef ref, CFStringRef attribute, size_t count) {
  CFArrayRef result = nullptr;
  AXError error = AXUIElementCopyAttributeValues(ref, attribute, 0, static_cast<CFIndex>(count), &result);
  if (error != kAXErrorSuccess) {
    return nil;
  }

  return (NSArray *)CFBridgingRelease(result);
}

//----------------------------------------------------------------------------------------------------------------------

std::string Accessible::getStringValue(AXUIElementRef ref, CFStringRef attribute,
                                       std::string const& attributeName, bool noThrow) {
  CFTypeRef result;
  AXError error = AXUIElementCopyAttributeValue(ref, attribute, &result);
  if (!noThrow)
    handleUnsupportedError(error, attributeName);

  if (result == nullptr || CFGetTypeID(result) != CFStringGetTypeID())
    return "";

  std::string text = toString(result);
  CFRelease(result);

  return text;
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::setStringValue(AXUIElementRef ref, CFStringRef attribute, std::string const& value,
                                std::string const& attributeName) {
  if (!isSettable(ref, attribute))
    throw std::runtime_error("Attribute cannot be set: " + attributeName);

  NSString *native = [NSString stringWithUTF8String: value.c_str()];
  AXError error = AXUIElementSetAttributeValue(ref, attribute, (__bridge CFStringRef)native);
  handleUnsupportedError(error, attributeName);
}

//----------------------------------------------------------------------------------------------------------------------

bool Accessible::getBoolValue(AXUIElementRef ref, CFStringRef attribute, std::string const& attributeName,
                              bool noThrow) {
  CFTypeRef value;
  AXError error = AXUIElementCopyAttributeValue(ref, attribute, &value);
  if (!noThrow)
    handleUnsupportedError(error, attributeName);
  else if (error != kAXErrorSuccess)
    return false;

  bool result = false;
  if (CFGetTypeID(value) == CFStringGetTypeID()) {
    NSString *s = (__bridge NSString *)value;
    result = s.boolValue;
  } else if (CFGetTypeID(value) == CFBooleanGetTypeID()) {
    result = value == kCFBooleanTrue;
  } if (CFGetTypeID(value) == CFNumberGetTypeID()) {
    NSNumber *n = (__bridge NSNumber *)value;
    result = n.boolValue;
  }

  CFRelease(value);

  return result;
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::setBoolValue(AXUIElementRef ref, CFStringRef attribute, bool value, std::string const& attributeName) {
  if (!isSettable(ref, attribute))
    throw std::runtime_error("Attribute cannot be set: " + attributeName);

  NSNumber *temp = [NSNumber numberWithBool: value];
  AXError error = AXUIElementSetAttributeValue(ref, attribute, (__bridge CFTypeRef)temp);
  handleUnsupportedError(error, attributeName);
}

//----------------------------------------------------------------------------------------------------------------------

NSNumber *Accessible::getNumberValue(AXUIElementRef ref, CFStringRef attribute, std::string const& attributeName,
                                     bool noThrow) {
  CFTypeRef result;
  AXError error = AXUIElementCopyAttributeValue(ref, attribute, &result);
  if (!noThrow)
    handleUnsupportedError(error, attributeName);
  else if (error != kAXErrorSuccess)
    return nil;

  return (__bridge NSNumber *)result;
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::setNumberValue(AXUIElementRef ref, CFStringRef attribute, NSNumber *value,
                                std::string const& attributeName) {
  if (!isSettable(ref, attribute))
    throw std::runtime_error("Attribute cannot be set: " + attributeName);

  AXError error = AXUIElementSetAttributeValue(ref, attribute, (__bridge CFTypeRef)value);
  handleUnsupportedError(error, attributeName);
}

//----------------------------------------------------------------------------------------------------------------------

AXUIElementRef Accessible::getElementValue(AXUIElementRef ref, bool noThrow) {
  CFTypeRef value;
  AXError error = AXUIElementCopyAttributeValue(ref, kAXValueAttribute, &value);
  if (!noThrow)
    handleUnsupportedError(error, "value");
  else if (error != kAXErrorSuccess)
    return nil;

  return static_cast<AXUIElementRef>(value);
}

//----------------------------------------------------------------------------------------------------------------------

bool Accessible::hasBounds(AXUIElementRef ref) {
  return isSupported(ref, kAXPositionAttribute) && isSupported(ref, kAXSizeAttribute);
}

//----------------------------------------------------------------------------------------------------------------------

Rectangle Accessible::getBounds(AXUIElementRef ref, bool screenCoordinates) {
  AXValueRef valueRef;
  CGSize size;
  CGPoint point;

  AXError error = AXUIElementCopyAttributeValue(ref, kAXPositionAttribute, (CFTypeRef *)&valueRef);
  handleUnsupportedError(error, "position");

  Rectangle result;
  if (error == kAXErrorNoValue) {
    return result;
  }

  AXValueGetValue(valueRef, static_cast<AXValueType>(kAXValueCGPointType), (void *)&point);
  CFRelease(valueRef);

  error = AXUIElementCopyAttributeValue(ref, kAXSizeAttribute, (CFTypeRef *)&valueRef);
  if (error != kAXErrorSuccess) {
    return Rectangle();
  }

  AXValueGetValue(valueRef, static_cast<AXValueType>(kAXValueCGSizeType), (void *)&size);
  CFRelease(valueRef);

  if (!screenCoordinates) {
    AXUIElementRef parent = getParent(ref);
    if (parent != nullptr && hasBounds(parent)) {
      Rectangle parentBounds;
      parentBounds = getBounds(parent, true);
      result = Rectangle(point.x - parentBounds.minX(), point.y - parentBounds.minY(), size.width, size.height);
    } else {
      result = Rectangle(point.x, point.y, size.width, size.height);
    }
  } else {
    result = Rectangle(point.x, point.y, size.width, size.height);
  }

  return result;
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::setBounds(AXUIElementRef ref, geometry::Rectangle const& bounds) {
  CGSize size = { static_cast<CGFloat>(bounds.size.width), static_cast<CGFloat>(bounds.size.height) };
  CGPoint point = { static_cast<CGFloat>(bounds.position.x), static_cast<CGFloat>(bounds.position.y) };

  AXValueRef valueRef = AXValueCreate(static_cast<AXValueType>(kAXValueCGPointType), &point);
  AXError error = AXUIElementSetAttributeValue(ref, kAXPositionAttribute, valueRef);
  CFRelease(valueRef);
  handleUnsupportedError(error, "position");

  valueRef = AXValueCreate(static_cast<AXValueType>(kAXValueCGSizeType), &size);
  error = AXUIElementSetAttributeValue(ref, kAXSizeAttribute, valueRef);
  CFRelease(valueRef);
  handleUnsupportedError(error, "size");
}

//----------------------------------------------------------------------------------------------------------------------

bool Accessible::isSupported(AXUIElementRef ref, CFStringRef attribute) {
  CFTypeRef result;
  AXError error = AXUIElementCopyAttributeValue(ref, attribute, &result);
  if (result != nullptr)
    CFRelease(result);

  return error != kAXErrorAttributeUnsupported;
}

//----------------------------------------------------------------------------------------------------------------------

bool Accessible::isSettable(AXUIElementRef ref, CFStringRef attribute) {
  Boolean settable;
  AXUIElementIsAttributeSettable(ref, attribute, &settable);

  return settable;
}

//----------------------------------------------------------------------------------------------------------------------

/**
 * Returns a one line description of the given value.
 */
std::string Accessible::valueDescription(AXValueRef value) {
  std::stringstream ss;

  switch (AXValueGetType(value)) {
    case kAXValueCGPointType: {
      CGPoint point;
      if (AXValueGetValue(value, kAXValueTypeCGPoint, &point)) {
        ss << "{ x: " << point.x << ", y: " << point.y << " }";
      }
      break;
    }
      
    case kAXValueCGSizeType: {
      CGSize size;
      if (AXValueGetValue(value, kAXValueTypeCGSize, &size)) {
        ss << "{ width: " << size.width << ", height: " << size.height << " }";
      }
      break;
    }

    case kAXValueCGRectType: {
      CGRect rect;
      if (AXValueGetValue(value, kAXValueTypeCGRect, &rect)) {
        ss << "{ x: " << rect.origin.x << ", y: " << rect.origin.y << ", width: "
          << rect.size.width << ", height: " << rect.size.height << " }";
      }
      break;
    }

    case kAXValueCFRangeType: {
      CFRange range;
      if (AXValueGetValue(value, kAXValueTypeCFRange, &range)) {
        ss << "{ location: " << range.location << ", length: " << range.length << " }";
      }
      break;
    }

    default:
      if (CFGetTypeID(value) == CFArrayGetTypeID()) {
        NSArray *array = (__bridge NSArray *)value;
        ss << array.count << (array.count == 1 ? " element" : " elements");
      } else if (CFGetTypeID(value) == AXUIElementGetTypeID()) {
        std::string name = getStringValue((AXUIElementRef)value, kAXDescriptionAttribute, "name", true);
        if (name.empty())
          name = getStringValue((AXUIElementRef)value, kAXTitleAttribute, "name", true);

        if (name.empty()) {
          ss << "<no name>";
        } else {
          ss << name;
        }

        Role role = determineRole((AXUIElementRef)value);
        ss << " (" << roleToFriendlyString(role) << ")";
      } else {
        ss << [[(__bridge id)value description] UTF8String];
      }

      break;
  }

  return ss.str();
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::handleUnsupportedError(AXError error, std::string const& attribute) {
  // The two exceptions here use a very short timeout and don't wait for completion (to avoid blocking MGA).
  // Hence they will also produce the cannot-complete error, but that's ok in those cases.
  if (error == kAXErrorCannotComplete && attribute != "context menu" && attribute != "press")
    throw std::runtime_error("Cannot complete the accessibility call. Probably lost connection to target app.");

  if (error == kAXErrorAttributeUnsupported)
    throw std::runtime_error("Unsupported attribute: " + attribute);
  if (error == kAXErrorActionUnsupported)
    throw std::runtime_error("Unsupported action: " + attribute);
  if (error == kAXErrorInvalidUIElement)
    throw std::runtime_error("The specified UI element is not valid");
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::writeImageToFile(CGImageRef image, NSString *path) const {
  CFURLRef url = (__bridge CFURLRef)[NSURL fileURLWithPath: path isDirectory: NO];
  CGImageDestinationRef destination = CGImageDestinationCreateWithURL(url, kUTTypePNG, 1, nil);
  if (!destination)
    throw std::runtime_error("Can't create file: " + std::string(path.UTF8String));

  CGImageDestinationAddImage(destination, image, nil);

  if (!CGImageDestinationFinalize(destination)) {
    CFRelease(destination);
    throw std::runtime_error("Error during save file: " + std::string(path.UTF8String));
  }

  CFRelease(destination);
}

//----------------------------------------------------------------------------------------------------------------------

/**
 * Returns the first child element of the given parent. The caller must unref the result (if not null).
 */
AXUIElementRef Accessible::getFirstChild(AXUIElementRef parent) {
  auto children = getChildren(parent, 1);
  if (children == nil)
    return nullptr;

  return (__bridge_retained AXUIElementRef)(children[0]);
}

//----------------------------------------------------------------------------------------------------------------------

/**
 * Returns the parent of the given element. The caller must unref the result (if not null);
 */
AXUIElementRef Accessible::getParent(AXUIElementRef child) {
  CFTypeRef parent;
  AXError error = AXUIElementCopyAttributeValue(child, kAXParentAttribute, &parent);
  if (error != kAXErrorSuccess || parent == nullptr) {
    return nullptr;
  }

  return static_cast<AXUIElementRef>(parent);
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::press(AXUIElementRef element) {
  // Don't wait for the press action to complete (could open a modal window).
  AXUIElementSetMessagingTimeout(element, 0.1);
  AXError error = AXUIElementPerformAction(element, kAXPressAction);
  AXUIElementSetMessagingTimeout(element, 0); // Restore default.
  handleUnsupportedError(error, "press");
}

//----------------------------------------------------------------------------------------------------------------------

void Accessible::printInfo(AXUIElementRef element) {
  std::vector<std::string> parents;
  parents.push_back(valueDescription((AXValueRef)element));

  AXUIElementRef run = getParent(element);
  while (run != nullptr) {
    parents.insert(parents.begin(), valueDescription((AXValueRef)run));
    AXUIElementRef parent = getParent(run);
    CFRelease(run);
    run = parent;
  }

  for (auto &entry : parents) {
    std::cout << entry << std::endl;
  }

  CFArrayRef array;
  AXUIElementCopyAttributeNames(element, &array);
  if (array != nil) {
    std::cout << std::endl << "Attributes:" << std::endl;

    NSArray *names = (__bridge NSArray *)array;
    for (NSString *name in names) {
      Boolean settable;
      AXUIElementIsAttributeSettable(element, (CFStringRef)name, &settable);
      std::cout << "\t" << name.UTF8String << (settable ? " (R/W)" : " (R)") << ": ";

      CFTypeRef value;
      AXError error = AXUIElementCopyAttributeValue(element, (CFStringRef)name, &value);
      if (error == kAXErrorSuccess) {
        std::cout << valueDescription((AXValueRef)value);
      }
      std::cout << std::endl;
    }

    CFRelease(array);
  }

  AXUIElementCopyParameterizedAttributeNames(element, &array);
  if (array != nil) {
    NSArray *names = (__bridge NSArray *)array;
    if (names.count > 0) {
      std::cout << std::endl << "Parameterized Attributes:" << std::endl;
      for (NSString *name in names) {
        std::cout << "\t" << name.UTF8String;

        CFTypeRef value;
        AXError error = AXUIElementCopyAttributeValue(element, (CFStringRef)name, &value);
        if (error == kAXErrorSuccess) {
          std::cout  << ": "<< valueDescription((AXValueRef)value);
        }
        std::cout << std::endl;
      }
    }
    CFRelease(array);
  }

  AXUIElementCopyActionNames(element, &array);
  if (array != nil) {
    NSArray *names = (__bridge NSArray *)array;
    if (names.count > 0) {
      std::cout << std::endl << "Actions:" << std::endl;

      for (NSString *name in names) {
        CFStringRef value;
        AXUIElementCopyActionDescription(element, (CFStringRef)name, &value);
        std::cout << "\t" << name.UTF8String << " - " << toString(value) <<  std::endl;
      }
    }
    CFRelease(array);
  }

  std::cout << std::endl;
}

//----------------------------------------------------------------------------------------------------------------------
