/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */



#include "net/instaweb/rewriter/public/js_replacer.h"

#include "base/logging.h"
#include "pagespeed/kernel/js/js_keywords.h"

using pagespeed::JsKeywords;
using pagespeed::js::JsTokenizer;

namespace net_instaweb {

namespace {

enum State {
  kStart,
  kSawIdent,
  kSawIdentDot,
  kSawIdentDotIdent,
  kSawIdentDotIdentEquals
};

void ResetState(State* state, GoogleString* maybe_object,
                GoogleString* maybe_field) {
  *state = kStart;
  maybe_object->clear();
  maybe_field->clear();
}

}  // namespace.

JsReplacer::~JsReplacer() {}

void JsReplacer::AddPattern(const GoogleString& object,
                            const GoogleString& field,
                            StringRewriter* rewriter) {
  patterns_.push_back(Pattern(object, field, rewriter));
}

bool JsReplacer::Transform(StringPiece in, GoogleString* out) {
  State state = kStart;

  GoogleString maybe_object;
  GoogleString maybe_field;

  pagespeed::js::JsTokenizer tokenizer(js_tokenizer_patterns_, in);
  out->clear();
  while (true) {
    // Note that this may get modified in the switch below.
    StringPiece token;
    GoogleString replacement;  // in case we need to replace token.
    JsKeywords::Type type = tokenizer.NextToken(&token);
    // TODO(morlovich): This only matches object.field, not object['field'].
    switch (type) {
      case JsKeywords::kEndOfInput:
        return true;
      case JsKeywords::kError:
        return false;
      case JsKeywords::kComment:
      case JsKeywords::kWhitespace:
      case JsKeywords::kLineSeparator:
      case JsKeywords::kSemiInsert:
        // Whitespace is just passed through, and doesn't cause state machine
        // transitions.
        break;
      case JsKeywords::kIdentifier:
        switch (state) {
          case kStart:
          case kSawIdent:
          case kSawIdentDotIdent:
          case kSawIdentDotIdentEquals:
            state = kSawIdent;
            token.CopyToString(&maybe_object);
            break;
          case kSawIdentDot:
            state = kSawIdentDotIdent;
            token.CopyToString(&maybe_field);
            break;
        }
        break;
      case JsKeywords::kOperator:
        if (token == ".") {
          switch (state) {
            case kStart:
            case kSawIdentDot:
            case kSawIdentDotIdentEquals:
              // No clue on how some of these could parse.
              ResetState(&state, &maybe_object, &maybe_field);
              break;
            case kSawIdent:
              state = kSawIdentDot;
              break;
            case kSawIdentDotIdent:
              // This is something like a.b. -> so what we thought was a field
              // is now "object".
              state = kSawIdentDot;
              maybe_object = maybe_field;
              maybe_field.clear();
              break;
          }
        } else if (token == "=") {
          switch (state) {
            case kStart:
            case kSawIdent:
            case kSawIdentDot:
            case kSawIdentDotIdentEquals:
              // No clue on how some of these could parse.
              ResetState(&state, &maybe_object, &maybe_field);
              break;

            case kSawIdentDotIdent:
              state = kSawIdentDotIdentEquals;
              break;
          }
        } else {
          // Things other than . and = are uninteresting to us.
          ResetState(&state, &maybe_object, &maybe_field);
        }
        break;
      case JsKeywords::kStringLiteral:
        if (state == kSawIdentDotIdentEquals) {
          if (HandleCandidate(maybe_object, maybe_field, token, &replacement)) {
            token = replacement;
          }
        } else {
          ResetState(&state, &maybe_object, &maybe_field);
        }
        break;
      default:
        // Something unexpected --- reset matching.
        ResetState(&state, &maybe_object, &maybe_field);
    }

    StrAppend(out, token);
  }
}

bool JsReplacer::HandleCandidate(
    const GoogleString& object, const GoogleString& field,
    StringPiece value, GoogleString* out) {
  // Note that the token still has the quotes; we strip them before invoking
  // the callback and then restore them when serializing.
  CHECK_GE(value.length(), 2) << value;
  char quote = value[0];
  CHECK(quote == '\'' || quote == '"');
  CHECK_EQ(quote, value[value.length() - 1]);
  value.remove_prefix(1);
  value.remove_suffix(1);

  // Check patterns.
  for (int i = 0, n = patterns_.size(); i < n; ++i) {
    const Pattern& pat = patterns_[i];
    if (pat.object == object && pat.field == field) {
      GoogleString quote_str(1, quote);
      GoogleString rewriter_inout;
      value.CopyToString(&rewriter_inout);
      pat.rewriter->Run(&rewriter_inout);
      out->clear();
      StrAppend(out, quote_str, rewriter_inout, quote_str);
      return true;
    }
  }

  return false;
}

}  // namespace net_instaweb
