// Copyright 2020 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.
package com.google.devtools.build.lib.worker;

import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.worker.WorkerProtocol.Input;
import com.google.devtools.build.lib.worker.WorkerProtocol.WorkRequest;
import com.google.devtools.build.lib.worker.WorkerProtocol.WorkResponse;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.MalformedJsonException;
import com.google.protobuf.ByteString;
import com.google.protobuf.util.JsonFormat;
import com.google.protobuf.util.JsonFormat.Printer;
import java.io.BufferedWriter;
import java.io.EOFException;
import java.io.IOException;
import java.util.List;

/** Implementation of the Worker Protocol using JSON to communicate with Bazel. */
public final class JsonWorkerMessageProcessor implements WorkRequestHandler.WorkerMessageProcessor {
  /** Reader for reading the WorkResponse. */
  private final JsonReader reader;
  /** Printer for printing the WorkRequest. */
  private final Printer jsonPrinter;
  /** Writer for writing the WorkRequest to the worker. */
  private final BufferedWriter jsonWriter;

  /** Constructs a {@code WorkRequestHandler} that reads and writes JSON. */
  public JsonWorkerMessageProcessor(JsonReader reader, BufferedWriter jsonWriter) {
    this.reader = reader;
    reader.setLenient(true);
    this.jsonWriter = jsonWriter;
    jsonPrinter =
        JsonFormat.printer().omittingInsignificantWhitespace().includingDefaultValueFields();
  }

  private static ImmutableList<String> readArguments(JsonReader reader) throws IOException {
    reader.beginArray();
    ImmutableList.Builder<String> argumentsBuilder = ImmutableList.builder();
    while (reader.hasNext()) {
      argumentsBuilder.add(reader.nextString());
    }
    reader.endArray();
    return argumentsBuilder.build();
  }

  private static ImmutableList<Input> readInputs(JsonReader reader) throws IOException {
    reader.beginArray();
    ImmutableList.Builder<Input> inputsBuilder = ImmutableList.builder();
    while (reader.hasNext()) {
      String digest = null;
      String path = null;

      reader.beginObject();
      while (reader.hasNext()) {
        String name = reader.nextName();
        switch (name) {
          case "digest":
            if (digest != null) {
              throw new IOException("Input cannot have more than one digest");
            }
            digest = reader.nextString();
            break;
          case "path":
            if (path != null) {
              throw new IOException("Input cannot have more than one path");
            }
            path = reader.nextString();
            break;
          default:
            // As per https://docs.bazel.build/versions/main/creating-workers.html#work-responses,
            // unknown fields are ignored.
            reader.skipValue();
            break;
        }
      }
      reader.endObject();
      Input.Builder inputBuilder = Input.newBuilder();
      if (digest != null) {
        inputBuilder.setDigest(ByteString.copyFromUtf8(digest));
      }
      if (path != null) {
        inputBuilder.setPath(path);
      }
      inputsBuilder.add(inputBuilder.build());
    }
    reader.endArray();
    return inputsBuilder.build();
  }

  @Override
  public WorkRequest readWorkRequest() throws IOException {
    List<String> arguments = null;
    List<Input> inputs = null;
    Integer requestId = null;
    try {
      reader.beginObject();
      while (reader.hasNext()) {
        String name = reader.nextName();
        switch (name) {
          case "arguments":
            if (arguments != null) {
              throw new IOException("WorkRequest cannot have more than one 'arguments' field");
            }
            arguments = readArguments(reader);
            break;
          case "inputs":
            if (inputs != null) {
              throw new IOException("WorkRequest cannot have more than one 'inputs' field");
            }
            inputs = readInputs(reader);
            break;
          case "requestId":
            if (requestId != null) {
              throw new IOException("WorkRequest cannot have more than one requestId");
            }
            requestId = reader.nextInt();
            break;
          default:
            // As per https://docs.bazel.build/versions/main/creating-workers.html#work-responses,
            // unknown fields are ignored.
            reader.skipValue();
            break;
        }
      }
      reader.endObject();
    } catch (MalformedJsonException | IllegalStateException | EOFException e) {
      throw new IOException(e);
    }

    WorkRequest.Builder requestBuilder = WorkRequest.newBuilder();
    if (arguments != null) {
      requestBuilder.addAllArguments(arguments);
    }
    if (inputs != null) {
      requestBuilder.addAllInputs(inputs);
    }
    if (requestId != null) {
      requestBuilder.setRequestId(requestId);
    }
    return requestBuilder.build();
  }

  @Override
  public void writeWorkResponse(WorkResponse response) throws IOException {
    jsonPrinter.appendTo(response, jsonWriter);
    jsonWriter.flush();
  }

  @Override
  public void close() throws IOException {
    jsonWriter.close();
  }
}
