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

#include "base/logging.h"
#include "pagespeed/kernel/base/basictypes.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/http/bot_checker.h"
#include "pagespeed/kernel/http/content_type.h"
#include "pagespeed/kernel/http/http_names.h"
#include "pagespeed/kernel/http/request_headers.h"
#include "pagespeed/kernel/http/user_agent_matcher.h"

namespace net_instaweb {

DeviceProperties::DeviceProperties(UserAgentMatcher* matcher)
    : ua_matcher_(matcher),
      supports_critical_css_(kNotSet),
      supports_image_inlining_(kNotSet),
      supports_js_defer_(kNotSet),
      supports_lazyload_images_(kNotSet),
      requests_save_data_(kNotSet),
      accepts_webp_(kNotSet),
      supports_webp_rewritten_urls_(kNotSet),
      supports_webp_lossless_alpha_(kNotSet),
      supports_webp_animated_(kNotSet),
      is_bot_(kNotSet),
      is_mobile_user_agent_(kNotSet),
      supports_split_html_(kNotSet),
      supports_flush_early_(kNotSet),
      device_type_set_(kNotSet),
      device_type_(UserAgentMatcher::kDesktop),
      has_via_header_(kNotSet) {
}

DeviceProperties::~DeviceProperties() {
}

void DeviceProperties::SetUserAgent(const StringPiece& user_agent_string) {
  user_agent_string.CopyToString(&user_agent_);

  // Reset everything determined by user agent.
  supports_critical_css_ = kNotSet;
  supports_image_inlining_ = kNotSet;
  supports_js_defer_ = kNotSet;
  supports_lazyload_images_ = kNotSet;
  supports_webp_rewritten_urls_ = kNotSet;
  supports_webp_lossless_alpha_ = kNotSet;
  supports_webp_animated_ = kNotSet;
  is_bot_ = kNotSet;
  is_mobile_user_agent_ = kNotSet;
  supports_split_html_ = kNotSet;
  supports_flush_early_ = kNotSet;
}

void DeviceProperties::ParseRequestHeaders(
    const RequestHeaders& request_headers) {
  DCHECK_EQ(kNotSet, accepts_webp_) << "Double call to ParseRequestHeaders";
  accepts_webp_ =
      request_headers.HasValue(HttpAttributes::kAccept,
                               kContentTypeWebp.mime_type()) ?
      kTrue : kFalse;
  accepts_gzip_ =
      request_headers.HasValue(HttpAttributes::kAcceptEncoding,
                               HttpAttributes::kGzip) ?
      kTrue : kFalse;

  const char* save_data_header =
      request_headers.Lookup1(HttpAttributes::kSaveData);
  if (save_data_header != nullptr && StringCaseEqual("on", save_data_header)) {
    requests_save_data_ = kTrue;
  } else {
    requests_save_data_ = kFalse;
  }

  has_via_header_ =
      request_headers.Has(HttpAttributes::kVia) ?
      kTrue : kFalse;
}

bool DeviceProperties::AcceptsGzip() const {
  if (accepts_gzip_ == kNotSet) {
    LOG(DFATAL) << "Check of AcceptsGzip before value is set.";
    accepts_gzip_ = kFalse;
  }
  return (accepts_gzip_ == kTrue);
}

bool DeviceProperties::SupportsImageInlining() const {
  if (supports_image_inlining_ == kNotSet) {
    supports_image_inlining_ =
        ua_matcher_->SupportsImageInlining(user_agent_) ? kTrue : kFalse;
  }
  return (supports_image_inlining_ == kTrue);
}

bool DeviceProperties::SupportsLazyloadImages() const {
  if (supports_lazyload_images_ == kNotSet) {
    supports_lazyload_images_ =
        (!IsBot() && ua_matcher_->SupportsLazyloadImages(user_agent_)) ?
        kTrue : kFalse;
  }
  return (supports_lazyload_images_ == kTrue);
}

bool DeviceProperties::SupportsCriticalCss() const {
  // Currently CriticalSelectorFilter can't deal with IE conditional comments,
  // so we disable ourselves for IE.
  // TODO(morlovich): IE10 in strict mode disables the conditional comments
  // feature; but the strict mode is determined by combination of doctype and
  // X-UA-Compatible, which can come in both meta and header flavors. Once we
  // have a good way of detecting this case, we can enable us for strict IE10.
  if (supports_critical_css_ == kNotSet) {
    supports_critical_css_ = !ua_matcher_->IsIe(user_agent_) ? kTrue : kFalse;
  }
  return (supports_critical_css_ == kTrue);
}

bool DeviceProperties::SupportsCriticalImagesBeacon() const {
  // For now this script has the same user agent requirements as image inlining,
  // however that could change in the future if more advanced JS is used by the
  // beacon. Also disable for bots. See
  // https://github.com/apache/incubator-pagespeed-mod/issues/813.
  return SupportsImageInlining() && !IsBot();
}

// Note that the result of the function is cached as supports_js_defer_. This
// must be cleared before calling the function a second time with a different
// value for allow_mobile.
bool DeviceProperties::SupportsJsDefer(bool allow_mobile) const {
  if (supports_js_defer_ == kNotSet) {
    supports_js_defer_ =
        ua_matcher_->SupportsJsDefer(user_agent_, allow_mobile) ?
        kTrue : kFalse;
  }
  return (supports_js_defer_ == kTrue);
}

bool DeviceProperties::SupportsWebpInPlace() const {
  // We used to check accepts_webp_ == kNotSet here, but many tests don't bother
  // setting request headers.  So we simply use kNotSet to detect
  // double-initialization above.
  return (accepts_webp_ == kTrue);
}

// TODO(huibao): Only use "accept: image/webp" header to determine whether and
// which format of WebP is supported. Currently there are some browsers which
// have "accept: image/webp" but only support lossy/lossless format, and some
// browsers which don't have "accept" header but support lossy format. Once
// the market share of these browsers is small enough, we can simplify the logic
// by only checking the "accept" header.
bool DeviceProperties::SupportsWebpRewrittenUrls() const {
  if (supports_webp_rewritten_urls_ == kNotSet) {
    if ((accepts_webp_ == kTrue) || ua_matcher_->LegacyWebp(user_agent_)) {
      supports_webp_rewritten_urls_ = kTrue;
    } else {
      supports_webp_rewritten_urls_ = kFalse;
    }
  }
  return (supports_webp_rewritten_urls_ == kTrue);
}

bool DeviceProperties::SupportsWebpLosslessAlpha() const {
  if (supports_webp_lossless_alpha_ == kNotSet) {
    if ((accepts_webp_ == kTrue) &&
        ua_matcher_->SupportsWebpLosslessAlpha(user_agent_)) {
      supports_webp_lossless_alpha_ = kTrue;
    } else {
      supports_webp_lossless_alpha_ = kFalse;
    }
  }
  return (supports_webp_lossless_alpha_ == kTrue);
}

bool DeviceProperties::SupportsWebpAnimated() const {
  if (supports_webp_animated_ == kNotSet) {
    if ((accepts_webp_ == kTrue) &&
        ua_matcher_->SupportsWebpAnimated(user_agent_)) {
      supports_webp_animated_ = kTrue;
    } else {
      supports_webp_animated_ = kFalse;
    }
  }
  return (supports_webp_animated_ == kTrue);
}

bool DeviceProperties::IsBot() const {
  if (is_bot_ == kNotSet) {
    is_bot_ = BotChecker::Lookup(user_agent_) ? kTrue : kFalse;
  }
  return (is_bot_ == kTrue);
}

UserAgentMatcher::DeviceType DeviceProperties::GetDeviceType() const {
  if (device_type_set_ == kNotSet) {
    device_type_ = ua_matcher_->GetDeviceTypeForUA(user_agent_);
    device_type_set_ = kTrue;
  }
  return device_type_;
}

// Chrome 36 on iOS devices failed to display inlined WebP image, so inlining
// WebP on these devices is forbidden.
// https://bugs.chromium.org/p/chromium/issues/detail?id=402514
bool DeviceProperties::ForbidWebpInlining() const {
  if (ua_matcher_->IsiOSUserAgent(user_agent_)) {
    int major = kNotSet;
    int minor = kNotSet;
    int build = kNotSet;
    int patch = kNotSet;
    if (ua_matcher_->GetChromeBuildNumber(user_agent_, &major, &minor, &build,
                                          &patch) &&
        (major == 36 || major == 37)) {
      return true;
    }
  }
  return false;
}

bool DeviceProperties::RequestsSaveData() const {
  return (requests_save_data_ == kTrue);
}

bool DeviceProperties::HasViaHeader() const {
  return (has_via_header_ == kTrue);
}

}  // namespace net_instaweb
