/**
 * Copyright 2009 Google Inc.
 *
 * Licensed 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 "pagespeed/css/cssmin.h"

#include "base/basictypes.h"
#include "base/logging.h"
#include "base/string_piece.h"
#include "pagespeed/core/string_util.h"

namespace {

const int kEOF = -1;  // represents the end of the input

// A token can either be a character (0-255) or one of these constants:
const int kStartToken = 256;  // the start of the input
const int kCommentToken = 257;  // a comment (that we chose to preserve)
const int kStringToken = 259;  // a string literal

// Lovingly copied from the js minifier rule implementation.
// TODO Extract these classes to a separate file to avoid the love from
//      spreading further.
class StringConsumer {
 public:
  explicit StringConsumer(std::string* output) : output_(output) {}

  void push_back(char c) {
    output_->push_back(c);
  }

  void append(const base::StringPiece& str) {
    output_->append(str.data(), str.size());
  }

 private:
  std::string* output_;

  DISALLOW_COPY_AND_ASSIGN(StringConsumer);
};

class SizeConsumer {
 public:
  explicit SizeConsumer(std::string* ignored) : size_(0) {}

  int size() const { return size_; }

  void push_back(char c) {
    ++size_;
  }

  void append(const base::StringPiece& str) {
    size_ += str.size();
  }

 private:
  int size_;

  DISALLOW_COPY_AND_ASSIGN(SizeConsumer);
};

// Return true for any character that never needs to be separated from other
// characters via whitespace.
bool Unextendable(int c) {
  switch (c) {
    case kStartToken:
    case kCommentToken:
    case '{':
    case '}':
    case '/':
    case ';':
    case ':':
      return true;
    default:
      return false;
  }
}

// Return true for any character that must separated from other "extendable"
// characters by whitespace on the _right_ in order keep tokens separate.
bool IsExtendableOnRight(int c) {
  switch (c) {
    // N.B. Left paren/bracket, but not right -- see
    //      http://code.google.com/p/page-speed/issues/detail?id=339 and
    //      http://code.google.com/p/page-speed/issues/detail?id=265
    case '(':
    case '[':
      return false;
    default:
      return !Unextendable(c);
  }
}

// Return true for any character that must separated from other "extendable"
// characters by whitespace on the _left_ in order keep tokens separate.
bool IsExtendableOnLeft(int c) {
  switch (c) {
    case ')':
    case ']':
      return false;
    default:
      return !Unextendable(c);
  }
}

template<typename OutputConsumer>
class Minifier {
 public:
  Minifier(const base::StringPiece& input, std::string* output);
  ~Minifier() {}

  // Return a pointer to an OutputConsumer instance if minification was
  // successful, NULL otherwise.
  OutputConsumer* GetOutput();

 private:
  int Peek();
  void ChangeToken(int next_token);
  void ConsumeComment();
  void ConsumeString();
  void Minify();

  // Represents what kind of whitespace we've seen since the last token:
  //   NO_WHITESPACE means that there is no whitespace between the tokens.
  //   SPACE means there's been at least one space/tab, but no linebreaks.
  //   LINEBREAK means there's been at least one linebreak.
  enum Whitespace { NO_WHITESPACE, SPACE, LINEBREAK };

  const base::StringPiece input_;
  size_t index_;
  OutputConsumer output_;
  Whitespace whitespace_;  // whitespace since the previous token
  int prev_token_;
  bool error_;
};

template<typename OutputConsumer>
Minifier<OutputConsumer>::Minifier(const base::StringPiece& input,
                                   std::string* output)
  : input_(input),
    index_(0),
    output_(output),
    whitespace_(NO_WHITESPACE),
    prev_token_(kStartToken),
    error_(false) {}

template<typename OutputConsumer>
OutputConsumer* Minifier<OutputConsumer>::GetOutput() {
  Minify();
  if (!error_) {
    return &output_;
  }
  return NULL;
}

// Return the next character after index_, or kEOF if there aren't any more.
template<typename OutputConsumer>
int Minifier<OutputConsumer>::Peek() {
  return (index_ + 1 < input_.size() ?
          static_cast<int>(input_[index_ + 1]) : kEOF);
}

// Switch to a new prev_token, and insert whitespace if necessary.  Call this
// right before appending a token onto the output.
template<typename OutputConsumer>
void Minifier<OutputConsumer>::ChangeToken(int next_token) {
  if (whitespace_ != NO_WHITESPACE) {
    if (prev_token_ == '}') {
      output_.push_back('\n');
    } else if (IsExtendableOnRight(prev_token_) &&
               IsExtendableOnLeft(next_token)) {
      output_.push_back(whitespace_ == LINEBREAK ? '\n' : ' ');
    }
    whitespace_ = NO_WHITESPACE;
  }
  prev_token_ = next_token;
}

template<typename OutputConsumer>
void Minifier<OutputConsumer>::ConsumeComment() {
  DCHECK(index_ + 1 < input_.size());
  DCHECK(input_[index_] == '/');
  DCHECK(input_[index_ + 1] == '*');
  const int begin = index_;
  index_ += 2;
  while (index_ < input_.size()) {
    if (input_[index_] == '*' && Peek() == '/') {
      index_ += 2;
      const base::StringPiece comment = input_.substr(begin, index_ - begin);
      // We want to remove comments, but we need to preserve comments intended
      // as IE hacks to avoid breaking sites that rely on them.
      // See http://code.google.com/p/page-speed/issues/detail?id=432
      if (comment == "/**/") {
        ChangeToken(kCommentToken);
        output_.append(comment);
      } else if (whitespace_ == NO_WHITESPACE) {
        whitespace_ = SPACE;
      }
      return;
    }
    ++index_;
  }
  // If we reached EOF without the comment being closed, that's okay; just
  // don't include the partial comment in the output.
}

template<typename OutputConsumer>
void Minifier<OutputConsumer>::ConsumeString() {
  DCHECK(index_ < input_.size());
  const int begin = index_;
  const char quote = input_[begin];
  DCHECK(quote == '"' || quote == '\'');
  ++index_;
  while (index_ < input_.size()) {
    const char ch = input_[index_];
    ++index_;
    if (ch == '\\') {
      ++index_;
    } else if (ch == quote) {
      break;
    }
  }
  ChangeToken(kStringToken);
  output_.append(input_.substr(begin, index_ - begin));
}

template<typename OutputConsumer>
void Minifier<OutputConsumer>::Minify() {
  while (index_ < input_.size() && !error_) {
    const char ch = input_[index_];
    // Track whitespace since the previous token.  NO_WHITESPACE means no
    // whitespace; LINEBREAK means there's been at least one linebreak; SPACE
    // means there's been spaces/tabs, but no linebreaks.
    if (ch == '\n' || ch == '\r') {
      whitespace_ = LINEBREAK;
      ++index_;
    }
    else if (ch == ' ' || ch == '\t') {
      if (whitespace_ == NO_WHITESPACE) {
        whitespace_ = SPACE;
      }
      ++index_;
    }
    // Strings:
    else if (ch == '\'' || ch == '"') {
      ConsumeString();
    }
    // Comments:
    else if (ch == '/' && Peek() == '*') {
      ConsumeComment();
    }
    // All other characters:
    else {
      ChangeToken(ch);
      output_.push_back(ch);
      ++index_;
    }
  }
}

}  // namespace

namespace pagespeed {

namespace css {

bool MinifyCss(const std::string& input, std::string* out) {
  Minifier<StringConsumer> minifier(input, out);
  return (minifier.GetOutput() != NULL);
}

bool GetMinifiedCssSize(const std::string& input, int* minified_size) {
  Minifier<SizeConsumer> minifier(input, NULL);
  SizeConsumer* output = minifier.GetOutput();
  if (output) {
    *minified_size = output->size();
    return true;
  } else {
    return false;
  }
}

}  // namespace css

}  // namespace pagespeed
