/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "Platform.h"

#include "DocAccessibleWrap.h"
#include "SessionAccessibility.h"
#include "TextLeafRange.h"
#include "mozilla/Components.h"
#include "mozilla/a11y/RemoteAccessible.h"
#include "nsIAccessibleEvent.h"
#include "nsIAccessiblePivot.h"
#include "nsIStringBundle.h"

#define ROLE_STRINGS_URL "chrome://global/locale/AccessFu.properties"

using namespace mozilla;
using namespace mozilla::a11y;

constinit static nsTHashMap<nsStringHashKey, nsString> sLocalizedStrings;

void a11y::PlatformInit() {
  nsresult rv = NS_OK;
  nsCOMPtr<nsIStringBundleService> stringBundleService =
      components::StringBundle::Service();
  if (!stringBundleService) return;

  nsCOMPtr<nsIStringBundle> stringBundle;
  nsCOMPtr<nsIStringBundleService> sbs = components::StringBundle::Service();
  if (NS_FAILED(rv)) {
    NS_WARNING("Failed to get string bundle service");
    return;
  }

  rv = sbs->CreateBundle(ROLE_STRINGS_URL, getter_AddRefs(stringBundle));
  if (NS_FAILED(rv)) {
    NS_WARNING("Failed to get string bundle");
    return;
  }

  nsString localizedStr;
  // Preload the state required localized string.
  rv = stringBundle->GetStringFromName("stateRequired", localizedStr);
  if (NS_SUCCEEDED(rv)) {
    sLocalizedStrings.InsertOrUpdate(u"stateRequired"_ns, localizedStr);
  }

  // Preload heading level localized descriptions 1 thru 6.
  for (int32_t level = 1; level <= 6; level++) {
    nsAutoString token;
    token.AppendPrintf("heading-%d", level);

    nsAutoString formatString;
    formatString.AppendInt(level);
    AutoTArray<nsString, 1> formatParams;
    formatParams.AppendElement(formatString);
    rv = stringBundle->FormatStringFromName("headingLevel", formatParams,
                                            localizedStr);
    if (NS_SUCCEEDED(rv)) {
      sLocalizedStrings.InsertOrUpdate(token, localizedStr);
    }
  }

  // Preload any roles that have localized versions
#define ROLE(geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
             msaaRole, ia2Role, androidClass, iosIsElement, uiaControlType, \
             nameRule)                                                      \
  rv = stringBundle->GetStringFromName(stringRole, localizedStr);           \
  if (NS_SUCCEEDED(rv)) {                                                   \
    sLocalizedStrings.InsertOrUpdate(u##stringRole##_ns, localizedStr);     \
  }

#include "RoleMap.inc"
#undef ROLE
}

void a11y::PlatformShutdown() { sLocalizedStrings.Clear(); }

void a11y::ProxyCreated(RemoteAccessible* aProxy) {
  SessionAccessibility::RegisterAccessible(aProxy);
}

void a11y::ProxyDestroyed(RemoteAccessible* aProxy) {
  SessionAccessibility::UnregisterAccessible(aProxy);
}

void a11y::PlatformEvent(Accessible* aTarget, uint32_t aEventType) {
  RefPtr<SessionAccessibility> sessionAcc =
      SessionAccessibility::GetInstanceFor(aTarget);
  if (!sessionAcc) {
    return;
  }

  switch (aEventType) {
    case nsIAccessibleEvent::EVENT_REORDER:
      sessionAcc->SendWindowContentChangedEvent();
      break;
    case nsIAccessibleEvent::EVENT_SCROLLING_START:
      if (Accessible* result = AccessibleWrap::DoPivot(
              aTarget, java::SessionAccessibility::HTML_GRANULARITY_DEFAULT,
              true, true)) {
        sessionAcc->SendAccessibilityFocusedEvent(result, false);
      }
      break;
    default:
      break;
  }
}

void a11y::PlatformStateChangeEvent(Accessible* aTarget, uint64_t aState,
                                    bool aEnabled) {
  RefPtr<SessionAccessibility> sessionAcc =
      SessionAccessibility::GetInstanceFor(aTarget);

  if (!sessionAcc) {
    return;
  }

  if (aState & states::CHECKED) {
    sessionAcc->SendClickedEvent(
        aTarget, java::SessionAccessibility::FLAG_CHECKABLE |
                     (aEnabled ? java::SessionAccessibility::FLAG_CHECKED : 0));
  }

  if (aState & states::EXPANDED) {
    sessionAcc->SendClickedEvent(
        aTarget,
        java::SessionAccessibility::FLAG_EXPANDABLE |
            (aEnabled ? java::SessionAccessibility::FLAG_EXPANDED : 0));
  }

  if (aState & states::SELECTED) {
    sessionAcc->SendSelectedEvent(aTarget, aEnabled);
  }

  if (aState & states::BUSY) {
    sessionAcc->SendWindowStateChangedEvent(aTarget);
  }
}

void a11y::PlatformFocusEvent(Accessible* aTarget) {
  if (RefPtr<SessionAccessibility> sessionAcc =
          SessionAccessibility::GetInstanceFor(aTarget)) {
    sessionAcc->SendFocusEvent(aTarget);
  }
}

void a11y::PlatformCaretMoveEvent(Accessible* aTarget, int32_t aOffset,
                                  bool aIsSelectionCollapsed,
                                  int32_t aGranularity, bool aFromUser) {
  RefPtr<SessionAccessibility> sessionAcc =
      SessionAccessibility::GetInstanceFor(aTarget);
  if (!sessionAcc) {
    return;
  }

  if (!aTarget->IsDoc() && !aFromUser && !aIsSelectionCollapsed) {
    // Pivot to the caret's position if it has an expanded selection.
    // This is used mostly for find in page.
    Accessible* leaf = TextLeafPoint::GetCaret(aTarget).mAcc;
    if (leaf) {
      if (Accessible* result = AccessibleWrap::DoPivot(
              leaf, java::SessionAccessibility::HTML_GRANULARITY_DEFAULT, true,
              true)) {
        sessionAcc->SendAccessibilityFocusedEvent(result, false);
      }
    }
  }

  sessionAcc->SendTextSelectionChangedEvent(aTarget, aOffset);
}

void a11y::PlatformTextChangeEvent(Accessible* aTarget, const nsAString& aStr,
                                   int32_t aStart, uint32_t aLen,
                                   bool aIsInsert, bool aFromUser) {
  RefPtr<SessionAccessibility> sessionAcc =
      SessionAccessibility::GetInstanceFor(aTarget);

  if (sessionAcc) {
    sessionAcc->SendTextChangedEvent(aTarget, aStr, aStart, aLen, aIsInsert,
                                     aFromUser);
  }
}

void a11y::PlatformShowHideEvent(Accessible* aTarget, Accessible* aParent,
                                 bool aInsert, bool aFromUser) {
  // We rely on the window content changed events to be dispatched
  // after the viewport cache is refreshed.
}

void a11y::PlatformSelectionEvent(Accessible*, Accessible*, uint32_t) {}

void a11y::PlatformScrollingEvent(Accessible* aTarget, uint32_t aEventType,
                                  uint32_t aScrollX, uint32_t aScrollY,
                                  uint32_t aMaxScrollX, uint32_t aMaxScrollY) {
  if (aEventType == nsIAccessibleEvent::EVENT_SCROLLING) {
    RefPtr<SessionAccessibility> sessionAcc =
        SessionAccessibility::GetInstanceFor(aTarget);

    if (sessionAcc) {
      sessionAcc->SendScrollingEvent(aTarget, aScrollX, aScrollY, aMaxScrollX,
                                     aMaxScrollY);
    }
  }
}

void a11y::PlatformAnnouncementEvent(Accessible* aTarget,
                                     const nsAString& aAnnouncement,
                                     uint16_t aPriority) {
  RefPtr<SessionAccessibility> sessionAcc =
      SessionAccessibility::GetInstanceFor(aTarget);

  if (sessionAcc) {
    sessionAcc->SendAnnouncementEvent(aTarget, aAnnouncement, aPriority);
  }
}

bool a11y::LocalizeString(const nsAString& aToken, nsAString& aLocalized) {
  MOZ_ASSERT(XRE_IsParentProcess());

  auto str = sLocalizedStrings.Lookup(aToken);
  if (str) {
    aLocalized.Assign(*str);
  } else {
  }

  return !!str;
}

uint64_t a11y::GetCacheDomainsForKnownClients(uint64_t aCacheDomains) {
  (void)aCacheDomains;
  // XXX: Respond to clients such as TalkBack. For now, be safe and default to
  // caching all domains.
  return CacheDomain::All;
}

void a11y::GetHumanReadableInstantiatorStr(nsAString& aResult) {
  aResult.Truncate();
}
