content: /*
   * Licensed to the Apache Software Foundation (ASF) under one
   * or more contributor license agreements.  See the NOTICE file
   * distributed with this work for additional information
   * regarding copyright ownership.  The ASF licenses this file
   * to you 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 accord.maelstrom;
  
  import java.io.IOException;
  import java.util.*;
  
  import accord.api.RoutingKey;
  import accord.local.Node;
  import accord.api.Result;
  import accord.messages.ReadData.ReadOk;
  import accord.primitives.*;
  import com.google.gson.Gson;
  import com.google.gson.GsonBuilder;
  import com.google.gson.TypeAdapter;
  import com.google.gson.stream.JsonReader;
  import com.google.gson.stream.JsonToken;
  import com.google.gson.stream.JsonWriter;
  import accord.local.Node.Id;
  import accord.api.Key;
  
  import static accord.utils.Invariants.illegalState;
  
  public class Json
  {
      public static final Gson GSON;
      public static final TypeAdapter<Object> DEFAULT_ADAPTER = new TypeAdapter<Object>()
      {
          @Override
          public void write(JsonWriter out, Object value)
          {
              GSON.toJson(value, Object.class, out);
          }
  
          @Override
          public Object read(JsonReader in)
          {
              return GSON.fromJson(in, Object.class);
          }
      };
  
      public static final TypeAdapter<Id> ID_ADAPTER = new TypeAdapter<Id>()
      {
          @Override
          public void write(JsonWriter out, Id value) throws IOException
          {
              if (value.id == 0) out.nullValue();
              else out.value(Json.toString(value));
          }
  
          @Override
          public Id read(JsonReader in) throws IOException
          {
              if (in.peek() == JsonToken.NULL)
              {
                  in.nextNull();
                  return Id.NONE;
              }
  
              return parseId(in.nextString());
          }
      };
  
      public static Id parseId(String id)
      {
          switch (id.charAt(0))
          {
              //TODO(Review) - toString idn't remove the - so doing - parseInt makes the value positive, which changes
              // the value
              case 'c': return new Id( Integer.parseInt(id.substring(1)));
              case 'n': return new Id( Integer.parseInt(id.substring(1)));
              default: throw new IllegalStateException();
          }
      }
  
      public static String toString(Id id)
      {
          if (id.id < 0) return "c" + id.id;
          else return "n" + id.id;
      }
  
      public static final TypeAdapter<Timestamp> TIMESTAMP_ADAPTER = new TypeAdapter<Timestamp>()
      {
          @Override
          public void write(JsonWriter out, Timestamp value) throws IOException
          {
              if (value == null) out.nullValue();
              else writeTimestamp(out, value);
          }
  
          @Override
          public Timestamp read(JsonReader in) throws IOException
          {
              if (in.peek() == JsonToken.NULL)
              {
                  in.nextNull();
                  return null;
              }
              return readTimestamp(in, Timestamp::fromBits);
          }
      };
  
      private interface TimestampFactory<T>
      {
          T create(long msb, long lsb, Id node);
      }
  
      private static <T> T readTimestamp(JsonReader in, TimestampFactory<T> factory) throws IOException
      {
          if (in.peek() == JsonToken.NULL)
          {
              in.nextNull();
              return null;
          }
          in.beginArray();
          long msb = in.nextLong();
          long lsb = in.nextLong();
          Id node = ID_ADAPTER.read(in);
          in.endArray();
          return factory.create(msb, lsb, node);
      }
  
      private static void writeTimestamp(JsonWriter out, Timestamp timestamp) throws IOException
      {
          if (timestamp == null)
          {
              out.nullValue();
              return;
          }
          out.beginArray();
          out.value(timestamp.msb);
          out.value(timestamp.lsb);
          ID_ADAPTER.write(out, timestamp.node);
          out.endArray();
      }
  
      public static final TypeAdapter<TxnId> TXNID_ADAPTER = new TypeAdapter<TxnId>()
      {
          @Override
          public void write(JsonWriter out, TxnId value) throws IOException
          {
              writeTimestamp(out, value);
          }
  
          @Override
          public TxnId read(JsonReader in) throws IOException
          {
              return readTimestamp(in, TxnId::fromBits);
          }
      };
  
      public static final TypeAdapter<Ballot> BALLOT_ADAPTER = new TypeAdapter<Ballot>()
      {
          @Override
          public void write(JsonWriter out, Ballot value) throws IOException
          {
              writeTimestamp(out, value);
          }
  
          @Override
          public Ballot read(JsonReader in) throws IOException
          {
              return readTimestamp(in, Ballot::fromBits);
          }
      };
  
  
      public static final TypeAdapter<Keys> KEYS_ADAPTER = new TypeAdapter<Keys>()
      {
          @Override
          public void write(JsonWriter out, Keys value) throws IOException
          {
              out.beginArray();
              for (Key key : value)
                  ((MaelstromKey)key).datum.write(out);
              out.endArray();
          }
  
          @Override
          public Keys read(JsonReader in) throws IOException
          {
              List<Key> keys = new ArrayList<>();
              in.beginArray();
              while (in.hasNext())
                  keys.add(MaelstromKey.readKey(in));
              in.endArray();
              return Keys.of(keys);
          }
      };
  
      public static final TypeAdapter<Txn> TXN_ADAPTER = new TypeAdapter<Txn>()
      {
          @Override
          public void write(JsonWriter out, Txn txn) throws IOException
          {
              if (txn == null)
              {
                  out.nullValue();
                  return;
              }
  
              Keys keys = (Keys)txn.keys();
              MaelstromRead read = (MaelstromRead) txn.read();
              MaelstromUpdate update = (MaelstromUpdate) txn.update();
  
              out.beginObject();
              out.name("r");
              out.beginArray();
              for (int i = 0 ; i < keys.size() ; ++i)
              {
                  MaelstromKey.Key key = (MaelstromKey.Key) keys.get(i);
                  if (read.keys.indexOf(key) >= 0)
                  {
                      key.datum.write(out);
                  }
              }
              out.endArray();
              out.name("append");
              out.beginArray();
              for (int i = 0 ; i < keys.size() ; ++i)
              {
                  MaelstromKey key = (MaelstromKey) keys.get(i);
                  if (update != null && update.containsKey(key))
                  {
                      out.beginArray();
                      key.datum.write(out);
                      update.get(key).write(out);
                      out.endArray();
                  }
              }
              out.endArray();
              out.name("client");
              out.value(((MaelstromQuery)txn.query()).client.id);
              out.name("requestId");
              out.value(((MaelstromQuery)txn.query()).requestId);
              out.endObject();
          }
  
          @Override
          public Txn read(JsonReader in) throws IOException
          {
              if (in.peek() == JsonToken.NULL)
                  return null;
  
              NavigableSet<Key> buildReadKeys = new TreeSet<>();
              NavigableSet<Key> buildKeys = new TreeSet<>();
              MaelstromUpdate update = new MaelstromUpdate();
  
              Node.Id client = null;
              long requestId = Long.MIN_VALUE;
              in.beginObject();
              while (in.hasNext())
              {
                  String kind = in.nextName();
                  switch (kind)
                  {
                      default: throw illegalState("Invalid kind: " + kind);
                      case "r":
                          in.beginArray();
                          while (in.hasNext())
                              buildReadKeys.add(MaelstromKey.readKey(in));
                          in.endArray();
                          break;
                      case "append":
                          in.beginArray();
                          while (in.hasNext())
                          {
                              in.beginArray();
                              Key key = MaelstromKey.readKey(in);
                              buildKeys.add(key);
                              Value append = Value.read(in);
                              update.put(key, append);
                              in.endArray();
                          }
                          in.endArray();
                          break;
                      case "client":
                          client = ID_ADAPTER.read(in);
                          break;
                      case "requestId":
                          requestId = in.nextLong();
                          break;
                  }
              }
              in.endObject();
  
              if (client == null)
                  throw new IllegalStateException();
  
              buildKeys.addAll(buildReadKeys);
              Keys readKeys = new Keys(buildReadKeys);
              Keys keys = new Keys(buildKeys);
              MaelstromRead read = new MaelstromRead(readKeys, keys);
              MaelstromQuery query = new MaelstromQuery(client, requestId);
  
              return new Txn.InMemory(keys, read, query, update);
          }
      };
  
      public static final TypeAdapter<Deps> DEPS_ADAPTER = new TypeAdapter<Deps>()
      {
          @Override
          public void write(JsonWriter out, Deps value) throws IOException
          {
              out.beginObject();
              out.name("keyDeps");
              out.beginArray();
              for (Map.Entry<RoutingKey, TxnId> e : value.keyDeps)
              {
                  out.beginArray();
                  ((MaelstromKey.Routing)e.getKey()).datum.write(out);
                  GSON.toJson(e.getValue(), TxnId.class, out);
                  out.endArray();
              }
              out.endArray();
              out.name("rangeDeps");
              out.beginArray();
              for (Map.Entry<Range, TxnId> e : value.rangeDeps)
              {
                  out.beginArray();
                  ((MaelstromKey.Routing)e.getKey().start()).datum.write(out);
                  ((MaelstromKey.Routing)e.getKey().end()).datum.write(out);
                  GSON.toJson(e.getValue(), TxnId.class, out);
                  out.endArray();
              }
              out.endArray();
              out.endObject();
          }
  
          @Override
          public Deps read(JsonReader in) throws IOException
          {
              KeyDeps keyDeps = KeyDeps.NONE;
              KeyDeps directKeyDeps = KeyDeps.NONE;
              RangeDeps rangeDeps = RangeDeps.NONE;
              in.beginObject();
              while (in.hasNext())
              {
                  String name;
                  switch (name = in.nextName())
                  {
                      case "keyDeps":
                      {
                          try (KeyDeps.Builder builder = KeyDeps.builder())
                          {
                              in.beginArray();
                              while (in.hasNext())
                              {
                                  in.beginArray();
                                  RoutingKey key = MaelstromKey.readRouting(in);
                                  TxnId txnId = GSON.fromJson(in, TxnId.class);
                                  builder.add(key, txnId);
                                  in.endArray();
                              }
                              in.endArray();
                              keyDeps = builder.build();
                          }
                      }
                      break;
                      case "directKeyDeps":
                      {
                          try (KeyDeps.Builder builder = KeyDeps.builder())
                          {
                              in.beginArray();
                              while (in.hasNext())
                              {
                                  in.beginArray();
                                  RoutingKey key = MaelstromKey.readRouting(in);
                                  TxnId txnId = GSON.fromJson(in, TxnId.class);
                                  builder.add(key, txnId);
                                  in.endArray();
                              }
                              in.endArray();
                              directKeyDeps = builder.build();
                          }
                      }
                      break;
                      case "rangeDeps":
                      {
                          try (RangeDeps.BuilderByRange builder = RangeDeps.builderByRange())
                          {
                              in.beginArray();
                              while (in.hasNext())
                              {
                                  in.beginArray();
                                  RoutingKey start = MaelstromKey.readRouting(in);
                                  RoutingKey end = MaelstromKey.readRouting(in);
                                  TxnId txnId = GSON.fromJson(in, TxnId.class);
                                  builder.add(new MaelstromKey.Range(start, end), txnId);
                                  in.endArray();
                              }
                              in.endArray();
                              rangeDeps = builder.build();
                          }
                      }
                      break;
                      default: throw new AssertionError("Unknown name: " + name);
                  }
              }
              in.endObject();
              return new Deps(keyDeps, rangeDeps);
          }
      };
  
      public static final TypeAdapter<Writes> TXN_WRITES_ADAPTER = new TypeAdapter<Writes>()
      {
          @Override
          public void write(JsonWriter out, Writes value) throws IOException
          {
              if (value == null)
              {
                  out.nullValue();
                  return;
              }
              out.beginObject();
              out.name("txnId");
              GSON.toJson(value.txnId, TxnId.class, out);
              out.name("executeAt");
              GSON.toJson(value.executeAt, Timestamp.class, out);
              out.name("keys");
              Keys keys = (Keys) value.keys;
              KEYS_ADAPTER.write(out, keys);
              out.name("writes");
              MaelstromWrite write = (MaelstromWrite) value.write;
              out.beginArray();
              for (Key key : keys)
              {
                  Value append = write.get(key);
                  if (append == null) out.nullValue();
                  else append.write(out);
              }
              out.endArray();
              out.endObject();
          }
  
          @Override
          public Writes read(JsonReader in) throws IOException
          {
              if (in.peek() == JsonToken.NULL)
                  return null;
  
              in.beginObject();
              TxnId txnId = null;
              Timestamp executeAt = null;
              Keys keys = null;
              List<Value> writes = null;
              while (in.hasNext())
              {
                  switch (in.nextName())
                  {
                      default: throw new IllegalStateException();
                      case "txnId":
                          txnId = GSON.fromJson(in, TxnId.class);
                          break;
                      case "executeAt":
                          executeAt = GSON.fromJson(in, Timestamp.class);
                          break;
                      case "keys":
                          keys = KEYS_ADAPTER.read(in);
                          break;
                      case "writes":
                          writes = new ArrayList<>();
                          in.beginArray();
                          while (in.hasNext())
                              writes.add(Value.read(in));
                          in.endArray();
                          break;
                  }
              }
              in.endObject();
  
              MaelstromWrite write = new MaelstromWrite();
              if (writes != null)
              {
                  for (int i = 0 ; i < writes.size() ; ++i)
                  {
                      if (writes.get(i) != null)
                          write.put(keys.get(i), writes.get(i));
                  }
              }
              return new Writes(txnId, executeAt, keys, write);
          }
      };
  
      public static final TypeAdapter<ReadOk> READ_OK_ADAPTER = new TypeAdapter<ReadOk>()
      {
          @Override
          public void write(JsonWriter out, ReadOk value) throws IOException
          {
              out.beginArray();
              if (value.data != null)
              {
                  for (Map.Entry<Key, Value> e : ((MaelstromData)value.data).entrySet())
                  {
                      out.beginArray();
                      ((MaelstromKey)e.getKey()).datum.write(out);
                      e.getValue().write(out);
                      out.endArray();
                  }
              }
              out.endArray();
          }
  
          @Override
          public ReadOk read(JsonReader in) throws IOException
          {
              MaelstromData result = new MaelstromData();
              in.beginArray();
              while (in.hasNext())
              {
                  in.beginArray();
                  Key key = MaelstromKey.readKey(in);
                  Value value = Value.read(in);
                  result.put(key, value);
                  in.endArray();
              }
              in.endArray();
              return new ReadOk(Ranges.EMPTY, result, 0);
          }
      };
  
      static final TypeAdapter FAIL = new TypeAdapter()
      {
          @Override
          public void write(JsonWriter out, Object value)
          {
              throw new UnsupportedOperationException();
          }
  
          @Override
          public Object read(JsonReader in)
          {
              throw new UnsupportedOperationException();
          }
      };
  
      static
      {
          GSON = new GsonBuilder().registerTypeAdapter(Packet.class, Packet.GSON_ADAPTER)
                                  .registerTypeAdapter(Id.class, ID_ADAPTER)
                                  .registerTypeAdapter(Txn.class, TXN_ADAPTER)
                                  .registerTypeAdapter(Ballot.class, BALLOT_ADAPTER)
                                  .registerTypeAdapter(TxnId.class, TXNID_ADAPTER)
                                  .registerTypeAdapter(Timestamp.class, TIMESTAMP_ADAPTER)
                                  .registerTypeAdapter(Datum.class, Datum.GSON_ADAPTER)
                                  .registerTypeAdapter(MaelstromKey.Key.class, MaelstromKey.GSON_KEY_ADAPTER)
                                  .registerTypeAdapter(MaelstromKey.Routing.class, MaelstromKey.GSON_ROUTING_ADAPTER)
                                  .registerTypeAdapter(Value.class, Value.GSON_ADAPTER)
                                  .registerTypeAdapter(Writes.class, TXN_WRITES_ADAPTER)
                                  .registerTypeAdapter(MaelstromResult.class, MaelstromResult.GSON_ADAPTER)
                                  .registerTypeAdapter(ReadOk.class, READ_OK_ADAPTER)
                                  .registerTypeAdapter(Deps.class, Json.DEPS_ADAPTER)
                                  .registerTypeAdapter(Keys.class, KEYS_ADAPTER)
                                  .registerTypeAdapter(Body.class, Body.FAIL_READ)
                                  .registerTypeAdapter(Result.class, MaelstromResult.GSON_ADAPTER)
                                  .registerTypeAdapter(MaelstromRequest.class, Body.FAIL_READ)
                                  .registerTypeAdapter(MaelstromReply.class, Body.FAIL_READ)
                                  .create();
      }
  
  }
  