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

#include <set>
#include <vector>

#include "net/instaweb/rewriter/public/critical_finder_support_util.h"
#include "net/instaweb/rewriter/public/critical_selector_finder.h"
#include "net/instaweb/rewriter/public/css_tag_scanner.h"
#include "net/instaweb/rewriter/public/css_util.h"
#include "net/instaweb/rewriter/public/request_properties.h"
#include "net/instaweb/rewriter/public/rewrite_driver.h"
#include "net/instaweb/rewriter/public/rewrite_driver_factory.h"
#include "net/instaweb/rewriter/public/rewrite_options.h"
#include "net/instaweb/rewriter/public/server_context.h"
#include "net/instaweb/rewriter/public/static_asset_manager.h"
#include "pagespeed/kernel/base/escaping.h"
#include "pagespeed/kernel/base/hasher.h"
#include "pagespeed/kernel/base/statistics.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/html/html_element.h"
#include "pagespeed/kernel/html/html_name.h"
#include "pagespeed/kernel/http/google_url.h"
#include "webutil/css/media.h"
#include "webutil/css/parser.h"
#include "webutil/css/selector.h"

using Css::Selectors;
using Css::Stylesheet;
using Css::Ruleset;
using Css::Rulesets;

namespace net_instaweb {

const char CriticalCssBeaconFilter::kInitializePageSpeedJs[] =
    "var pagespeed = pagespeed || {};";

// Counters.
const char CriticalCssBeaconFilter::kCriticalCssBeaconAddedCount[] =
    "critical_css_beacon_filter_script_added_count";
const char CriticalCssBeaconFilter::kCriticalCssNoBeaconDueToMissingData[] =
    "critical_css_no_beacon_due_to_missing_data";
const char CriticalCssBeaconFilter::kCriticalCssSkippedDueToCharset[] =
    "critical_css_skipped_due_to_charset";

CriticalCssBeaconFilter::CriticalCssBeaconFilter(RewriteDriver* driver)
    : CssSummarizerBase(driver) {
  Statistics* stats = driver->server_context()->statistics();
  critical_css_beacon_added_count_ = stats->GetVariable(
      kCriticalCssBeaconAddedCount);
  critical_css_no_beacon_due_to_missing_data_ = stats->GetVariable(
      kCriticalCssNoBeaconDueToMissingData);
  critical_css_skipped_due_to_charset_ = stats->GetVariable(
      kCriticalCssSkippedDueToCharset);
}

CriticalCssBeaconFilter::~CriticalCssBeaconFilter() {}

void CriticalCssBeaconFilter::InitStats(Statistics* statistics) {
  statistics->AddVariable(kCriticalCssBeaconAddedCount);
  statistics->AddVariable(kCriticalCssNoBeaconDueToMissingData);
  statistics->AddVariable(kCriticalCssSkippedDueToCharset);
}

bool CriticalCssBeaconFilter::MustSummarize(HtmlElement* element) const {
  // Don't summarize alternate stylesheets, they are clearly non-critical.
  if (element->keyword() == HtmlName::kLink &&
      CssTagScanner::IsAlternateStylesheet(
          element->AttributeValue(HtmlName::kRel))) {
    return false;
  }

  // Don't summarize non-screen-affecting or <noscript> CSS at all; the time we
  // spend doing that is better devoted to summarizing CSS selectors we will
  // actually consider critical.
  return (noscript_element() == NULL) &&
      css_util::CanMediaAffectScreen(element->AttributeValue(HtmlName::kMedia));
}

void CriticalCssBeaconFilter::Summarize(Stylesheet* stylesheet,
                                        GoogleString* out) const {
  StringSet selectors;
  FindSelectorsFromStylesheet(*stylesheet, &selectors);
  // Serialize set into out.
  AppendJoinCollection(out, selectors, ",");
}

// Append the selector list initialization JavaScript to |script|.
// Right now the result looks like:
//   pagespeed.selectors=["selector 1","selector 2","selector 3"];
void CriticalCssBeaconFilter::AppendSelectorsInitJs(
    GoogleString* script, const StringSet& selectors) {
  StrAppend(script, "pagespeed.selectors=[");
  for (StringSet::const_iterator i = selectors.begin();
       i != selectors.end(); ++i) {
    if (i != selectors.begin()) {
      StrAppend(script, ",");
    }
    EscapeToJsStringLiteral(*i, true /* quote */, script);
  }
  StrAppend(script, "];");
}

// Append the beacon initialization JavaScript to |script|.
// Right now the result looks like:
//   pagespeed.criticalCssBeaconInit('beacon_url','page_url','options_hash',
//        pagespeed.selectors);
void CriticalCssBeaconFilter::AppendBeaconInitJs(
    const BeaconMetadata& metadata, GoogleString* script) {
  GoogleString beacon_url = driver()->IsHttps() ?
      driver()->options()->beacon_url().https :
      driver()->options()->beacon_url().http;
  GoogleString page_url;
  EscapeToJsStringLiteral(driver()->google_url().Spec(), false /* add_quotes */,
                          &page_url);
  Hasher* hasher = driver()->server_context()->hasher();
  GoogleString options_hash = hasher->Hash(driver()->options()->signature());
  StrAppend(script,
            "pagespeed.criticalCssBeaconInit('",
            beacon_url, "','", page_url, "','",
            options_hash, "','", metadata.nonce, "',pagespeed.selectors);");
}

void CriticalCssBeaconFilter::SummariesDone() {
  // We parse each summary back into component selectors from its
  // comma-separated string, using a StringSet to remove duplicates (they'll be
  // sorted, too, which makes this easier to test).  We re-serialize the set.
  StringSet selectors;
  for (int i = 0; i < NumStyles(); ++i) {
    const SummaryInfo& summary_info = GetSummaryForStyle(i);
    // The critical_selector_filter doesn't include <noscript>-specific CSS
    // in the critical CSS it computes; so there is no need to figure out
    // critical selectors for such CSS.
    if (summary_info.is_inside_noscript) {
      continue;
    }
    switch (summary_info.state) {
      case kSummaryStillPending:
        // Don't beacon if we're still waiting for critical selector data.
        return;
      case kSummaryOk: {
        // Include the selectors in the beacon
        StringPieceVector temp;
        SplitStringPieceToVector(summary_info.data, ",", &temp,
                                 true /* omit_empty_strings */);
        for (StringPieceVector::const_iterator i = temp.begin(),
                 end = temp.end();
             i != end; ++i) {
          selectors.insert(i->as_string());
        }
        break;
      }
      case kSummarySlotRemoved:
        // Another filter (likely combine CSS) has eliminated this CSS.
        continue;
      case kSummaryCssParseError:
      case kSummaryResourceCreationFailed:
      case kSummaryInputUnavailable:
        // The CSS couldn't be fetched or parsed in some fashion.  This will
        // be left in place by the rewriter, so we don't need to consider it for
        // beaconing either.  NOTE: this requires the rewriter to inject
        // critical CSS in situ so that we don't disrupt the cascade order
        // around the unparseable data.
        // TODO(jmaessen): Consider handling unparseable data within the CSS
        // parse tree, which would let us extract critical CSS selectors from
        // CSS with a mix of parseable and unparseable rules.
        continue;
    }
  }
  BeaconMetadata metadata =
      driver()->server_context()->critical_selector_finder()->
          PrepareForBeaconInsertion(selectors, driver());
  if (metadata.status == kDoNotBeacon) {
    // No beaconing required according to current pcache state and computed
    // selector set.
    return;
  }

  // Insert the beaconing code and selectors.
  GoogleString script;
  StaticAssetManager* asset_manager =
      driver()->server_context()->static_asset_manager();
  if (driver()->server_context()->factory()->UseBeaconResultsInFilters()) {
    script = asset_manager->GetAsset(
        StaticAssetEnum::CRITICAL_CSS_BEACON_JS, driver()->options());
    AppendSelectorsInitJs(&script, selectors);
    AppendBeaconInitJs(metadata, &script);
  } else {
    script = kInitializePageSpeedJs;
    AppendSelectorsInitJs(&script, selectors);
  }
  HtmlElement* script_element = driver()->NewElement(NULL, HtmlName::kScript);
  driver()->AddAttribute(script_element, HtmlName::kDataPagespeedNoDefer,
                         StringPiece());
  InsertNodeAtBodyEnd(script_element);
  AddJsToElement(script, script_element);

  if (critical_css_beacon_added_count_ != NULL) {
    critical_css_beacon_added_count_->Add(1);
  }
}

void CriticalCssBeaconFilter::DetermineEnabled(GoogleString* disabled_reason) {
  set_is_enabled(driver()->request_properties()->SupportsCriticalCssBeacon());
}

void CriticalCssBeaconFilter::FindSelectorsFromRuleset(
    const Ruleset& ruleset, StringSet* selectors) {
  const Selectors& rule_selectors = ruleset.selectors();
  for (int i = 0, n = rule_selectors.size(); i < n; ++i) {
    GoogleString trimmed(css_util::JsDetectableSelector(*rule_selectors[i]));
    if (!trimmed.empty()) {
      // Non-empty trimmed selector.  An empty trimmed selector (eg :hover,
      // which gets stripped away as it's not JS detectable) is *automatically*
      // critical, and we could also ignore the selector * (:hover is implicitly
      // *:hover).
      selectors->insert(trimmed);
    }
  }
}

// Returns false on parse failure, else records css selectors (in normalized
// string form) in selectors.  The selectors will be sorted and unique.  Logging
// of failures etc. should be done in the caller.
void CriticalCssBeaconFilter::FindSelectorsFromStylesheet(
    const Stylesheet& css, StringSet* selectors) {
  const Rulesets& rulesets = css.rulesets();
  for (int i = 0, n = rulesets.size(); i < n; ++i) {
    Ruleset* ruleset = rulesets[i];
    if (ruleset->type() == Ruleset::UNPARSED_REGION) {
      // Couldn't parse this as a rule.
      continue;
    }
    // Skip rules that can't apply to the screen.
    if (!css_util::CanMediaAffectScreen(ruleset->media_queries().ToString())) {
      continue;
    }
    // Record the selectors associated with this ruleset.
    FindSelectorsFromRuleset(*ruleset, selectors);
  }
}

}  // namespace net_instaweb
