// Copyright 2023 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.packages;

import com.google.common.collect.Ordering;
import com.google.devtools.build.docgen.annot.DocCategory;
import java.util.List;
import java.util.Map;
import net.starlark.java.annot.Param;
import net.starlark.java.annot.ParamType;
import net.starlark.java.annot.StarlarkBuiltin;
import net.starlark.java.annot.StarlarkMethod;
import net.starlark.java.eval.EvalException;
import net.starlark.java.eval.Mutability;
import net.starlark.java.eval.Starlark;
import net.starlark.java.eval.StarlarkFloat;
import net.starlark.java.eval.StarlarkInt;
import net.starlark.java.eval.StarlarkIterable;
import net.starlark.java.eval.StarlarkSemantics;
import net.starlark.java.eval.StarlarkThread;
import net.starlark.java.eval.StarlarkValue;
import net.starlark.java.eval.Structure;
import net.starlark.java.lib.StarlarkEncodable;

/** Proto defines the "proto" Starlark module of utilities for protocol message processing. */
@StarlarkBuiltin(
    name = "proto",
    category = DocCategory.TOP_LEVEL_MODULE,
    doc = "A module for protocol message processing.")
public class Proto implements StarlarkValue {

  // Note: in due course this is likely to move to net.starlark.java.lib.proto.
  // Do not add functions that would not belong there!
  // Functions related to running the protocol compiler belong in proto_common.

  private Proto() {}

  public static final Proto INSTANCE = new Proto();

  @StarlarkMethod(
      name = "encode_text",
      doc =
          "Returns the struct argument's encoding as a text-format protocol message.\n"
              + "The data structure must be recursively composed of strings, ints, floats, or"
              + " bools, or structs, sequences, and dicts of these types.\n"
              + "<p>A struct is converted to a message. Fields are emitted in name order.\n"
              + "Each struct field whose value is None is ignored.\n"
              + "<p>A sequence (such as a list or tuple) is converted to a repeated field.\n"
              + "Its elements must not be sequences or dicts.\n"
              + "<p>A dict is converted to a repeated field of messages with fields named 'key' and"
              + " 'value'.\n"
              + "Entries are emitted in iteration (insertion) order.\n"
              + "The dict's keys must be strings or ints, and its values must not be sequences or"
              + " dicts.\n"
              + "Examples:<br><pre class=language-python>proto.encode_text(struct(field=123))\n"
              + "# field: 123\n\n"
              + "proto.encode_text(struct(field=True))\n"
              + "# field: true\n\n"
              + "proto.encode_text(struct(field=[1, 2, 3]))\n"
              + "# field: 1\n"
              + "# field: 2\n"
              + "# field: 3\n\n"
              + "proto.encode_text(struct(field='text', ignored_field=None))\n"
              + "# field: \"text\"\n\n"
              + "proto.encode_text(struct(field=struct(inner_field='text', ignored_field=None)))\n"
              + "# field {\n"
              + "#   inner_field: \"text\"\n"
              + "# }\n\n"
              + "proto.encode_text(struct(field=[struct(inner_field=1), struct(inner_field=2)]))\n"
              + "# field {\n"
              + "#   inner_field: 1\n"
              + "# }\n"
              + "# field {\n"
              + "#   inner_field: 2\n"
              + "# }\n\n"
              + "proto.encode_text(struct(field=struct(inner_field=struct(inner_inner_field='text'))))\n"
              + "# field {\n"
              + "#    inner_field {\n"
              + "#     inner_inner_field: \"text\"\n"
              + "#   }\n"
              + "# }\n\n"
              + "proto.encode_text(struct(foo={4: 3, 2: 1}))\n"
              + "# foo: {\n"
              + "#   key: 4\n"
              + "#   value: 3\n"
              + "# }\n"
              + "# foo: {\n"
              + "#   key: 2\n"
              + "#   value: 1\n"
              + "# }\n"
              + "</pre>",
      parameters = {
        @Param(
            name = "x",
            allowedTypes = {
              @ParamType(type = Structure.class),
              @ParamType(type = StarlarkEncodable.class)
            })
      },
      useStarlarkThread = true)
  public String encodeText(Object x, StarlarkThread thread)
      throws EvalException, InterruptedException {
    TextEncoder enc = new TextEncoder(thread.getSemantics());
    enc.message(enc.toStructure(x));
    return enc.out.toString();
  }

  private static final class TextEncoder {

    private final StringBuilder out = new StringBuilder();
    private int indent = 0;
    private final StarlarkSemantics semantics;

    private TextEncoder(StarlarkSemantics semantics) {
      this.semantics = semantics;
    }

    private Structure toStructure(Object x) throws EvalException {
      Object originalX = x;
      if (x instanceof StarlarkEncodable encodable) {
        x = encodable.objectForEncoding(semantics);
      }
      if (!(x instanceof Structure struct)) {
        throw Starlark.errorf("invalid message: got %s, want struct", Starlark.type(originalX));
      }
      return struct;
    }

    // Encodes Structure x as a protocol message.
    private void message(Structure x) throws EvalException, InterruptedException {
      // For determinism, sort fields.
      List<String> fields =
          Ordering.natural()
              .sortedCopy(Starlark.dir(Mutability.IMMUTABLE, StarlarkSemantics.DEFAULT, x));
      for (String field : fields) {
        try {
          Object value =
              Starlark.getattr(Mutability.IMMUTABLE, StarlarkSemantics.DEFAULT, x, field, null);
          field(field, value);
        } catch (EvalException ex) {
          throw Starlark.errorf("in %s field .%s: %s", Starlark.type(x), field, ex.getMessage());
        }
      }
    }

    // Encodes Structure field (name, v) as a message field
    // (a repeated field, if v is a dict or sequence.)
    private void field(String name, Object v) throws EvalException, InterruptedException {
      if (v instanceof StarlarkEncodable encodable) {
        v = encodable.objectForEncoding(semantics);
      }
      // dict or other mapping?
      if (v instanceof Map<?, ?> map) {
        for (Map.Entry<?, ?> entry : map.entrySet()) {
          Object key = entry.getKey();
          if (!(key instanceof String || key instanceof StarlarkInt)) {
            throw Starlark.errorf(
                "invalid dict key: got %s, want int or string", Starlark.type(key));
          }
          emitLine(name, " {");
          indent++;
          fieldElement("key", key); // can't fail
          try {
            fieldElement("value", entry.getValue());
          } catch (EvalException ex) {
            throw Starlark.errorf(
                "in value for dict key %s: %s",
                Starlark.repr(key, StarlarkSemantics.DEFAULT), ex.getMessage());
          }
          indent--;
          emitLine("}");
        }
        return;
      }

      // list or tuple?
      if (v instanceof StarlarkIterable<?> sequence) {
        int i = 0;
        for (Object item : sequence) {
          try {
            fieldElement(name, item);
          } catch (EvalException ex) {
            throw Starlark.errorf("at %s index %d: %s", Starlark.type(v), i, ex.getMessage());
          }
          i++;
        }
        return;
      }

      // non-repeated field
      if (v == Starlark.NONE) {
        return;
      }
      fieldElement(name, v);
    }

    // Emits field (name, v) as a message field, or one element of a repeated field.
    // v must be an int, float, string, bool, Structure, or a StarlarkEncodable encoding to one of
    // these.
    private void fieldElement(String name, Object v) throws EvalException, InterruptedException {
      if (v instanceof StarlarkEncodable encodable) {
        v = encodable.objectForEncoding(semantics);
      }

      if (v instanceof Structure struct) {
        emitLine(name, " {");
        indent++;
        message(struct);
        indent--;
        emitLine("}");

      } else if (v instanceof String s) {
        emitLine(
            name, ": \"", s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n"), "\"");

      } else if (v instanceof StarlarkInt || v instanceof Boolean) {
        emitLine(name, ": ", v.toString());
      } else if (v instanceof StarlarkFloat) {
        String s = v.toString();
        // Encoding to textproto via proto.encode_text requires "inf" for "+inf".
        if (s.equals("+inf")) {
          s = "inf";
        }
        emitLine(name, ": ", s);
      } else {
        throw Starlark.errorf("got %s, want string, int, float, bool, or struct", Starlark.type(v));
      }
    }

    // Emits items on an indented line.
    private void emitLine(String... items) {
      for (int i = 0; i < indent; i++) {
        out.append("  ");
      }
      for (String item : items) {
        out.append(item);
      }
      out.append('\n');
    }
  }
}
