/*
 * 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/javascript_code_block.h"

#include <cstddef>

#include "net/instaweb/rewriter/public/javascript_library_identification.h"
#include "pagespeed/kernel/base/message_handler.h"
#include "pagespeed/kernel/base/source_map.h"
#include "pagespeed/kernel/base/statistics.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/js/js_minify.h"
#include "pagespeed/kernel/js/js_tokenizer.h"

namespace net_instaweb {

// Statistics names
const char JavascriptRewriteConfig::kBlocksMinified[] =
    "javascript_blocks_minified";
const char JavascriptRewriteConfig::kLibrariesIdentified[] =
    "javascript_libraries_identified";
const char JavascriptRewriteConfig::kMinificationFailures[] =
    "javascript_minification_failures";
const char JavascriptRewriteConfig::kTotalBytesSaved[] =
    "javascript_total_bytes_saved";
const char JavascriptRewriteConfig::kTotalOriginalBytes[] =
    "javascript_total_original_bytes";
const char JavascriptRewriteConfig::kMinifyUses[] = "javascript_minify_uses";
const char JavascriptRewriteConfig::kNumReducingMinifications[] =
    "javascript_reducing_minifications";


const char JavascriptRewriteConfig::kJSMinificationDisabled[] =
    "javascript_minification_disabled";
const char JavascriptRewriteConfig::kJSDidNotShrink[] =
    "javascript_did_not_shrink";
const char JavascriptRewriteConfig::kJSFailedToWrite[] =
    "javascript_failed_to_write";

const char JavascriptCodeBlock::kIntrospectionComment[] =
    "This script contains introspective JavaScript and is unsafe to replace.";

JavascriptRewriteConfig::JavascriptRewriteConfig(
    Statistics* stats, bool minify, bool use_experimental_minifier,
    const JavascriptLibraryIdentification* identification,
    const pagespeed::js::JsTokenizerPatterns* js_tokenizer_patterns)
    : minify_(minify),
      use_experimental_minifier_(use_experimental_minifier),
      library_identification_(identification),
      js_tokenizer_patterns_(js_tokenizer_patterns),
      blocks_minified_(stats->GetVariable(kBlocksMinified)),
      libraries_identified_(stats->GetVariable(kLibrariesIdentified)),
      minification_failures_(stats->GetVariable(kMinificationFailures)),
      total_bytes_saved_(stats->GetVariable(kTotalBytesSaved)),
      total_original_bytes_(stats->GetVariable(kTotalOriginalBytes)),
      num_uses_(stats->GetVariable(kMinifyUses)),
      num_reducing_minifications_(
          stats->GetVariable(kNumReducingMinifications)),
      minification_disabled_(stats->GetVariable(kJSMinificationDisabled)),
      did_not_shrink_(stats->GetVariable(kJSDidNotShrink)),
      failed_to_write_(stats->GetVariable(kJSFailedToWrite)) {
}

void JavascriptRewriteConfig::InitStats(Statistics* statistics) {
  statistics->AddVariable(kBlocksMinified);
  statistics->AddVariable(kLibrariesIdentified);
  statistics->AddVariable(kMinificationFailures);
  statistics->AddVariable(kTotalBytesSaved);
  statistics->AddVariable(kTotalOriginalBytes);
  statistics->AddVariable(kMinifyUses);
  statistics->AddVariable(kNumReducingMinifications);

  statistics->AddVariable(kJSMinificationDisabled);
  statistics->AddVariable(kJSDidNotShrink);
  statistics->AddVariable(kJSFailedToWrite);
}

JavascriptCodeBlock::JavascriptCodeBlock(
    const StringPiece& original_code, JavascriptRewriteConfig* config,
    const StringPiece& message_id, MessageHandler* handler)
    : config_(config),
      message_id_(message_id.data(), message_id.size()),
      original_code_(original_code.data(), original_code.size()),
      rewritten_(false),
      successfully_rewritten_(false),
      handler_(handler) {
}

JavascriptCodeBlock::~JavascriptCodeBlock() { }

// Is this URL sanitary to be appended (in a line comment) to the JS doc?
bool JavascriptCodeBlock::IsSanitarySourceMapUrl(StringPiece url) {
  for (int i = 0, n = url.size(); i < n; ++i) {
    if (!IsNonControlAscii(url[i])) {
      // This is a bit broader than necessary. JS line comments can only be
      // terminated by Unicode line/paragraph separators (Zl/Zp). Instead of
      // searching for all of these, we simply check any non-standard chars.
      // Specifically, we reject any URL with control chars (0x00-0x1F,0x7F)
      // or any non-ASCII UTF-8 chars (Bytes 0x80-0xFF).
      // Because URLs passed in here are .pagespeed. rewritten URLs, we do
      // not expect any of them to be of this form anyway, so this case
      // shouldn't be hit.
      return false;
    }
  }
  return true;
}

void JavascriptCodeBlock::AppendSourceMapUrl(StringPiece url) {
  DCHECK(rewritten_);
  DCHECK(successfully_rewritten_);
  if (!IsSanitarySourceMapUrl(url)) {
    LOG(DFATAL) << "Unsanitary source map URL could not be added to JS " << url;
    return;
  }

  StrAppend(&rewritten_code_, "\n//# sourceMappingURL=", url, "\n");
}

StringPiece JavascriptCodeBlock::ComputeJavascriptLibrary() const {
  // TODO(jmaessen): when we compute minified version and find
  // a match, consider adding the un-minified hash to the library
  // identifier, and then using that to speed up identification
  // in future (at the cost of a double lookup for a miss).  Also
  // consider pruning candidate JS that is simply too small to match
  // a registered library.
  DCHECK(rewritten_);
  StringPiece result;
  if (rewritten_) {
    const JavascriptLibraryIdentification* library_identification =
        config_->library_identification();
    if (library_identification != NULL) {
      result = library_identification->Find(rewritten_code_);
      if (!result.empty()) {
        config_->libraries_identified()->Add(1);
      }
    }
  }
  return result;
}

bool JavascriptCodeBlock::UnsafeToRename(const StringPiece& script) {
  // If you're pulling out script elements it's probably because
  // you're trying to do a kind of reflection that would break if we
  // minified the code and mutated its url.
  return script.find("document.getElementsByTagName('script')")
           != StringPiece::npos ||
         script.find("document.getElementsByTagName(\"script\")")
           != StringPiece::npos ||
         script.find("$('script')")  // jquery version
           != StringPiece::npos ||
         script.find("$(\"script\")")
           != StringPiece::npos;
}

bool JavascriptCodeBlock::Rewrite() {
  DCHECK(!rewritten_);
  if (rewritten_) {
    return successfully_rewritten_;
  }

  rewritten_ = true;
  successfully_rewritten_ = false;
  // We minify for two reasons: because the user wants minified js code (in
  // which case output_code_ should point to the minified code when we're
  // done), or because we're trying to identify a javascript library.
  // Bail if we're not doing one of these things.
  if (!config_->minify() && (config_->library_identification() == NULL)) {
    return successfully_rewritten_;
  }

  if (MinifyJs(original_code_, &rewritten_code_, &source_mappings_)) {
    // Minification succeeded. The fact that it succeeded doesn't imply that
    // it actually saved anything; we increment num_reducing_uses when there
    // were actual savings.
    config_->blocks_minified()->Add(1);
    if (config_->minify() && rewritten_code_.size() < original_code_.size()) {
      // Minification will actually be used.
      successfully_rewritten_ = true;
      config_->num_reducing_uses()->Add(1);
      config_->total_original_bytes()->Add(original_code_.size());
      // Note: This unsigned arithmetic is guaranteed not to underflow because
      // of the if statement above.
      config_->total_bytes_saved()->Add(
          original_code_.size() - rewritten_code_.size());
    }
  } else {  // Minification failed.
    handler_->Message(kInfo, "%s: Javascript minification failed.  "
                      "Preserving old code.", message_id_.c_str());
    // Note: Although we set rewritten_code_, we do not consider this a
    // successful rewrite and thus will not minify. This is only used for
    // canonical library identification.
    TrimWhitespace(original_code_, &rewritten_code_);
    // Update stats.
    config_->minification_failures()->Add(1);
  }
  return successfully_rewritten_;
}

void JavascriptCodeBlock::SwapRewrittenString(GoogleString* other) {
  DCHECK(rewritten_);
  DCHECK(successfully_rewritten_);

  other->swap(rewritten_code_);

  rewritten_code_.clear();
  rewritten_ = false;
  successfully_rewritten_ = false;
}

bool JavascriptCodeBlock::MinifyJs(
    StringPiece input, GoogleString* output,
    source_map::MappingVector* source_mappings) {
  if (config_->use_experimental_minifier()) {
    return pagespeed::js::MinifyUtf8JsWithSourceMap(
        config_->js_tokenizer_patterns(), input, output, source_mappings);
  } else {
    return pagespeed::js::MinifyJs(input, output);
  }
}

}  // namespace net_instaweb
