// Copyright 2014 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <brillo/http/http_utils.h>

#include <algorithm>

#include <base/bind.h>
#include <base/json/json_reader.h>
#include <base/json/json_writer.h>
#include <base/values.h>
#include <brillo/data_encoding.h>
#include <brillo/errors/error_codes.h>
#include <brillo/mime_utils.h>
#include <brillo/streams/memory_stream.h>

using brillo::mime::AppendParameter;
using brillo::mime::RemoveParameters;

namespace brillo {
namespace http {

std::unique_ptr<Response> GetAndBlock(const std::string& url,
                                      const HeaderList& headers,
                                      std::shared_ptr<Transport> transport,
                                      brillo::ErrorPtr* error) {
  return SendRequestWithNoDataAndBlock(
      request_type::kGet, url, headers, transport, error);
}

RequestID Get(const std::string& url,
              const HeaderList& headers,
              std::shared_ptr<Transport> transport,
              const SuccessCallback& success_callback,
              const ErrorCallback& error_callback) {
  return SendRequestWithNoData(request_type::kGet,
                               url,
                               headers,
                               transport,
                               success_callback,
                               error_callback);
}

std::unique_ptr<Response> HeadAndBlock(const std::string& url,
                                       std::shared_ptr<Transport> transport,
                                       brillo::ErrorPtr* error) {
  return SendRequestWithNoDataAndBlock(
      request_type::kHead, url, {}, transport, error);
}

RequestID Head(const std::string& url,
               std::shared_ptr<Transport> transport,
               const SuccessCallback& success_callback,
               const ErrorCallback& error_callback) {
  return SendRequestWithNoData(request_type::kHead,
                               url,
                               {},
                               transport,
                               success_callback,
                               error_callback);
}

std::unique_ptr<Response> PostTextAndBlock(const std::string& url,
                                           const std::string& data,
                                           const std::string& mime_type,
                                           const HeaderList& headers,
                                           std::shared_ptr<Transport> transport,
                                           brillo::ErrorPtr* error) {
  return PostBinaryAndBlock(
      url, data.data(), data.size(), mime_type, headers, transport, error);
}

RequestID PostText(const std::string& url,
                   const std::string& data,
                   const std::string& mime_type,
                   const HeaderList& headers,
                   std::shared_ptr<Transport> transport,
                   const SuccessCallback& success_callback,
                   const ErrorCallback& error_callback) {
  return PostBinary(url,
                    data.data(),
                    data.size(),
                    mime_type,
                    headers,
                    transport,
                    success_callback,
                    error_callback);
}

std::unique_ptr<Response> SendRequestAndBlock(
    const std::string& method,
    const std::string& url,
    const void* data,
    size_t data_size,
    const std::string& mime_type,
    const HeaderList& headers,
    std::shared_ptr<Transport> transport,
    brillo::ErrorPtr* error) {
  Request request(url, method, transport);
  request.AddHeaders(headers);
  if (data_size > 0) {
    CHECK(!mime_type.empty()) << "MIME type must be specified if request body "
                                 "message is provided";
    request.SetContentType(mime_type);
    if (!request.AddRequestBody(data, data_size, error))
      return std::unique_ptr<Response>();
  }
  return request.GetResponseAndBlock(error);
}

std::unique_ptr<Response> SendRequestWithNoDataAndBlock(
    const std::string& method,
    const std::string& url,
    const HeaderList& headers,
    std::shared_ptr<Transport> transport,
    brillo::ErrorPtr* error) {
  return SendRequestAndBlock(
      method, url, nullptr, 0, {}, headers, transport, error);
}

RequestID SendRequest(const std::string& method,
                      const std::string& url,
                      StreamPtr stream,
                      const std::string& mime_type,
                      const HeaderList& headers,
                      std::shared_ptr<Transport> transport,
                      const SuccessCallback& success_callback,
                      const ErrorCallback& error_callback) {
  Request request(url, method, transport);
  request.AddHeaders(headers);
  if (stream && (!stream->CanGetSize() || stream->GetRemainingSize() > 0)) {
    CHECK(!mime_type.empty()) << "MIME type must be specified if request body "
                                 "message is provided";
    request.SetContentType(mime_type);
    brillo::ErrorPtr error;
    if (!request.AddRequestBody(std::move(stream), &error)) {
      transport->RunCallbackAsync(
          FROM_HERE, base::Bind(error_callback,
                                0, base::Owned(error.release())));
      return 0;
    }
  }
  return request.GetResponse(success_callback, error_callback);
}

RequestID SendRequest(const std::string& method,
                      const std::string& url,
                      const void* data,
                      size_t data_size,
                      const std::string& mime_type,
                      const HeaderList& headers,
                      std::shared_ptr<Transport> transport,
                      const SuccessCallback& success_callback,
                      const ErrorCallback& error_callback) {
  return SendRequest(method,
                     url,
                     MemoryStream::OpenCopyOf(data, data_size, nullptr),
                     mime_type,
                     headers,
                     transport,
                     success_callback,
                     error_callback);
}

RequestID SendRequestWithNoData(const std::string& method,
                                const std::string& url,
                                const HeaderList& headers,
                                std::shared_ptr<Transport> transport,
                                const SuccessCallback& success_callback,
                                const ErrorCallback& error_callback) {
  return SendRequest(method,
                     url,
                     {},
                     {},
                     headers,
                     transport,
                     success_callback,
                     error_callback);
}

std::unique_ptr<Response> PostBinaryAndBlock(
    const std::string& url,
    const void* data,
    size_t data_size,
    const std::string& mime_type,
    const HeaderList& headers,
    std::shared_ptr<Transport> transport,
    brillo::ErrorPtr* error) {
  return SendRequestAndBlock(request_type::kPost,
                             url,
                             data,
                             data_size,
                             mime_type,
                             headers,
                             transport,
                             error);
}

RequestID PostBinary(const std::string& url,
                     StreamPtr stream,
                     const std::string& mime_type,
                     const HeaderList& headers,
                     std::shared_ptr<Transport> transport,
                     const SuccessCallback& success_callback,
                     const ErrorCallback& error_callback) {
  return SendRequest(request_type::kPost,
                     url,
                     std::move(stream),
                     mime_type,
                     headers,
                     transport,
                     success_callback,
                     error_callback);
}

RequestID PostBinary(const std::string& url,
                     const void* data,
                     size_t data_size,
                     const std::string& mime_type,
                     const HeaderList& headers,
                     std::shared_ptr<Transport> transport,
                     const SuccessCallback& success_callback,
                     const ErrorCallback& error_callback) {
  return SendRequest(request_type::kPost,
                     url,
                     data,
                     data_size,
                     mime_type,
                     headers,
                     transport,
                     success_callback,
                     error_callback);
}

std::unique_ptr<Response> PostFormDataAndBlock(
    const std::string& url,
    const FormFieldList& data,
    const HeaderList& headers,
    std::shared_ptr<Transport> transport,
    brillo::ErrorPtr* error) {
  std::string encoded_data = brillo::data_encoding::WebParamsEncode(data);
  return PostBinaryAndBlock(url,
                            encoded_data.c_str(),
                            encoded_data.size(),
                            brillo::mime::application::kWwwFormUrlEncoded,
                            headers,
                            transport,
                            error);
}

std::unique_ptr<Response> PostFormDataAndBlock(
    const std::string& url,
    std::unique_ptr<FormData> form_data,
    const HeaderList& headers,
    std::shared_ptr<Transport> transport,
    brillo::ErrorPtr* error) {
  Request request(url, request_type::kPost, transport);
  request.AddHeaders(headers);
  if (!request.AddRequestBodyAsFormData(std::move(form_data), error))
    return std::unique_ptr<Response>();
  return request.GetResponseAndBlock(error);
}

RequestID PostFormData(const std::string& url,
                       const FormFieldList& data,
                       const HeaderList& headers,
                       std::shared_ptr<Transport> transport,
                       const SuccessCallback& success_callback,
                       const ErrorCallback& error_callback) {
  std::string encoded_data = brillo::data_encoding::WebParamsEncode(data);
  return PostBinary(url,
                    encoded_data.c_str(),
                    encoded_data.size(),
                    brillo::mime::application::kWwwFormUrlEncoded,
                    headers,
                    transport,
                    success_callback,
                    error_callback);
}

RequestID PostFormData(const std::string& url,
                       std::unique_ptr<FormData> form_data,
                       const HeaderList& headers,
                       std::shared_ptr<Transport> transport,
                       const SuccessCallback& success_callback,
                       const ErrorCallback& error_callback) {
  Request request(url, request_type::kPost, transport);
  request.AddHeaders(headers);
  brillo::ErrorPtr error;
  if (!request.AddRequestBodyAsFormData(std::move(form_data), &error)) {
    transport->RunCallbackAsync(
        FROM_HERE, base::Bind(error_callback, 0, base::Owned(error.release())));
    return 0;
  }
  return request.GetResponse(success_callback, error_callback);
}

std::unique_ptr<Response> PostJsonAndBlock(const std::string& url,
                                           const base::Value* json,
                                           const HeaderList& headers,
                                           std::shared_ptr<Transport> transport,
                                           brillo::ErrorPtr* error) {
  std::string data;
  if (json)
    base::JSONWriter::Write(*json, &data);
  std::string mime_type = AppendParameter(brillo::mime::application::kJson,
                                          brillo::mime::parameters::kCharset,
                                          "utf-8");
  return PostBinaryAndBlock(
      url, data.c_str(), data.size(), mime_type, headers, transport, error);
}

RequestID PostJson(const std::string& url,
                   std::unique_ptr<base::Value> json,
                   const HeaderList& headers,
                   std::shared_ptr<Transport> transport,
                   const SuccessCallback& success_callback,
                   const ErrorCallback& error_callback) {
  std::string data;
  if (json)
    base::JSONWriter::Write(*json, &data);
  std::string mime_type = AppendParameter(brillo::mime::application::kJson,
                                          brillo::mime::parameters::kCharset,
                                          "utf-8");
  return PostBinary(url,
                    data.c_str(),
                    data.size(),
                    mime_type,
                    headers,
                    transport,
                    success_callback,
                    error_callback);
}

std::unique_ptr<Response> PatchJsonAndBlock(
    const std::string& url,
    const base::Value* json,
    const HeaderList& headers,
    std::shared_ptr<Transport> transport,
    brillo::ErrorPtr* error) {
  std::string data;
  if (json)
    base::JSONWriter::Write(*json, &data);
  std::string mime_type = AppendParameter(brillo::mime::application::kJson,
                                          brillo::mime::parameters::kCharset,
                                          "utf-8");
  return SendRequestAndBlock(request_type::kPatch,
                             url,
                             data.c_str(),
                             data.size(),
                             mime_type,
                             headers,
                             transport,
                             error);
}

RequestID PatchJson(const std::string& url,
                    std::unique_ptr<base::Value> json,
                    const HeaderList& headers,
                    std::shared_ptr<Transport> transport,
                    const SuccessCallback& success_callback,
                    const ErrorCallback& error_callback) {
  std::string data;
  if (json)
    base::JSONWriter::Write(*json, &data);
  std::string mime_type =
      AppendParameter(brillo::mime::application::kJson,
                      brillo::mime::parameters::kCharset, "utf-8");
  return SendRequest(request_type::kPatch, url, data.c_str(), data.size(),
                     mime_type, headers, transport, success_callback,
                     error_callback);
}

std::unique_ptr<base::DictionaryValue> ParseJsonResponse(
    Response* response,
    int* status_code,
    brillo::ErrorPtr* error) {
  std::unique_ptr<base::DictionaryValue> result;
  if (!response)
    return result;

  if (status_code)
    *status_code = response->GetStatusCode();

  // Make sure we have a correct content type. Do not try to parse
  // binary files, or HTML output. Limit to application/json and text/plain.
  auto content_type = RemoveParameters(response->GetContentType());
  if (content_type != brillo::mime::application::kJson &&
      content_type != brillo::mime::text::kPlain) {
    brillo::Error::AddTo(error, FROM_HERE, brillo::errors::json::kDomain,
                         "non_json_content_type",
                         "Unexpected response content type: " + content_type);
    return result;
  }

  std::string json = response->ExtractDataAsString();
  std::string error_message;
  auto value = base::JSONReader::ReadAndReturnError(json, base::JSON_PARSE_RFC,
                                                    nullptr, &error_message);
  if (!value) {
    brillo::Error::AddToPrintf(error, FROM_HERE, brillo::errors::json::kDomain,
                               brillo::errors::json::kParseError,
                               "Error '%s' occurred parsing JSON string '%s'",
                               error_message.c_str(), json.c_str());
    return result;
  }
  result = base::DictionaryValue::From(std::move(value));
  if (!result) {
    brillo::Error::AddToPrintf(error, FROM_HERE, brillo::errors::json::kDomain,
                               brillo::errors::json::kObjectExpected,
                               "Response is not a valid JSON object: '%s'",
                               json.c_str());
  }
  return result;
}

std::string GetCanonicalHeaderName(const std::string& name) {
  std::string canonical_name = name;
  bool word_begin = true;
  for (char& c : canonical_name) {
    if (c == '-') {
      word_begin = true;
    } else {
      if (word_begin) {
        c = toupper(c);
      } else {
        c = tolower(c);
      }
      word_begin = false;
    }
  }
  return canonical_name;
}

}  // namespace http
}  // namespace brillo