// Copyright 2016 The Bazel Authors. All rights reserved.
//
// 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.

#ifndef THIRD_PARTY_BAZEL_SRC_TOOLS_SINGLEJAR_TOKEN_STREAM_H_
#define THIRD_PARTY_BAZEL_SRC_TOOLS_SINGLEJAR_TOKEN_STREAM_H_ 1

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <memory>
#include <string>
#include <utility>
#include <vector>

#include "src/main/cpp/util/path_platform.h"
#include "src/tools/singlejar/diag.h"

/*
 * Tokenize command line containing indirect command line arguments.
 * An '@' at the beginning of a command line argument indicates that
 * the rest of the argument is the name of the file which should be
 * read and tokenized as Bash does it: tokens are separated by the
 * whitespace, quotes and double quotes can be used to have whitespace
 * and the other quote inside the token, and backslash followed by
 * newline is treated as empty string.
 */

class ArgTokenStream {
  /* This class is used as follows:
   *
   *  int main(int argc, char* argv[]) {
   *    ArgTokenStream tokens(argc-1, argv+1);
   *    while (!tokens.AtEnd()) {
   *       if (tokens.MatchAndSet("--opt1", ...) ||
   *           tokens.MatchAndSet("--opt2", ...) ||
   *           ...) {
   *         continue;
   *       }
   *       // Process non-option argument or report an error.
   *       // ArgTokenStream::token() returns the current token.
   *    }
   *  }
   */

 private:
  // Internal class to handle indirect command files.
  class FileTokenStream {
   public:
    FileTokenStream(const char *filename) {
#ifdef _WIN32
      std::wstring wpath;
      std::string error;
      if (!blaze_util::AsAbsoluteWindowsPath(filename, &wpath, &error)) {
        diag_err(1, "%s:%d: AsAbsoluteWindowsPath failed: %s", __FILE__,
                 __LINE__, error.c_str());
      }
      fp_ = _wfopen(wpath.c_str(), L"r");
#else
      fp_ = fopen(filename, "r");
#endif

      if (!fp_) {
        diag_err(1, "%s", filename);
      }
      filename_ = filename;
      next_char();
    }

    ~FileTokenStream() { close(); }

    // Assign next token to TOKEN, return true on success, false on EOF.
    bool next_token(std::string *token) {
      if (!fp_) {
        return false;
      }
      *token = "";
      while (current_char_ != EOF && isspace(current_char_)) {
        next_char();
      }
      if (current_char_ == EOF) {
        close();
        return false;
      }
      for (;;) {
        if (current_char_ == '\'' || current_char_ == '"') {
          process_quoted(token);
          if (isspace(current_char_)) {
            next_char();
            return true;
          } else {
            next_char();
          }
        } else if (current_char_ == '\\') {
          next_char();
          if ((current_char_ != EOF)) {
            token->push_back(current_char_);
            next_char();
          } else {
            diag_errx(1, "Expected character after \\, got EOF in %s",
                      filename_.c_str());
          }
        } else if (current_char_ == EOF || isspace(current_char_)) {
          next_char();
          return true;
        } else {
          token->push_back(current_char_);
          next_char();
        }
      }
    }

   private:
    void close() {
      if (fp_) {
        fclose(fp_);
        fp_ = nullptr;
      }
      filename_.clear();
    }

    // Append the quoted string to the TOKEN. The quote character (which can be
    // single or double quote) is in the current character. Everything up to the
    // matching quote character is appended.
    void process_quoted(std::string *token) {
      char quote = current_char_;
      next_char();
      while (current_char_ != quote) {
        if (current_char_ == '\\' && quote == '"') {
          // In the "-quoted token, \" stands for ", and \x
          // is copied literally for any other x.
          next_char();
          if (current_char_ == '"') {
            token->push_back('"');
            next_char();
          } else if (current_char_ != EOF) {
            token->push_back('\\');
            token->push_back(current_char_);
            next_char();
          } else {
            diag_errx(1, "No closing %c in %s", quote, filename_.c_str());
          }
        } else if (current_char_ != EOF) {
          token->push_back(current_char_);
          next_char();
        } else {
          diag_errx(1, "No closing %c in %s", quote, filename_.c_str());
        }
      }
    }

    // Get the next character from the input stream. Skip backslash followed
    // by the newline.
    void next_char() {
      if (feof(fp_)) {
        current_char_ = EOF;
        return;
      }
      current_char_ = getc(fp_);
      // Eat "\\\n" sequence.
      while (current_char_ == '\\') {
        int c = getc(fp_);
        if (c == '\n') {
          current_char_ = getc(fp_);
        } else {
          if (c != EOF) {
            ungetc(c, fp_);
          }
          break;
        }
      }
    }

    FILE *fp_;
    std::string filename_;
    int current_char_;
  };

 public:
  // Constructor. Automatically reads the first token.
  ArgTokenStream(int argc, const char *const *argv)
      : argv_(argv), argv_end_(argv + argc) {
    next();
  }

  // Process --OPTION
  // If the current token is --OPTION, set given FLAG to true, proceed to next
  // token and return true
  bool MatchAndSet(const char *option, bool *flag) {
    if (token_.compare(option) != 0) {
      return false;
    }
    *flag = true;
    next();
    return true;
  }

  // Process --OPTION OPTARG
  // If the current token is --OPTION, set OPTARG to the next token, proceed to
  // the next token after it and return true.
  bool MatchAndSet(const char *option, std::string *optarg) {
    if (token_.compare(option) != 0) {
      return false;
    }
    next();
    if (AtEnd()) {
      diag_errx(1, "%s requires argument", option);
    }
    *optarg = token_;
    next();
    return true;
  }

  // Process --OPTION OPTARG1 OPTARG2 ...
  // If a current token is --OPTION, push_back all subsequent tokens up to the
  // next option to the OPTARGS array, proceed to the next option and return
  // true.
  bool MatchAndSet(const char *option, std::vector<std::string> *optargs) {
    if (token_.compare(option) != 0) {
      return false;
    }
    next();
    while (!AtEnd() && '-' != token_.at(0)) {
      optargs->push_back(token_);
      next();
    }
    return true;
  }

  // Process --OPTION OPTARG1,OPTSUFF1 OPTARG2,OPTSUFF2 ...
  // If a current token is --OPTION, push_back all subsequent tokens up to the
  // next option to the OPTARGS array, splitting the OPTARG,OPTSUFF by a comma,
  // proceed to the next option and return true.
  bool MatchAndSet(const char *option,
                   std::vector<std::pair<std::string, std::string> > *optargs) {
    if (token_.compare(option) != 0) {
      return false;
    }
    next();
    while (!AtEnd() && '-' != token_.at(0)) {
      size_t commapos = token_.find(',');
      if (commapos == std::string::npos) {
        optargs->push_back(std::pair<std::string, std::string>(token_, ""));
      } else {
        std::string first = token_.substr(0, commapos);
        token_.erase(0, commapos + 1);
        optargs->push_back(std::pair<std::string, std::string>(first, token_));
      }

      next();
    }
    return true;
  }

  // Current token.
  const std::string &token() const { return token_; }

  // Read the next token.
  void next() {
    if (AtEnd()) {
      return;
    }
    if (file_token_stream_.get() && token_from_file()) {
      return;
    }
    while (argv_ < argv_end_) {
      if (**argv_ != '@') {
        token_ = *argv_++;
        return;
      }
      file_token_stream_.reset(new FileTokenStream(*(argv_++) + 1));
      if (token_from_file()) {
        return;
      }
    }
    argv_++;
  }

  // True if there are no more tokens.
  bool AtEnd() const { return argv_ > argv_end_; }

 private:
  bool token_from_file() {
    if (file_token_stream_->next_token(&token_)) {
      return true;
    }
    file_token_stream_.reset(nullptr);
    return false;
  }
  std::unique_ptr<FileTokenStream> file_token_stream_;
  const char *const *argv_;
  const char *const *argv_end_;
  std::string token_;
};

#endif  //  THIRD_PARTY_BAZEL_SRC_TOOLS_SINGLEJAR_TOKEN_STREAM_H_
