/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 "mozilla/KeyframeEffectParams.h"

#include "mozilla/AnimationUtils.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/KeyframeUtils.h"
#include "mozilla/RangedPtr.h"
#include "nsReadableUtils.h"

namespace mozilla {

static inline bool
IsLetter(char16_t aCh)
{
  return (0x41 <= aCh && aCh <= 0x5A) || (0x61 <= aCh && aCh <= 0x7A);
}

static inline bool
IsDigit(char16_t aCh)
{
  return 0x30 <= aCh && aCh <= 0x39;
}

static inline bool
IsNameStartCode(char16_t aCh)
{
  return IsLetter(aCh) || aCh >= 0x80 || aCh == '_';
}

static inline bool
IsNameCode(char16_t aCh)
{
  return IsNameStartCode(aCh) || IsDigit(aCh) || aCh == '-';
}

static inline bool
IsNewLine(char16_t aCh)
{
  // 0x0A (LF), 0x0C (FF), 0x0D (CR), or pairs of CR followed by LF are
  // replaced by LF.
  return aCh == 0x0A || aCh == 0x0C || aCh == 0x0D;
}

static inline bool
IsValidEscape(char16_t aFirst, char16_t aSecond)
{
  return aFirst == '\\' && !IsNewLine(aSecond);
}

static bool
IsIdentStart(RangedPtr<const char16_t> aIter,
             const char16_t* const aEnd)
{
  if (aIter == aEnd) {
    return false;
  }

  if (*aIter == '-') {
    if (aIter + 1 == aEnd) {
      return false;
    }
    char16_t second = *(aIter + 1);
    return IsNameStartCode(second) ||
           second == '-' ||
           (aIter + 2 != aEnd && IsValidEscape(second, *(aIter + 2)));
  }
  return IsNameStartCode(*aIter) ||
         (aIter + 1 != aEnd && IsValidEscape(*aIter, *(aIter + 1)));
}

static void
ConsumeIdentToken(RangedPtr<const char16_t>& aIter,
                  const char16_t* const aEnd,
                  nsAString& aResult)
{
  aResult.Truncate();

  // Check if it starts with an identifier.
  if (!IsIdentStart(aIter, aEnd)) {
    return;
  }

  // Start to consume.
  while (aIter != aEnd) {
    if (IsNameCode(*aIter)) {
      aResult.Append(*aIter);
    } else if (*aIter == '\\') {
      const RangedPtr<const char16_t> secondChar = aIter + 1;
      if (secondChar == aEnd || !IsValidEscape(*aIter, *secondChar)) {
        break;
      }
      // Consume '\\' and append the character following this '\\'.
      ++aIter;
      aResult.Append(*aIter);
    } else {
      break;
    }
    ++aIter;
  }
}

/* static */ void
KeyframeEffectParams::ParseSpacing(const nsAString& aSpacing,
                                   SpacingMode& aSpacingMode,
                                   nsCSSPropertyID& aPacedProperty,
                                   nsAString& aInvalidPacedProperty,
                                   ErrorResult& aRv)
{
  aInvalidPacedProperty.Truncate();

  // Ignore spacing if the core API is not enabled since it is not yet ready to
  // ship.
  if (!AnimationUtils::IsCoreAPIEnabledForCaller()) {
    aSpacingMode = SpacingMode::distribute;
    return;
  }

  // Parse spacing.
  // distribute | paced({ident})
  // https://w3c.github.io/web-animations/#dom-keyframeeffectreadonly-spacing
  // 1. distribute spacing.
  if (aSpacing.EqualsLiteral("distribute")) {
    aSpacingMode = SpacingMode::distribute;
    return;
  }

  // 2. paced spacing.
  static const nsLiteralString kPacedPrefix = NS_LITERAL_STRING("paced(");
  if (!StringBeginsWith(aSpacing, kPacedPrefix)) {
    aRv.ThrowTypeError<dom::MSG_INVALID_SPACING_MODE_ERROR>(aSpacing);
    return;
  }

  RangedPtr<const char16_t> iter(aSpacing.Data() + kPacedPrefix.Length(),
                                 aSpacing.Data(), aSpacing.Length());
  const char16_t* const end = aSpacing.EndReading();

  nsAutoString identToken;
  ConsumeIdentToken(iter, end, identToken);
  if (identToken.IsEmpty()) {
    aRv.ThrowTypeError<dom::MSG_INVALID_SPACING_MODE_ERROR>(aSpacing);
    return;
  }

  aPacedProperty =
    nsCSSProps::LookupProperty(identToken, CSSEnabledState::eForAllContent);
  if (aPacedProperty == eCSSProperty_UNKNOWN ||
      aPacedProperty == eCSSPropertyExtra_variable ||
      !KeyframeUtils::IsAnimatableProperty(aPacedProperty)) {
    aPacedProperty = eCSSProperty_UNKNOWN;
    aInvalidPacedProperty = identToken;
  }

  if (end - iter.get() != 1 || *iter != ')') {
    aRv.ThrowTypeError<dom::MSG_INVALID_SPACING_MODE_ERROR>(aSpacing);
    return;
  }

  aSpacingMode = aPacedProperty == eCSSProperty_UNKNOWN
                 ? SpacingMode::distribute
                 : SpacingMode::paced;
}

} // namespace mozilla
