// Copyright 2017 Google LLC
//
// 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.api.graphql.grpc;

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableMap;
import graphql.Scalars;
import graphql.schema.GraphQLEnumType;
import graphql.schema.GraphQLEnumValueDefinition;
import graphql.schema.GraphQLFieldDefinition;
import graphql.schema.GraphQLList;
import graphql.schema.GraphQLNamedType;
import graphql.schema.GraphQLObjectType;
import graphql.schema.GraphQLSchema;
import graphql.schema.GraphQLType;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.Set;

/** Creates a proto source file for a {@link GraphQLSchema}. */
public final class SchemaToProto {

  private static final String HEADER =
      "// LINT: LEGACY_NAMES\n"
          + "// Autogenerated. Do not edit.\n"
          + "syntax = \"proto3\";\n\n"
          + "package google.api.graphql.rejoiner.proto;\n\n"
          + "option java_package = \"com.google.api.graphql.rejoiner.proto\";\n"
          + "option (jspb.js_namespace) = \"com.google.api.graphql.rejoiner.proto\";\n"
          + "import \"java/com/google/apps/jspb/jspb.proto\";\n\n";

  private static final ImmutableMap<String, String> TYPE_MAP =
      new ImmutableMap.Builder<String, String>()
          .put(Scalars.GraphQLBoolean.getName(), "bool")
          .put(Scalars.GraphQLFloat.getName(), "float")
          .put(Scalars.GraphQLInt.getName(), "int32")
          .put(Scalars.GraphQLLong.getName(), "int64")
          .put(Scalars.GraphQLString.getName(), "string")
          .build();

  /** Returns a proto source file for the schema. */
  public static String toProto(GraphQLSchema schema) {
    ArrayList<String> messages = new ArrayList<>();

    for (GraphQLType type : getAllTypes(schema)) {
      if (type instanceof GraphQLEnumType) {
        messages.add(toEnum((GraphQLEnumType) type));
      } else if (type instanceof GraphQLObjectType) {
        messages.add(toMessage((GraphQLObjectType) type));
      }
    }

    return HEADER + Joiner.on("\n\n").join(messages);
  }

  @SuppressWarnings("JdkObsolete")
  static Set<GraphQLType> getAllTypes(GraphQLSchema schema) {
    LinkedHashSet<GraphQLType> types = new LinkedHashSet<>();
    LinkedList<GraphQLObjectType> loop = new LinkedList<>();
    loop.add(schema.getQueryType());
    types.add(schema.getQueryType());

    while (!loop.isEmpty()) {
      for (GraphQLFieldDefinition field : loop.pop().getFieldDefinitions()) {
        GraphQLType type = field.getType();
        if (type instanceof GraphQLList) {
          type = ((GraphQLList) type).getWrappedType();
        }
        if (!types.contains(type)) {
          if (type instanceof GraphQLEnumType) {
            types.add(field.getType());
          }
          if (type instanceof GraphQLObjectType) {
            types.add(type);
            loop.add((GraphQLObjectType) type);
          }
        }
      }
    }
    return types;
  }

  private static String toEnum(GraphQLEnumType type) {
    ArrayList<String> values = new ArrayList<>();
    int i = 0;
    for (GraphQLEnumValueDefinition value : type.getValues()) {
      if (value.getName().equals("UNRECOGNIZED")) {
        continue;
      }
      values.add(String.format("%s = %d;", value.getName(), i));
      i++;
    }
    return String.format(
        "message %s {\n %s\n enum Enum {\n%s\n}\n}",
        type.getName(), getJspb(type), Joiner.on("\n").join(values));
  }

  private static String toMessage(GraphQLObjectType type) {
    ArrayList<String> fields = new ArrayList<>();
    fields.add(getJspb(type));
    int i = 1;
    for (GraphQLFieldDefinition field : type.getFieldDefinitions()) {
      if (field.getName().equals("_")) {
        continue;
      }
      fields.add(String.format("%s = %d;", toField(field), i));
      i++;
    }

    return String.format("message %s {\n%s\n}", type.getName(), Joiner.on("\n").join(fields));
  }

  private static String getJspb(GraphQLNamedType type) {
    return String.format("option (jspb.message_id) = \"graphql.%s\";", type.getName());
  }

  private static String toField(GraphQLFieldDefinition field) {
    return String.format("%s %s", toType(field.getType()), field.getName());
  }

  private static String toType(GraphQLType type) {
    if (type instanceof GraphQLList) {
      return "repeated " + toType(((GraphQLList) type).getWrappedType());
    } else if (type instanceof GraphQLObjectType) {
      return ((GraphQLObjectType) type).getName();
    } else if (type instanceof GraphQLEnumType) {
      return ((GraphQLEnumType) type).getName() + ".Enum";
    } else {
      return TYPE_MAP.get(((GraphQLNamedType) type).getName());
    }
  }

  private SchemaToProto() {}
}
