/**
 * Copyright 2013-2014 David Rusek <dave dot rusek at gmail dot com>
 *
 * 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 org.robotninjas.barge.jaxrs;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
import com.google.common.collect.Lists;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import jersey.repackaged.com.google.common.collect.Iterables;
import org.robotninjas.barge.api.AppendEntries;
import org.robotninjas.barge.api.AppendEntriesResponse;
import org.robotninjas.barge.api.Entry;
import org.robotninjas.barge.api.RequestVote;
import org.robotninjas.barge.api.RequestVoteResponse;

/**
 * Utility methods for configuring Jackson.
 */
public abstract class Jackson {


  /**
   * @return a new Object mapper with configured deserializer for barge' object model.
   */
  public static ObjectMapper objectMapper() {
    ObjectMapper mapper = new ObjectMapper();
    SimpleModule raftMessagesModule = new SimpleModule("MyModule", new Version(0, 1, 0, null, "org.robotninjas", "barge"))
        .addDeserializer(RequestVote.class, new RequestVoteDeserializer())
        .addDeserializer(HttpClusterConfig.class, new HttpClusterConfigDeserializer())
        .addDeserializer(HttpReplica.class, new HttpReplicaDeserializer())
        .addDeserializer(RequestVoteResponse.class, new RequestVoteResponseDeserializer())
        .addDeserializer(AppendEntries.class, new AppendEntriesDeserializer())
        .addDeserializer(AppendEntriesResponse.class, new AppendEntriesResponseDeserializer());
    mapper.registerModule(raftMessagesModule);
    return mapper;
  }

  /**
   * @return a suitable provider with configured deserialization objects.
   */
  public static JacksonJaxbJsonProvider customJacksonProvider() {
    JacksonJaxbJsonProvider provider = new JacksonJaxbJsonProvider();
    provider.setMapper(objectMapper());
    return provider;
  }

  private static class RequestVoteDeserializer extends StdDeserializer<RequestVote> {

    protected RequestVoteDeserializer() {
      super(RequestVote.class);
    }

    /**
     * @see <a href="http://www.cowtowncoder.com/blog/archives/2009/01/entry_132.html">Jackson Documentation</a>
     */
    @Override
    public RequestVote deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
      RequestVote.Builder builder = RequestVote.newBuilder();

      while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
        String fieldName = jsonParser.getCurrentName();

        jsonParser.nextToken();

        switch (fieldName) {
          case "term":
            builder.setTerm(jsonParser.getLongValue());
            break;
          case "candidateId":
            builder.setCandidateId(jsonParser.getText());
            break;
          case "lastLogIndex":
            builder.setLastLogIndex(jsonParser.getLongValue());
            break;
          case "lastLogTerm":
            builder.setLastLogTerm(jsonParser.getLongValue());
            break;
        }
      }
      
      return builder.build();
    }

  }

  private static class RequestVoteResponseDeserializer extends StdDeserializer<RequestVoteResponse> {

    public RequestVoteResponseDeserializer() {
      super(RequestVoteResponse.class);
    }


    /**
     * @see <a href="http://www.cowtowncoder.com/blog/archives/2009/01/entry_132.html">Jackson Documentation</a>
     */
    @Override
    public RequestVoteResponse deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
      RequestVoteResponse.Builder builder = RequestVoteResponse.newBuilder();

      while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
        String fieldName = jsonParser.getCurrentName();

        jsonParser.nextToken();

        if (fieldName.equals("term")) {
          builder.setTerm(jsonParser.getLongValue());
        } else if (fieldName.equals("voteGranted")) {
          builder.setVoteGranted(jsonParser.getBooleanValue());
        }
      }

      return builder.build();
    }
  }


  private static class AppendEntriesDeserializer extends StdDeserializer<AppendEntries> {
    public AppendEntriesDeserializer() {
      super(AppendEntries.class);
    }

    @Override
    public AppendEntries deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
      AppendEntries.Builder builder = AppendEntries.newBuilder();

      while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
        String fieldName = jsonParser.getCurrentName();

        JsonToken token = jsonParser.nextToken();

        switch (fieldName) {
          case "term":
            builder.setTerm(jsonParser.getLongValue());
            break;
          case "prevLogIndex":
            builder.setPrevLogIndex(jsonParser.getLongValue());
            break;
          case "prevLogTerm":
            builder.setPrevLogTerm(jsonParser.getLongValue());
            break;
          case "commitIndex":
            builder.setCommitIndex(jsonParser.getLongValue());
            break;
          case "leaderId":
            builder.setLeaderId(jsonParser.getText());
            break;
          case "entriesList":
            if (token != JsonToken.START_ARRAY) {
              throw new IOException("entriesList should be an array, got " + token);
            }
            while (jsonParser.nextToken() != JsonToken.END_ARRAY) {
              builder.addEntry(deserializeEntry(jsonParser));
            }
            break;
        }
      }

      return builder.build();
    }

    private Entry deserializeEntry(JsonParser jsonParser) throws IOException {
      Entry.Builder builder = Entry.newBuilder();


      while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
        String fieldName = jsonParser.getCurrentName();

        jsonParser.nextToken();

        if (fieldName.equals("term")) {
          builder.setTerm(jsonParser.getLongValue());
        } else if (fieldName.equals("command")) {
          builder.setCommand(jsonParser.getBinaryValue());
        }
      }

      return builder.build();
    }
  }

  private static class AppendEntriesResponseDeserializer extends StdDeserializer<AppendEntriesResponse> {
    protected AppendEntriesResponseDeserializer() {
      super(AppendEntriesResponse.class);
    }

    @Override
    public AppendEntriesResponse deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
      AppendEntriesResponse.Builder builder = AppendEntriesResponse.newBuilder();

      while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
        String fieldName = jsonParser.getCurrentName();

        jsonParser.nextToken();

        switch (fieldName) {
          case "term":
            builder.setTerm(jsonParser.getLongValue());
            break;
          case "success":
            builder.setSuccess(jsonParser.getBooleanValue());
            break;
          case "lastLogIndex":
            builder.setLastLogIndex(jsonParser.getLongValue());
            break;
        }
      }

      return builder.build();
    }
  }

  private static class HttpClusterConfigDeserializer extends JsonDeserializer<HttpClusterConfig> {
    @Override
    public HttpClusterConfig deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException, JsonProcessingException {
      List<HttpReplica> replicas = Lists.newArrayList();

      while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
        String fieldName = jsonParser.getCurrentName();

        JsonToken token = jsonParser.nextToken();

        switch (fieldName) {
          case "cluster":
            if (token != JsonToken.START_ARRAY) {
              throw new IOException("cluster should be an array, got " + token);
            }
            while (jsonParser.nextToken() != JsonToken.END_ARRAY) {
              replicas.add(jsonParser.readValueAs(HttpReplica.class));
            }
            break;
        }
      }

      return new HttpClusterConfig(replicas.get(0), Iterables.toArray(Iterables.skip(replicas, 1), HttpReplica.class));
    }
  }

  private static class HttpReplicaDeserializer extends JsonDeserializer<HttpReplica> {
    @Override
    public HttpReplica deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException, JsonProcessingException {
      URI uri = null;

      while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
        String fieldName = jsonParser.getCurrentName();

        jsonParser.nextToken();

        switch (fieldName) {
          case "uri":
            String uriString = jsonParser.getValueAsString();
            try {
              uri = new URI(uriString);
            } catch (URISyntaxException e) {
              throw new IllegalStateException("failed to build proper URI from parsed uri field " + uriString);
            }
            break;
        }
      }

      return new HttpReplica(uri);

    }
  }
}