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

#include <vector>

#include "pagespeed/kernel/base/scoped_ptr.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/html/html_element.h"
#include "pagespeed/kernel/html/html_name.h"
#include "util/utf8/public/unicodetext.h"
#include "webutil/css/media.h"
#include "webutil/css/parser.h"
#include "webutil/css/property.h"
#include "webutil/css/selector.h"
#include "webutil/css/value.h"

namespace net_instaweb {

namespace css_util {

// Extract the numerical value from a values vector.
// TODO(nforman): Allow specification what what style of numbers we can handle.
int GetValueDimension(const Css::Values* values) {
  for (Css::Values::const_iterator value_iter = values->begin();
       value_iter != values->end(); ++value_iter) {
    Css::Value* value = *value_iter;
    if ((value->GetLexicalUnitType() == Css::Value::NUMBER)
        && (value->GetDimension() == Css::Value::PX)) {
      return value->GetIntegerValue();
    }
  }
  return kNoValue;
}

DimensionState GetDimensions(Css::Declarations* decls,
                             int* width, int* height) {
  bool has_width = false;
  bool has_height = false;
  *width = kNoValue;
  *height = kNoValue;
  for (Css::Declarations::iterator decl_iter = decls->begin();
       decl_iter != decls->end() && (!has_width || !has_height); ++decl_iter) {
    Css::Declaration* decl = *decl_iter;
    switch (decl->prop()) {
      case Css::Property::WIDTH: {
        *width = GetValueDimension(decl->values());
        has_width = true;
        break;
      }
      case Css::Property::HEIGHT: {
        *height = GetValueDimension(decl->values());
        has_height = true;
        break;
      }
      default:
        break;
    }
  }
  if (has_width && has_height && *width != kNoValue && *height != kNoValue) {
    return kHasBothDimensions;
  } else if ((has_width && *width == kNoValue) ||
             (has_height && *height == kNoValue)) {
    return kNotParsable;
  } else if (has_width) {
    return kHasWidthOnly;
  } else if (has_height) {
    return kHasHeightOnly;
  }
  return kNoDimensions;
}

StyleExtractor::StyleExtractor(HtmlElement* element)
    : decls_(GetDeclsFromElement(element)),
      width_px_(kNoValue),
      height_px_(kNoValue) {
  if (decls_.get() != NULL) {
    state_ = GetDimensions(decls_.get(), &width_px_, &height_px_);
  } else {
    state_ = kNoDimensions;
  }
}

StyleExtractor::~StyleExtractor() {}

// Return a Declarations* from the style attribute of an element.  If
// there is no style, return NULL.
Css::Declarations* StyleExtractor::GetDeclsFromElement(HtmlElement* element) {
  HtmlElement::Attribute* style = element->FindAttribute(HtmlName::kStyle);
  if ((style != NULL) && (style->DecodedValueOrNull() != NULL)) {
    Css::Parser parser(style->DecodedValueOrNull());
    return parser.ParseDeclarations();
  }
  return NULL;
}

void VectorizeMediaAttribute(const StringPiece& input_media,
                             StringVector* output_vector) {
  // Split on commas, trim whitespace from each element found, delete empties.
  // Note that we hand trim because SplitStringPieceToVector() trims elements
  // of zero length but not those comprising one or more whitespace chars.
  StringPieceVector media_vector;
  SplitStringPieceToVector(input_media, ",", &media_vector, false);
  std::vector<StringPiece>::iterator it;
  for (it = media_vector.begin(); it != media_vector.end(); ++it) {
    TrimWhitespace(&(*it));
    if (StringCaseEqual(*it, kAllMedia)) {
      // Special case: an element of value 'all'.
      output_vector->clear();
      break;
    } else if (!it->empty()) {
      it->CopyToString(StringVectorAdd(output_vector));
    }
  }

  return;
}

GoogleString StringifyMediaVector(const StringVector& input_media) {
  GoogleString result;
  // Special case: inverse of the special rule in the vectorize function.
  if (input_media.empty()) {
    result = kAllMedia;
  } else {
    AppendJoinCollection(&result, input_media, ",");
  }
  return result;
}

bool IsComplexMediaQuery(const Css::MediaQuery& query) {
  return (query.qualifier() != Css::MediaQuery::NO_QUALIFIER ||
          !query.expressions().empty());
}

bool ConvertMediaQueriesToStringVector(const Css::MediaQueries& in_vector,
                                       StringVector* out_vector) {
  out_vector->clear();
  Css::MediaQueries::const_iterator iter;
  for (iter = in_vector.begin(); iter != in_vector.end(); ++iter) {
    // Reject complex media queries immediately.
    if (IsComplexMediaQuery(**iter)) {
      out_vector->clear();
      return false;
    } else {
      const UnicodeText& media_type = (*iter)->media_type();
      StringPiece element(media_type.utf8_data(), media_type.utf8_length());
      TrimWhitespace(&element);
      if (!element.empty()) {
        element.CopyToString(StringVectorAdd(out_vector));
      }
    }
  }
  return true;
}

void ConvertStringVectorToMediaQueries(const StringVector& in_vector,
                                       Css::MediaQueries* out_vector) {
  out_vector->Clear();
  std::vector<GoogleString>::const_iterator iter;
  for (iter = in_vector.begin(); iter != in_vector.end(); ++iter) {
    StringPiece element(*iter);
    TrimWhitespace(&element);
    if (!element.empty()) {
      Css::MediaQuery* query = new Css::MediaQuery;
      query->set_media_type(UTF8ToUnicodeText(element.data(), element.size()));
      out_vector->push_back(query);
    }
  }
}

void ClearVectorIfContainsMediaAll(StringVector* media) {
  StringVector::const_iterator iter;
  for (iter = media->begin(); iter != media->end(); ++iter) {
    if (StringCaseEqual(*iter, kAllMedia)) {
      media->clear();
      break;
    }
  }
}

namespace {

// Does the given data start with the given word followed by whitespace, '(', or
// end of string?  If so, strip the token and spaces and return true.  Otherwise
// return false and leave data alone.
bool StartsWithWord(const StringPiece& word, StringPiece* data) {
  // Make a local copy, so we only shorten on success.
  StringPiece local(*data);
  if (!local.starts_with(word)) {
    return false;
  }
  local.remove_prefix(word.size());
  if (TrimLeadingWhitespace(&local) ||
      local.empty() ||
      local[0] == '(') {
    *data = local;
    return true;
  }
  return false;
}

}  // namespace

bool CanMediaAffectScreen(const StringPiece& media) {
  // TODO(jmaessen): re-implement via CSS parser once it has an entry point for
  // media parsing.
  if (media.empty()) {
    // Media type "" appears to be either screen or all depending on spec
    // version, and affects the screen either way.
    return true;
  }
  StringPieceVector media_vector;
  SplitStringPieceToVector(media, ",", &media_vector, true);
  for (int i = 0, n = media_vector.size(); i < n; ++i) {
    StringPiece current(media_vector[i]);
    TrimLeadingWhitespace(&current);
    // Recognize a CSS3 media query.  We are generous in our recognition here:
    // we'll take anything that contains "screen" or "all" as a token.  Compare
    // with http://www.w3.org/TR/css3-mediaqueries/ which is relatively strict.
    // Note that we rely on the fact that the media itself must come first, so
    // we stop once we've seen that or a left paren.  Also, we don't require
    // whitespace before (.
    // First, we strip a leading "only" if it exists.  This is a no-op in CSS3
    // (but causes CSS2 to not use this rule).
    StartsWithWord("only", &current);
    bool initial_not = StartsWithWord("not", &current);
    if (StartsWithWord("screen", &current) ||
        StartsWithWord("all", &current) ||
        current.empty() ||
        current[0] == '(') {
      // Affects screen, unless there was an initial not.
      if (!initial_not) {
        return true;
      }
    } else if (initial_not) {
      // Something like "not print" that affects screen.
      return true;
    }
  }
  return false;
}

GoogleString JsDetectableSelector(const Css::Selector& selector) {
  // Create a temporary selector representing the desired result that shares
  // structure with the given selector.  We do this because a SimpleSelector
  // isn't copyable without about another page of code.  We're only creating
  // this AST fragment locally and throwing it away.
  Css::Selector trimmed;
  for (int i = 0, n = selector.size(); i < n; ++i) {
    Css::SimpleSelectors* simple_selectors = selector[i];
    scoped_ptr<Css::SimpleSelectors> trimmed_selectors(
        new Css::SimpleSelectors(simple_selectors->combinator()));
    for (int j = 0, m = simple_selectors->size(); j < m; ++j) {
      Css::SimpleSelector* simple_selector = (*simple_selectors)[j];
      // For now we simply discard all pseudoclass attributes.
      // TODO(jmaessen): Only discard pseudoclass attributes that
      // refer to UI elements or dynamic pseudo-classes; see
      // http://www.w3.org/TR/selectors/#pseudo-classes
      if (simple_selector->type() != Css::SimpleSelector::PSEUDOCLASS) {
        trimmed_selectors->push_back(simple_selector);
      }
    }
    if (trimmed_selectors->empty()) {
      // If there's no simple selector at this point, our combinators may have
      // gotten messed up.  Conservatively truncate the Css selector.  This
      // should be difficult in practice, as it requires rules like "p > :hover
      // > a" whose exact interpretation are ambiguous.  We'll truncate such a
      // rule to "p".  Note that rules like "p :hover a" should end up sensibly
      // as "p a".
      break;
    }
    trimmed.push_back(trimmed_selectors.release());
  }
  GoogleString result(trimmed.ToString());
  for (int i = 0, n = trimmed.size(); i < n; ++i) {
    // Remove the SimpleSelector objects without cleaning them up, since we
    // don't own them.
    trimmed[i]->clear();
  }
  return result;
}

}  // namespace css_util

}  // namespace net_instaweb
